Solana commitment 级别:决定成交率的那一行配置

读取、发送、对账分别该用哪个 commitment,以及为什么三处共用一个默认值在两个方向上都是错的。

BoltTx Team··14 min read
solanacommitmentrpc交易上链确认交易机器人

大多数 Solana 代码库在 Connection 构造函数里设一次 commitment,之后再也不想它。而这一个选择随后被套用到了读取、发送、确认和对账上 —— 四种情况,四个不同的正确答案。

★选错的代价是不对称的。★ 太弱,你会基于一个之后被回滚的状态行动;太强,你在为自己并不需要的确定性等待若干 slot。

三个级别分别是什么意思

级别 含义 会被回滚吗
processed ★某个验证节点执行了它★ 会
confirmed 超级多数已对该区块投票 极少
finalized 上面已经又建了 31 个以上区块 ★不会★

processed 是你能拿到的最快的东西,也是最不确定的。finalized 是确定性,代价大约是三十个区块的等待。confirmed 夹在中间,也是大多数应用逻辑该待的地方。

★confirmed 和 finalized 之间的实际差距,远大于 processed 和 confirmed 之间的差距。★ 正是这个不对称,让"反正用 finalized 保险"成为一个比大家以为的更贵的默认值。

三种情况,三个答案

读取你将据以行动的状态:confirmed。

const connection = new Connection(url, "confirmed");

这个级别的回滚罕见到大多数交易逻辑可以当噪音处理,而延迟是可以接受的。

检测你要抢的事件:processed。

connection.onProgramAccountChange(
  PROGRAM_ID,
  (info) => handleLaunch(info),
  "processed",              // ★能拿到的最快★
);

如果你在抢新盘,等 confirmed 意味着你比那些不等的人更晚知道。★接受一小部分可能被回滚,并在做任何不可逆的事情之前先验证。★

对账与记账:finalized。

const tx = await connection.getTransaction(sig, {
  commitment: "finalized",             // ★不会再变★
  maxSupportedTransactionVersion: 0,
});

任何会变成报表里一个数字的东西,都该在 finalized 读。用 confirmed 的数据算出来的盈亏,是一个仍然可能改变的数字。

如果你的 commitment 都选对了、交易还是落得晚,免费领个 BoltTx key 改一行就能测测提交路径。

Preflight 有它自己的 commitment

这是被完全忽略的那个设置:

await connection.sendRawTransaction(raw, {
  skipPreflight: false,
  preflightCommitment: "processed",     // ★和连接的默认值是分开的★
});

如果你留着 skipPreflight: false、而 preflightCommitment 默认成了 finalized,★你的模拟跑的是大约三十个区块之前的状态。★ 一笔对当前状态完全有效的交易,可能因为它需要的账户是二十个区块前才创建的而没过 preflight。

这会产出 Solana 开发里最让人困惑的一类报告:"这笔交易过一分钟重试就好了,没人说得清为什么。"

两个修法,而第二个通常更好:

让 preflight 和你的发送 commitment 对齐。 如果你是对着当前状态发送,就用 processed。

干脆跳过 preflight。 对交易场景来说这通常是对的 —— 它省掉一次往返,而且它模拟的本来就是错的那个 slot。

确认不该用 finalized

一个常见且昂贵的写法:

// ★为了一件你已经知道的事,等大约 30 个区块。★
await connection.confirmTransaction(sig, "finalized");

对交易机器人来说这基本总是错的。等它返回的时候机会已经没了,而你一直在阻塞等待一个下一步动作并不需要的确定性。

// 按 confirmed 轮询,到过期为止。
const { value } = await connection.getSignatureStatuses([sig]);
const st = value[0];

if (st?.confirmationStatus === "confirmed" || st?.confirmationStatus === "finalized") {
  return st.err ? "reverted" : "success";
}

★getSignatureStatuses 会返回它达到的级别,所以你可以逐次调用再决定,而不是提前锁死一个。★ 这比 confirmTransaction 有用 —— 后者替你把决定做了。

processed 真正咬人的地方

回滚风险是真实的,而且它集中在一种情况:基于尚未确认的状态做不可逆的事。

// ★危险的形状。★
const balance = await connection.getBalance(user, "processed");
await creditUserInDatabase(balance);      // 链下,不可逆

如果那个区块被回滚,链会忘记,而你的数据库不会。于是你的记录和现实不一致,而且没有任何东西会自动纠正它。

保证安全的规则是:★用 processed 决定要做什么,用 confirmed 或 finalized 记录发生了什么。★ 在 processed 上做检测没问题,因为最坏情况是浪费一笔交易;在 processed 上记账不行,因为最坏情况是一个永久性的不一致。

commitment 也影响你的 blockhash

getLatestBlockhash 接受 commitment 参数,而它改变你的交易能有效多久:

// ★blockhash 越旧,剩下的有效窗口越短。★
const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash("confirmed");

processed 的 blockhash 是最新的,意味着它前面有完整的有效窗口 —— 但它来自一个仍可能被回滚的区块,而针对一个被回滚的 blockhash 签名的交易会被拒绝。

finalized 的 blockhash 是安全的,但已经是三十个区块之前的了,所以你在发送之前就烧掉了五分之一的窗口。

