新消息:Helius 收购 Light Protocol
优先费:了解 Solana 的交易费用机制
博客/基础知识

优先费:了解 Solana 的交易费用机制

Developer Experience EngineerX 上的 0xIchigoLinkedIn 上的 0xIchigoGitHub 上的 0xIchigo
阅读需 10 分钟

本文介绍什么?

Solana 速度很快。然而,即使是在现有最快的区块链上,用户也希望重要交易能得到优化处理。优先费可以确保用户的交易排在执行顺序队列的前列。这是一项可选的额外费用,用户可以将其添加到交易中。

本文简要探讨 Solana 交易处理的细节,包括交易、交易生命周期以及交易费用的运作方式。随后,本文将介绍优先费、如何通过编程方式实现优先费,以及相关最佳实践。

交易及其生命周期

交易用于调用 Solana 程序并改变状态。交易由多条指令组成(即调用单个程序的指令),这些指令会告诉验证者要执行哪些操作、在哪些账户上执行,以及是否具备必要权限。

Solana 交易的一般生命周期如下:

  • 用户明确自己想要执行的操作。例如,Alice 想向 Bob 发送 10 SOL
  • 用户为目标操作生成交易。例如,Alice 创建一笔交易,其中包含将 10 SOL 从她的账户转入 Bob 账户的指令。Alice 还会加入最近的区块哈希,并使用私钥签署交易
  • 用户将交易发送到网络,随后会收到交易是否成功添加的信息。例如,Alice 以 confirmed 承诺状态发送交易。交易确认后,她会收到一个交易签名。Alice 可以在 Orb 等区块浏览器中使用该交易签名,查看她是否已成功向 Bob 发送 10 SOL。她的账户会扣除 10 SOL,而 Bob 的账户会收到 10 SOL

用户向网络发送已签名交易时,会使用 Helius 等 RPC 提供商。Helius 的 RPC 接收交易并检查当前的领导者计划。在 Solana 上,只有特定验证者负责在特定时间向账本添加条目。领导者负责为当前时隙生成区块,并会连续负责四个时隙。已签名交易会发送给当前领导者和接下来的两位领导者。

当前领导者会验证已签名交易,并在安排交易执行前完成其他预处理步骤。与 Ethereum 等其他 L1 不同,Solana 没有全局交易队列。大多数验证者使用 Solana Labs 提供的调度器实现。不过,运行 Jito 验证者客户端的验证者会使用伪内存池(即 MempoolStream)对交易排序。默认调度器采用多线程设计,每个线程都会维护一个等待执行的交易队列。交易会结合先进先出(FIFO)原则和优先费排序到区块中。需要注意的是,这种排序本质上是不确定的,因为交易会以某种随机方式分配给执行线程。

交易执行后,会通过 Turbine 传播,并相应支付费用。

Solana 交易费用的运作方式

交易费用是处理 Solana 交易时支付的小额费用。当前领导者会处理通过网络发送的交易,以生成账本条目。当交易被确认为状态的一部分时,需要支付费用来支持 Solana 的经济设计。交易费用从根本上使 Solana 受益。它们可以:

  • 为验证者提供报酬
  • 通过为交易引入实时成本,减少网络空间占用
  • 通过协议收取的最低费用,为网络提供长期经济稳定性

短期内,Solana 依靠基于通胀机制的协议奖励来保障网络安全。为此,网络设定了全局通胀率,以奖励验证者。长期来看,Solana 则依靠交易费用来维持安全性。每笔交易费用的固定部分(最初设定为 50%)会被销毁,其余部分则发送给当前领导者。Solana 通过销毁费用巩固 SOL 的价值,同时遏制恶意验证者审查交易。

交易费用根据静态设置的每签名基础费用,以及交易期间使用的计算资源来计算,后者以计算单元(CU)衡量。该基础费用的范围可以是每签名目标 lamport 数量的 50% 至 1000%。目前,每签名的默认目标值设为 10,000。每笔交易都会分配一个最大 CU 预算,称为计算预算。如果超出此预算,运行时会停止交易并返回错误。每笔交易的最大预算为 140 万 CU,区块空间上限为 4800 万 CU。

什么是优先费?

由于这些限制,计算量大的交易可能占满区块空间,导致其他交易延迟。Solana 引入了一项可选费用,使交易能够在领导者队列中获得高于其他交易的优先级,这就是优先费。支付这项费用可以有效提升交易优先级,从而缩短执行时间。这对时间敏感或高价值交易非常有用。交易的费用优先级由其请求的计算单元数量决定。交易请求的计算单元越多,为了保持其在交易队列中的优先级,需要支付的费用就越高。对更多计算单元收取更高费用,可以防止计算量大的垃圾交易。

