Solana compute unit 计价:limit、price,以及那个乘法

setComputeUnitLimit 和 setComputeUnitPrice 怎么相乘成为你的实际费用、默认 limit 为什么在浪费钱,以及怎么测出真实消耗。

BoltTx Team··13 min read
solanacompute-unitpriority-fee费用交易上链优化

有两条指令决定一笔 Solana 交易在基础费之外要花多少钱,而它们是相乘的:

priority fee = computeUnitLimit × computeUnitPrice

大多数人只设了 price,把 limit 留在默认值上。★这个组合是 Solana 上最常见的多付钱方式★,而且它在费用最要紧的那些时刻代价最大。

这两条各是什么

setComputeUnitLimit 声明这笔交易最多可以用多少计算量。它是个上限,不是预留 —— 但 ★你是按这个上限计费的,不是按实际消耗。★

setComputeUnitPrice 设定每个计算单元你付多少 micro-lamport。这才是竞争区块空间的那个旋钮。

import { ComputeBudgetProgram } from "@solana/web3.js";

instructions.unshift(
  ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
);

默认值为什么在坑你

不显式设 limit 的话,Solana 会按指令套用一个很宽松的默认值 —— 远高于大多数交易的实际用量。

接下来的算术很无情。假设你的交易真实消耗 40,000 CU,而默认值是 200,000:

真实用量:     40,000 CU
按此计费:    200,000 CU
★多付:         5 倍★

单价低的时候这是个舍入误差。★但在拥堵时,当"要上链就得付的价格"急剧上涨,你正在最糟的时刻多付五倍。★

把 limit 设对不会让你更快。它让同样的速度更便宜 —— 这意味着在同样的总预算下,你能承受更高的单价。而单价才是真正在竞争的东西。

如果费用已经调好、交易还是抢不到,免费领个 BoltTx key 改一行就能测测路由那一侧。

测出真实消耗

别猜。模拟。

const sim = await connection.simulateTransaction(tx, {
  sigVerify: false,
  replaceRecentBlockhash: true,
});

console.log("consumed:", sim.value.unitsConsumed);

unitsConsumed 就是真实数字。要对真实场景跑 —— 真实的池子、真实的账户状态,不是一个空的 devnet 夹具。

然后带余量设 limit:

const measured = sim.value.unitsConsumed ?? 200_000;

// 余量用来覆盖:会增长的账户状态、更深的 CPI 层级、
// 以及你模拟时没走到的分支。设太紧会让本来能落块的交易失败。
const limit = Math.ceil(measured * 1.2);

★这个余量不是可选的。★ 计算用量随账户状态变化 —— 穿过一个 tick 数据更多的池子做 swap,比穿过一个浅池子贵。把 limit 设成精确的实测值,在现实第一次和模拟不同时就会失败。

各类操作的大致消耗

这是用来给模拟结果做常识校验的,不是拿来写死的值:

操作 数量级
SOL 转账 很低
SPL 代币转账
简单 AMM swap 中等
多跳路由 swap ★高★
带多次 CPI 的清算 ★高★
创建账户 中等

★永远测你自己的。★ 这些数字随程序版本、账户状态、以及 CPI 链的深度而变。

费用是按账户竞争的

这是大多数费用逻辑写错的地方:priority fee 不是全局竞争的,而是按每个可写账户竞争。

// 错:一个和你这笔交易毫无关系的全局数字。
// const recent = await connection.getRecentPrioritizationFees();

// 对:传入你这笔交易实际会写入的账户。
const recent = await connection.getRecentPrioritizationFees({
  lockedWritableAccounts: [poolAccount, yourTokenAccount],
});

const fees = recent.map((r) => r.prioritizationFee).sort((a, b) => a - b);
const median = fees[Math.floor(fees.length / 2)] ?? 0;

★上新时对着热门池子做 swap,和一笔普通转账,面对的竞争完全不同。★ 不传账户列表查出来的数字,哪一种都描述不了。

拼起来

import { ComputeBudgetProgram } from "@solana/web3.js";

async function withComputeBudget(
  connection: Connection,
  instructions: TransactionInstruction[],
  writableAccounts: PublicKey[],
  measuredUnits: number,          // ★来自模拟,不是猜的★
  contentionMultiplier = 2,       // 上新和清算要更高
) {
  const recent = await connection.getRecentPrioritizationFees({
    lockedWritableAccounts: writableAccounts,
  });
  const fees = recent.map((r) => r.prioritizationFee).sort((a, b) => a - b);
  const median = fees[Math.floor(fees.length / 2)] ?? 0;

  return [
    ComputeBudgetProgram.setComputeUnitLimit({
      units: Math.ceil(measuredUnits * 1.2),
    }),
    ComputeBudgetProgram.setComputeUnitPrice({
      microLamports: Math.max(median * contentionMultiplier, 1_000),
    }),
    ...instructions,
  ];
}

