getRecentPrioritizationFees 返回最近 slot 窗口内按 slot 采样的数据,每个样本是一对 { slot, prioritizationFee },其中 prioritizationFee 是付费交易在该 slot 设置的每计算单元微 lamports 费用,而非总费用。全链范围的简单调用可能返回全零数组,或返回被非付费交易主导的样本,因此稳健的估算器必须显式处理无样本和全零情况,而不是盲目取平均。将调用限定到程序的可写热账户,能获得比全链样本更相关的信号;从排序后的值中选取百分位数(成本敏感时用 p50,追求更快打包时用 p75–p90)比取均值更稳健。将选定的每计算单元微 lamports 与交易的 compute-unit 上限相乘,即可得到实际额外 lamports;由于样本每个 slot 都会变化,需以较短间隔重新获取。本文用 Node.js 构建本地估算器,演示如何在 VersionedTransaction 上设置 SetComputeUnitPrice,并提供可复现的结果表和诚实的局限性说明。
getRecentPrioritizationFees 实际返回什么
Solana JSON-RPC 方法 getRecentPrioritizationFees 返回最近 slot 窗口内按 slot 采样的数据。每个样本是一对 { slot, prioritizationFee },其中 prioritizationFee 是付费交易在该 slot 设置的每计算单元微 lamports 费用——不是以 lamports 计的总费用。Solana getRecentPrioritizationFees 参考文档记录了参数和单位,Solana 优先费用指南解释了每计算单元微 lamports 与 SetComputeUnitPrice 的关系。
该方法接受可选的 addresses 参数。提供该参数时,节点会将样本限定到交易触及这些可写账户的 slot。这一点很重要,因为全链样本混合了所有程序的费用行为,而限定到程序的热账户则能给出反映你的交易实际将面临的争用的信号。Solana 优先费用估算与计算单元价格一文阐述了计算单元价格的概念;本文则聚焦于构建估算器本身。
由于响应是最近 slot 的窗口,它本质上是一个快照。样本每个 slot 都会变化,因此任何缓存值都会迅速过时。应将数组视为短期观测值,而非稳定的配置值。
- 每个样本:{ slot, prioritizationFee }。
- prioritizationFee 单位:每计算单元微 lamports。
- 可选的 addresses 参数将样本限定到触及这些可写账户的 slot。
- 窗口是最近的,且每个 slot 都会变化。
为什么全链范围的简单调用不可靠
不带 addresses 参数的简单调用可能返回全零数组,或返回被非付费交易主导的样本。这是一个众所周知的陷阱:许多交易不设置优先费用,因此全链分布严重偏向零。对该分布取平均得到的数字虽然技术上源自数据,但在争用情况下对打包几乎没有实际用处。
第二种失败模式是空结果或全零结果。如果每个样本都是零,均值就是零,简单的估算器会设置零优先费用。稳健的估算器必须显式处理“无样本”和“全零”——例如回退到配置的下限,而不是输出零。
第三种失败模式是作用域不匹配。全链样本可能看起来健康,而你特定的可写账户却很冷清,反之亦然。将作用域限定到交易将写入的账户能给出更相关的信号,但也可能返回更少的样本,这使得显式处理空数组更加重要。
- 全零数组:在少数交易支付优先费用时很常见。
- 非付费主导:均值被拉向零。
- 作用域不匹配:全链信号可能不反映你账户的争用。
- 必须显式处理无样本和全零情况。
将样本限定到可写热账户
addresses 参数接受一个可写账户列表。节点仅返回交易触及这些账户的 slot 的样本。对于只有一个热状态账户的程序,传入该账户可将样本聚焦到真正重要的争用上。对于有多个热账户的程序,可以传入多个,但要注意返回的窗口可能更稀疏。
限定作用域并非没有代价:范围过窄可能返回更少的样本,在冷清时段甚至可能返回空。这就是为什么估算器必须将空的作用域结果视为回退信号——要么回退到更宽的作用域,要么回退到配置的下限——而不是当作零费用。
当你使用托管端点(如 OnFinality 的 Solana 网络)时,该方法反映的是节点对最近 slot 的视图。不同端点可能返回不同样本,因此估算器应针对你实际发送请求的端点进行验证。
- 传入交易将触及的可写账户。
- 范围过窄可能返回更少甚至零样本。
- 作用域样本为空时回退到更宽范围或下限。
- 针对你实际发送请求的端点进行验证。
选择百分位数而非均值
一旦获得非空且非全零的样本,就对 prioritizationFee 值排序并选取百分位数。均值对异常值和零值密集的尾部很敏感;百分位数是你所关心的分布的更稳定概括。当成本敏感占主导且可接受较慢打包时,p50 是合适的。当更快打包比成本更重要时,p75 到 p90 是合适的。
选定百分位数后,应用下限和上限。下限防止零或接近零的结果产生在争用下无法被打包的交易。上限防止单个异常样本产生荒谬的费用。这两个边界都应是可按程序调整的配置值,而不是埋在估算器里的硬编码常量。
所选值的单位是每计算单元微 lamports。要将其转换为用户支付的额外 lamports,需乘以交易的 compute-unit 上限再除以一百万。该额外金额会加到基础费用上,也是任何面向用户的估算中应显示的数字。
- 对 prioritizationFee 值排序,然后选取 p50、p75 或 p90。
- 应用下限以避免零费用交易。
- 应用上限以限制异常值影响。
- 额外 lamports = 每计算单元微 lamports × compute-unit 上限 ÷ 1,000,000。
时效性、刷新频率与陈旧检测
样本每个 slot 都会变化,因此将费用缓存数分钟是正确性缺陷,而非优化。应以较短间隔或在每次发送前立即重新获取。如果必须缓存,请以 slot 而非分钟为单位衡量缓存时长,并积极失效。
通过读取当前 slot 并将其与返回样本中的最大 slot 比较,来检测样本窗口是否陈旧。如果差距超过你的容忍度,就将该窗口视为陈旧并重新获取。Solana 承诺级别与交易确认一文解释了承诺级别如何影响节点对“最近”的判定。
刷新频率与速率限制相互影响。如果每次发送前都重新获取,你的请求量会随交易量成比例增长。Solana RPC 超时与重试和 Solana RPC 延迟指南两篇文章涵盖了运维方面;估算器应将重新获取失败视为使用上次已知良好值加下限的理由,而不是发送零费用。
- 以较短间隔或在每次发送前重新获取。
- 比较当前 slot 与最大样本 slot 以检测陈旧。
- 重新获取失败时,使用上次已知良好值加下限。
- 请求量随交易量成比例增长。
可运行的 Node.js 估算器:带与不带地址作用域
以下示例调用 getRecentPrioritizationFees 两次——一次全链范围,一次限定到可写账户——过滤掉参考零样本,计算 p50、p75 和 p90,并打印结果。它使用 JSON-RPC 2.0 规范中描述的标准 JSON-RPC 2.0 请求结构,以及在适用情况下以太坊 JSON-RPC 的 method/params 结构约定。
请将端点和作用域地址替换为你自己的。该示例刻意不断言任何延迟或吞吐量数字;它仅演示请求和百分位数计算。
const ENDPOINT = process.env.SOLANA_RPC_URL || 'https://your-endpoint.example';
const SCOPED_ADDRESS = process.env.SCOPED_ADDRESS || 'YourWritableAccountPubkey';
async function rpc(method, params) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
function percentile(sorted, p) {
if (sorted.length === 0) return null;
const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
return sorted[idx];
}
function summarize(samples) {
const fees = samples
.map(s => s.prioritizationFee)
.filter(f => typeof f === 'number' && f > 0)
.sort((a, b) => a - b);
if (fees.length === 0) return { count: 0, p50: null, p75: null, p90: null };
return {
count: fees.length,
p50: percentile(fees, 50),
p75: percentile(fees, 75),
p90: percentile(fees, 90)
};
}
(async () => {
const chainWide = await rpc('getRecentPrioritizationFees', []);
const scoped = await rpc('getRecentPrioritizationFees', [[SCOPED_ADDRESS]]);
console.log('chain-wide', summarize(chainWide));
console.log('scoped', summarize(scoped));
})();在 VersionedTransaction 上设置 SetComputeUnitPrice
选定的每计算单元微 lamports 值通过 SetComputeUnitPrice 指令应用到交易上,该指令属于 compute budget 程序。Solana 优先费用指南记录了该指令。下面的示例构建一个 VersionedTransaction,设置 compute unit 上限和 compute unit 价格,并打印得到的每 CU 微 lamports。
注意,compute unit 上限和 compute unit 价格是分开的设置。上限限制交易可消耗的计算量;价格设定你为每单位支付多少。用户支付的额外 lamports 是两者之积除以一百万。
const { Connection, PublicKey, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } = require('@solana/web3.js');
const ENDPOINT = process.env.SOLANA_RPC_URL || 'https://your-endpoint.example';
const connection = new Connection(ENDPOINT, 'confirmed');
async function buildTx(payer, instructions, microLamportsPerCU, computeUnitLimit) {
const { blockhash } = await connection.getLatestBlockhash('confirmed');
const message = new TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: microLamportsPerCU }),
...instructions
]
}).compileToV0Message();
const tx = new VersionedTransaction(message);
console.log('micro-lamports per CU:', microLamportsPerCU);
console.log('compute unit limit:', computeUnitLimit);
console.log('extra lamports:', Math.floor((microLamportsPerCU * computeUnitLimit) / 1_000_000));
return tx;
}
module.exports = { buildTx };针对你的端点的可复现结果表
由于该方法反映本地节点对最近 slot 的视图,不同端点可能返回不同样本。要比较端点或调整估算器,请用你自己的测量值填写下表。不要将任何已发布的数字当作针对你实际发送请求的端点进行测量的替代品。
以固定频率运行估算器,记录样本数和百分位数,并记下当前 slot 和最大样本 slot,以便计算陈旧程度。在多个间隔上重复,以观察方差。
- 端点:你调用的 RPC URL。
- 作用域:全链范围或所用的可写账户。
- 样本数:非零 prioritizationFee 值的数量。
- p50 / p75 / p90:每 CU 微 lamports。
- 当前 slot 与最大样本 slot:用于判断陈旧。
- 选定费用与下限/上限:你实际设置的值。
局限性与权衡
这些值的单位是每计算单元微 lamports,切勿与总 lamports 混淆。介于 p50 和 p90 之间的费用仍不能保证在拥堵下被打包;它是启发式方法,而非契约。该方法反映本地节点对最近 slot 的视图,因此不同端点可能返回不同样本,且限定作用域的调用可能比全链调用返回更少的样本。
通过 simulateTransaction 进行预检是与费用市场不同的信号。成功的模拟说明交易很可能执行;它并不说明费用是否有竞争力。反之,有竞争力的费用也不保证交易能成功模拟。应将它们视为独立的输入。
最后,估算器的好坏取决于其刷新频率。陈旧的窗口产生陈旧的费用,零费用回退产生的交易可能永远无法上链。下限和上限是防止估算器输出病态值的安全护栏。
- 每 CU 微 lamports ≠ 总 lamports。
- p50–p90 不保证被打包。
- 端点视图不同;作用域样本可能稀疏。
- 预检与费用市场是独立信号。
- 陈旧窗口和零回退是主要失败模式。
排查常见估算器故障
当估算器返回零时,首先检查原始响应是空数组还是全零数组。如果是空数组,你的作用域可能太窄,或端点可能落后。如果是全零,样本被非付费交易主导;扩大作用域或提高下限。
当估算器返回的值看起来过高时,检查是否有单个异常样本,并确认已应用上限。当交易尽管费用有竞争力却未被打包时,检查 compute unit 上限——低估的上限可能导致交易在费用起作用之前就失败。Solana API 指南(RPC Assistant)涵盖了端点级诊断,OnFinality Learn 中心汇集了相关运维指南。
当不同端点结果不同时,这是预期之中的。将两者都记录在结果表中,并决定你将通过哪个端点发送。如果你需要行为一致的托管端点,请查看 RPC 定价和 API 服务选项。
- 零结果:区分空数组与全零数组。
- 过高:检查异常值并确认上限。
- 未被打包:检查 compute unit 上限和预检。
- 端点差异:记录两者并选择你的发送路径。
生产部署的后续步骤
将估算器放到一个小型服务后面,以较短间隔重新获取,应用下限和上限,并将选定的每计算单元微 lamports 暴露给交易构建器。记录样本数、百分位数和陈旧程度,以便诊断回归。针对你实际发送请求的端点进行验证,并随着程序争用的变化重新审视下限和上限。
关于相关运维主题,请参阅 Solana 优先费用估算与计算单元价格一文了解概念框架,以及 Solana RPC 超时与重试和 Solana RPC 延迟指南两篇文章了解传输层。当你准备比较端点时,OnFinality Solana 网络页面和 RPC 定价是起点。
- 将估算器封装到一个小型重新获取服务中。
- 记录样本数、百分位数和陈旧程度。
- 针对你的发送端点进行验证。
- 随着争用变化重新审视下限和上限。