优先费等于交易的计算预算乘以其计算单元价格,价格以 micro-lamport 计:priorityFees = computeBudget * computeUnitPrice

计算预算

computeBudget 指定交易可以消耗的最大计算单元数量、交易可能执行的不同操作所产生的成本,以及交易必须遵守的操作边界。以下操作会产生计算成本:

  • 执行 SBF 指令
  • 在程序之间传递数据
  • 调用系统调用(例如日志记录、创建程序地址、CPI)

对于跨程序调用(CPI),被调用程序会在调用方(即父)程序的计算预算内运行。如果被调用程序耗尽所有剩余计算预算或超出设定上限,整条程序调用链都会失败。这也包括对启动该流程的初始交易的执行。

你可以在此处查看当前的计算预算。

如何通过编程方式实现优先费

交易的优先费通过设置 SetComputeUnitPrice 指令https://github.com/solana-labs/solana/blob/2971e84ec87815adb1e4def95cbcd8d0d96845f5/sdk/src/compute_budget.rs#L59和可选的 SetComputeUnitLimit 指令来确定。如果未提供 SetComputeUnitPrice 指令,由于没有提供额外费用,交易将默认使用最低优先级。如果未提供 SetComputeUnitLimit 指令,则上限将按交易中的指令数量乘以默认计算单元上限计算。运行时使用计算单元价格和计算单元上限来计算优先费,并用该费用确定给定交易的优先级。

要通过编程方式添加优先费,我们必须将这些指令添加到目标交易中。在 Javascript 中,代码如下:

代码
import {
  Keypair,
  Connection,
  PublicKey,
  Transaction,
  SystemProgram,
  LAMPORTS_PER_SOL,
  sendAndConfirmTransaction,
  ComputeBudgetProgram,
} from "@solana/web3.js";

async function main() {
  // Initialize an RPC client
  const clusterUrl = "http://127.0.0.1:8899";
  const connection = new Connection(clusterUrl, "confirmed");

  // Initialize new sender and receiver keypairs
  const fromKeypair = Keypair.generate();
  const toPubkey = new PublicKey(Keypair.generate().publicKey);

  // Airdrop SOL to the from_keypair
  const airdropAmount = 100 * LAMPORTS_PER_SOL;
  try {
    const signature = await connection.requestAirdrop(
      fromKeypair.publicKey,
      airdropAmount
    );
    console.log("Airdrop requested. Signature:", signature);
    await connection.confirmTransaction({
      signature,
      confirmation: "confirmed",
    });
  } catch (e) {
    console.error("Failed to request airdrop:", e);
    return;
  }

  // Check if airdrop was successful
  const balance = await connection.getBalance(fromKeypair.publicKey);
  if (balance < airdropAmount) {
    console.error(
      "Airdrop was not successful. The current balance is insufficient"
    );
    return;
  }

  // Airdrop SOL to the toPubkey
  const airdropAmountTo = 100 * LAMPORTS_PER_SOL; // 1 SOL in lamports
  try {
    const signature = await connection.requestAirdrop(toPubkey, airdropAmount);
    console.log("Airdrop requested. Signature:", signature);
    await connection.confirmTransaction({
      signature,
      confirmation: "confirmed",
    });
  } catch (e) {
    console.error("Failed to request airdrop:", e);
    return;
  }

  // Check if airdrop was successful
  const balanceTo = await connection.getBalance(toPubkey);
  if (balance < airdropAmount) {
    console.error(
      "Airdrop was not successful. The current balance is insufficient"
    );
    return;
  }

  console.log(`Account balance: ${balance / LAMPORTS_PER_SOL} SOL`);

  // Create the priority fee instructions
  const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: 1,
  });

  const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
    units: 200_000,
  });

  // Create the transfer instruction
  const transferIx = SystemProgram.transfer({
    fromPubkey: fromKeypair.publicKey,
    toPubkey,
    lamports: 100_000,
  });

  // Create the transaction with priority fees
  const transaction = new Transaction().add(
    computePriceIx,
    computeLimitIx,
    transferIx
  );

  // Fetch the recent blockhash and sign the transaction
  transaction.recentBlockhash = (
    await connection.getLatestBlockhash()
  ).blockhash;
  transaction.sign(fromKeypair);

  // Send the transaction
  try {
    const txid = await sendAndConfirmTransaction(connection, transaction, [
      fromKeypair,
    ]);
    console.log("Transaction sent successfully with signature", txid);
  } catch (e) {
    console.error("Failed to send transaction:", e);
  }
}

main();