★这里值得选的默认值是 confirmed:已经有超级多数投过票,回滚罕见到可以按不会发生来规划;代价只是让出窗口里的几个区块。★

一条流程里混用多个 commitment

一个真实的交易流程会用到三个不同级别,而这是正确的,不是随意:

// 1. 快速检测。
subscribeToLaunches("processed");

// 2. 动钱之前先验证。
const pool = await connection.getAccountInfo(poolAddress, "confirmed");
if (!isValid(pool)) return;

// 3. 针对一个留有重试余地的 blockhash 签名。
const { blockhash, lastValidBlockHeight } =
  await connection.getLatestBlockhash("confirmed");

// 4. 不走 preflight 发送。
const sig = await connection.sendRawTransaction(raw, {
  skipPreflight: true,
  maxRetries: 0,
});

// 5. 按 confirmed 收敛,到过期为止。
const outcome = await resolve(sig, lastValidBlockHeight);

// 6. 按 finalized 记账。
if (outcome.status === "success") {
  await recordForAccounting(sig, "finalized");
}

★在 Connection 上设一个 commitment 然后到处继承,正是同时制造出两种失败模式的做法★ —— 检测太慢,记账太弱。

上链应该是什么水平

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

★commitment 决定的是你什么时候得知交易落块了,不是它什么时候落块。★ 这是两个不同的问题,把它们混淆,正是有些团队以为"把确认级别调低让机器人变快了"的原因。

BoltTx 在这里的位置

我们负责提交。commitment 是客户端的选择,完全归你。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。你的读取和确认保持你现在的配置就行。

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

免费领 API key,没有月费:

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

常见问题

processed、confirmed、finalized 有什么区别? processed 是某个验证节点执行了它,仍可能被回滚。confirmed 是超级多数对该区块投过票,极少被推翻。finalized 是上面又建了 31 个以上区块,不会再变。

Solana 交易机器人该用哪个 commitment? 按用途分别选:检测用 processed,据以行动的读取和 blockhash 用 confirmed,对账用 finalized。一个全局设置对三者中至少两个是错的。

用 processed 安全吗? 用来决定做什么,安全 —— 最坏情况是浪费一笔交易。用来记录发生了什么,不安全,因为一次回滚会让你的链下记录和链永久性地对不上。

我的交易为什么 preflight 失败、重试却成功? 多半是 preflightCommitment 默认成了 finalized,于是模拟跑的是大约三十个区块之前的状态。一个刚创建不久的账户,从那个视角看还不存在。

confirmTransaction 该用 finalized 吗? 很少该。它为了大多数交易逻辑并不需要的确定性,要等大约三十个区块。改成按 confirmed 轮询 getSignatureStatuses,并到 blockhash 过期为止。

getLatestBlockhash 该用哪个 commitment? 大多数情况用 confirmed。processed 给的 blockhash 最新但有被回滚的风险,finalized 已经是三十个区块之前的,在你发送之前就花掉了一部分有效窗口。

Solana 上 finalized 要多久? 区块产出之后,大约还需要 31 个区块的额外确认。这正是它属于对账、而不属于热路径的原因。

commitment 级别会影响交易速度吗? 不会。它影响的是你什么时候得知结果,不是交易多快到达出块方。 落块速度来自手续费、路由和重试行为。

confirmed 的交易会被回滚吗? 有可能但极其罕见,因为那需要一个被超级多数投过票的区块被丢弃。对任何会变成永久记录的东西,finalized 才是不带这种风险的级别。

preflightCommitment 是什么,为什么重要? 它是你的 preflight 模拟所依据的 commitment,和连接的默认值是分开的。设得太高,它会对着陈旧状态模拟,并拒绝掉本来有效的交易。

commitment 该设在 Connection 上还是逐次调用设? 在 Connection 上设一个合理的默认值,在重要的地方逐次覆盖。大多数方法都接受 commitment 参数,而关键调用值得显式写出来。

WebSocket 订阅该用哪个 commitment? 抢跑时用 processed,因为等 confirmed 意味着你比不等的人更晚知道。动钱之前再用更高级别验证。

为什么我在 processed 下看到的余额不对? 因为你可能读到了一个之后被回滚的区块。用于展示的余额可以容忍这一点,用于记账或给用户上账的余额不行。

commitment 会影响 getSignatureStatuses 吗? 响应会告诉你这个签名达到了哪个级别,所以你可以逐次调用再决定。这比 confirmTransaction 灵活,后者提前把决定固定死了。

如果我的 blockhash 来自一个被回滚的区块会怎样? 交易会被判为无效并拒绝,因为它引用的 blockhash 不在规范链上。这正是用 processed 取 getLatestBlockhash 的风险。

交易所充值需要 finalized 吗? 任何在链下给出真实价值的场景都需要。那恰恰是一次回滚会让你的系统和链永久不一致的情况。

读取该怎么选 commitment? 问一句:如果这个值最后是错的,会发生什么。 如果答案是浪费一笔交易,processed 就够;如果答案是一条必须人工纠正的永久记录,就用 finalized。

延伸阅读

← 返回博客列表