关于这个结构有两点。compute budget 指令要放最前面,在你真正的指令之前。另外 contentionMultiplier 是你表达"这一刻竞争有多激烈"的地方 —— 一笔日常转账和一次上新狙击,不该用同一个数字。

失败模式

limit 设太低。 交易在消耗了计算量、付了基础费之后,以超预算错误失败。★这是最糟的结果:你付了钱,什么也没拿到。★

limit 留在默认值。 按比例多付,拥堵时最严重。

price 写死。 网络清闲时浪费,繁忙时不够。两个方向都错。

查费用时不传账户。 你拿到一个全网数字,它描述不了你的竞争。

budget 指令不在最前面。 它们必须排在所适用的指令之前。

它解决不了什么

值得说清楚,因为 compute budget 调优经常被推荐去解决它碰不到的问题。

★把 CU 设对,不会让一笔交易上链。★ 它控制的是你付多少钱,以及避免一种特定的失败模式。如果你的交易不上链,原因是 blockhash 过期、单价不足以应对当前竞争、没有重试循环、或者提交路径在高负载下被降级。

费用调优让每次尝试更便宜、并且在被接收之后更容易被排上。它对"交易能不能及时到达出块方"毫无作用。

上链应该是什么水平

我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。

如果你的费用是按实时行情算的、CU limit 是实测的,而交易在高负载下仍然迟到,那剩下的就是路由。

BoltTx 在这里的位置

我们做路由那一半。不做费用估算,不做索引。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你的 compute budget 指令是你签名的那笔交易的一部分 —— 我们不修改交易内容。

你在本地签名。我们不托管资金、不代签。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

Solana 的 compute unit 是什么? 交易消耗的计算工作量单位。每笔交易声明一个上限,而 priority fee 就是这个上限乘以你设定的每单元价格。

不设 compute unit limit 会怎样? Solana 会套用一个宽松的默认值,而你按这个默认值计费,不是按真实用量。如果你的交易只用了其中一小部分,就按比例多付 —— 拥堵时最严重。

怎么知道我这笔交易的真实计算消耗?simulateTransactionunitsConsumed。要对真实账户状态模拟,不要用空夹具,因为用量随账户里的内容而变。

实测值之上该加多少余量? 20% 左右是个合理起点。计算用量随账户状态和 CPI 深度变化,而把 limit 设成精确实测值,在现实第一次和模拟不同时就会失败。

compute unit limit 设太低会怎样? 交易在消耗了计算量、付了基础费之后以超预算错误失败。这是最糟的结果 —— 付了钱又什么都没落成。

compute unit price 该怎么设对?getRecentPrioritizationFees 传入你会写入的账户查询,然后按"这一刻竞争有多激烈"放大中位数。写死的值在清闲时错、在繁忙时也错。

为什么要给 getRecentPrioritizationFees 传账户? 因为 priority fee 是按可写账户竞争的,不是全局的。对着热门池子做 swap 和一笔普通转账面对的竞争完全不同,而全局查询哪个都描述不了。

把 compute limit 设低能让交易更快吗? 不能。它让同一笔交易更便宜,从而让你在同样预算下能出更高的单价 —— 而单价才是竞争排程的东西

compute unit limit 和 price 有什么区别? limit 是你声明并据此计费的计算量。price 是每单元付多少 micro-lamport。你的 priority fee 是两者相乘。

compute budget 指令该放在交易的哪个位置? 最前面,排在它们所适用的指令之前。放在后面就管不到前面那些。

一笔 swap 要用多少 compute unit? 随程序和账户状态变化 —— 多跳路由的 swap 比简单 swap 贵得多。测你自己的,别依赖别人公布的数字。

compute unit 设置会影响交易能不能上链吗? 只是间接的。它控制成本、避免超预算失败。上链取决于 blockhash 新鲜度、单价相对竞争的高低、重试行为,以及提交路由。

不同交易之间该改 compute unit price 吗? 该,只要竞争程度不同。一笔日常转账和一次上新狙击面对的是完全不同的状况,两者用同一个数字,意味着一边多付、另一边失败

能通过模拟同时拿到费用和用量吗? 模拟给你 unitsConsumed。价格那一侧来自对你可写账户的 getRecentPrioritizationFees。两者要你自己组合。

为什么我的交易实际消耗比模拟的多? 账户状态在模拟和执行之间变了 —— 池子的 tick 数据更多了、CPI 链更长了、某个账户增长了。这正是 limit 需要在实测值之上留余量的原因。

延伸阅读

返回博客列表