在此代码片段中,我们:

  • 使用 Localhost 搭建测试环境
  • 创建两个新钱包(即 fromKeypair 和 toPubkey)
  • 向两个钱包分别空投 100 SOL
  • 确认空投成功
  • 创建优先费指令(即 computePriceIx 和 computeLimitIx)
  • 创建转账指令,将 100, 000 lamport 从 fromKeypair 发送到 toPubkey
  • 创建新交易,并将所有指令添加到其中
  • 将最新区块哈希附加到交易并签名
  • 发送交易并确认发送成功

就是这样——优先费只是添加到交易中的指令,用于换取更快的执行速度。此代码片段的大部分内容都在配置开发环境和两个用于相互转移 SOL 的钱包。我们真正关注的是创建添加优先费的指令,并将其附加到交易中:

代码
// Other code

const computePriceIx = ComputeBudgetProgram.setComputeUnitPrice({
	microLamports: 1,
});

const computeLimitIx = ComputeBudgetProgram.setComputeUnitLimit({
	units: 200_000,
});

// Other code

const transaction = new Transaction().add(computePriceIx, computeLimitIx, transferIx);

// Rest of the code

最佳实践

这些指令会按顺序执行,因此其顺序非常重要。例如,如果交易在扩展计算上限之前就超出默认计算上限(即 200k CU),交易将失败。假设有以下交易:

  • 指令 1 使用 100k
  • 指令 2 使用 150k
  • 指令 3 扩展计算上限

除非交换指令 2 和指令 3 的位置,否则该交易会失败。因此,建议先添加计算上限指令,再向交易添加其他指令。请记住,如果要向交易添加优先费,你不需要使用 SetComputeLimit 指令——它完全是可选的。SetComputePrice 指令的位置并不重要。

建议使用 getRecentPrioritizationFees RPC 方法获取近期支付的优先费列表。你可以使用这些数据估算合适的交易优先费,确保集群处理交易,同时尽量减少支付的费用。Helius 也提供了新的优先费 API,下一节将对此进行介绍。

交易还应请求执行所需的最少计算单元,以尽量降低这些费用。请注意,当请求的计算单元数量超过交易实际使用的单元总数时,费用不会相应调整。

Phantom 等钱包提供商知道 dApp 可以设置交易优先费。不过,他们并不鼓励这样做,因为这通常会给最终用户带来不必要的复杂性。相反,他们建议 dApp 开发者让 Phantom 代用户添加优先费。例如,Solfare 会自动检测 Solana 是否处于高负载状态,并略微提高费用,让你的交易优先于其他交易。

Helius 优先费 API

使用 getRecentPrioritizationFees RPC 方法按时隙计算优先费在直觉上很合理。然而,由于网络状况不断变化,加上 getRecentPrioritizationFees' 响应的性质,这可能很困难(即返回过去 150 个区块的值列表,而该列表只能用于判断应设置的最低费用)。

Helius 优先费 API 引入了新方法 getPriorityFeeEstimate,该方法会考虑全局和本地费用市场,将响应简化为单个值。此方法使用一组预定义的百分位数来确定估算值。这些百分位数(也称级别)的范围从 NONE(第 0 百分位)到 UNSAFE_MAX(第 100 百分位,并标记为不安全,以防用户意外耗尽资金)。用户还可以选择接收所有优先级别,并通过 lookbackSlots 调整此计算使用的范围。因此,与 getRecentPrioritizationFees 类似,用户可以在估算时回溯一定数量的时隙。

例如,我们可以使用以下脚本计算 Jupiter v6 程序的所有优先费级别:

代码
const url = `https://mainnet.helius-rpc.com/?api-key=`;

const getRecentPrioritizationFees = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "getPriorityFeeEstimate",
      params: [{
        "accountKeys": ["JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"],
        "options": {
            "includeAllPriorityFeeLevels": true,
        }
      }]
    }),
  });
  const data = await response.json();
  console.log("Fee: ", data);
};

getRecentPrioritizationFees();

此端点仍在积极开发中。有关更多信息,请访问 Helius 文档。

总结

深入了解 Solana 交易,可以看到一个在网络效率与经济激励之间取得平衡的复杂系统。了解交易的运作方式、交易费用和优先费,可以帮助开发者和用户做出更明智的决策,优化他们在 Solana 上的交互。通过编程方式实现优先费,为高价值和时间敏感型交易提供了新的可能性,也让 Solana 上的操作更加灵活、高效。

如果你读到了这里,感谢你,匿名朋友!请务必在下方输入你的电子邮箱地址,这样就不会错过 Solana 的任何最新动态。准备继续深入探索了吗?立即加入我们的 Discord,在性能最强的区块链上开始构建未来。

其他资源 / 延伸阅读

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片