
优先费:了解 Solana 的交易费用机制
本文介绍什么?
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 开发的最新动态,并在我们发布新内容时收到更新


