Solana 交易一直 pending:getSignatureStatuses 为什么返回 null

三种不同的状态看起来都像 pending,而每一种的正确应对是相反的。重发之前先分清它们。

BoltTx Team··13 min read
solanapending确认交易上链排查rpc

你手上有个签名。getSignatureStatuses 返回 null。什么都没发生。

★"pending"不是一种状态,是三种 —— 而每一种的正确应对都不同,其中有一种,重发是你能做的最糟的事。★

同一个症状背后的三种状态

const { value } = await connection.getSignatureStatuses([sig]);
const st = value[0];

if (st === null) {
  // ★要么从没到达,要么到了正在等。现在还分不清。★
} else if (st.confirmationStatus === "processed") {
  // ★已执行,在等投票。不要重发。★
} else if (st.err) {
  // ★落块并 revert 了。重发相同字节改变不了任何事。★
}
你看到的 真实情况 正确动作
null,blockhash 还有效 ★还没被收录★ ★继续重发★
null,blockhash 已过期 ★永远不会了★ ★重建★
processed,无 err 已执行,在等投票 ★等★
有状态且带 err 落块并 revert ★去修指令★

★中间那两种是最常被搞错的。★ 重发一笔已经执行了的交易,最好的情况也只是白费力气。而等一笔 blockhash 已过期的交易,是永远等下去。

唯一能给出答案的那个判断

一切都取决于一个比较:

const height = await connection.getBlockHeight();

if (height > lastValidBlockHeight) {
  // ★永久无效。没有任何东西能改变这一点。★
  return "expired";
}
// ★还在窗口内 —— 重发是对的。★

★墙上时钟的超时在这里是错误的工具。★ slot 产出速度会变,所以"已经过了三十秒"对"blockhash 是否还有效"什么都说明不了。区块高度是唯一的权威 —— 这也是为什么每次取 blockhash 都必须把 lastValidBlockHeight 一起留着。

如果你的交易是过期而不是 revert,免费领个 BoltTx key 改一行就能测测提交路径。

收敛到一个终态

正确回答这个问题的循环:

async function resolve(connection, sig, lastValidBlockHeight) {
  while (true) {
    const { value } = await connection.getSignatureStatuses([sig]);
    const st = value[0];

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

    const height = await connection.getBlockHeight();
    if (height > lastValidBlockHeight) {
      // ★最后再查一次:它可能在两次调用之间落块了。★
      const final = await connection.getSignatureStatuses([sig]);
      if (final.value[0]) continue;
      return { status: "expired" };
    }

    await sleep(400);
  }
}

★过期之后再查那一次,比它看起来重要得多。★ 在读状态和读高度之间,交易是可能落块的。 不做这最后一次检查就返回 expired 会产出一个假阴性 —— 而如果你的代码据此去重建,你现在就有两笔都可能执行的交易了。

processed 为什么会卡在那儿

一笔处于 processed 的交易,已经在某个验证节点上执行了,但还没被投票。通常一两个 slot 内就会推进。

当它没推进时,原因几乎总是分叉包含你交易的那个区块输了,所以它永远不会被确认,而交易会回到 pending 状态,就像它从没运行过一样

★这正是 processed 不是终态、绝不能当终态记录的原因。★ 一个在 processed 就把交易标记为完成的机器人,偶尔会有一笔"已完成但从没发生过"的交易。

正确的处理方式:processed 当作"仍在途中",让重发循环继续跑,只在 confirmed 或更高级别才认定结果

它为什么从没到达

如果状态在整个窗口里一直是 null,那这笔交易从没进过任何区块。原因都很普通:

当时的手续费太低。 手续费压力是按账户的,而且恰恰在你最想落块的时候飙升

它在传输途中被丢了。 高负载下的节点可以丢弃一笔交易而不告诉你。★这就是"只发一次"不是一种策略的原因★ —— 重发相同的已签名字节是安全的,也是唯一的防御

那个账户被争用了。 写同一个账户的交易会串行化,所以热门池子有一条你正排在里面的队

maxRetries 没设成 0。 RPC 在你的循环底下跑了它自己的重试节奏,于是两个组件按不同的时钟重发,而你们谁也没法推理它

告诉你是哪种问题的诊断方法

要看一批交易,而不是一笔:

metrics.record({ outcome, slotDistance: landSlot - submitSlot });

★两个比率,两种不同的修法:★

过期率高 —— 交易没有到达出块方。去看手续费、重试行为和路由。

revert 率高 —— 它们落块得好好的,是你的指令错了。去看滑点、余额和账户是否存在。

两个都低,但 slot 距离很宽 —— 你在落块,只是晚。那是路由那一半。

★从一笔卡住的交易做诊断是猜;从分布做诊断不是。★

上链应该是什么水平

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

★对着这个数字,一笔过了很多 slot 还 pending 的交易是反常的,不是正常的。★ 知道"典型"长什么样,才能让异常变得可见。

BoltTx 在这里的位置

我们负责提交。确认逻辑留在你的代码里。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。

你在本地签名。我们不托管资金、不代签、不修改交易内容 —— 所以通过我们重发相同字节,产出的是同一个签名,不可能重复执行。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

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

常见问题

我的 Solana 交易为什么一直 pending? 要么还没被收录,要么已执行在等投票,要么从没到达而且 blockhash 已过期。用区块高度和 lastValidBlockHeight 比较来分辨。

怎么知道一笔交易还有没有机会落块?getBlockHeight,和随 blockhash 一起返回的 lastValidBlockHeight 比。过了它,交易就永久无效了,你等多久都没用。

签名状态返回 null 是什么意思? 这笔交易不在节点已知的任何区块里。它同时涵盖"还没"和"永远不会" —— 只有区块高度的比较能区分两者。

重发一笔 pending 的交易安全吗? 安全,前提是字节完全相同。一个签名最多被收录一次,所以持续重发到过期为止是标准做法,不可能重复执行。

我的交易为什么停在 processed? 它在某个验证节点上执行了但还没被投票。如果一直停着,多半是那个区块在分叉里输了,交易会回到 pending,就像从没运行过。

能把 processed 当成已确认吗? 不能。它可能被回滚,所以在 processed 就把交易记为完成,风险是为一件从没发生的事留下一条完成记录。要在 confirmed 或更高才认定。

该等多久才放弃? 等到 blockhash 过期为止,按区块高度衡量而不是按秒。墙上时钟的超时不可靠,因为 slot 产出速度会变。

为什么有时候显示过期、其实交易落块了? 因为它在你查状态和查高度之间落块了。检测到过期之后,一定要再查一次状态,才能下"它从没运行"的结论。

重建一笔已经落块的交易会怎样? 重建出来的有新签名,可能执行第二次。这就是重建任何东西之前必须先把原签名的状态确定下来的原因。

调高优先费能解决 pending 吗? 当原因是争用时有帮助,因为手续费是按账户争的。当原因是指令 revert 或 blockhash 过期时,它一点用都没有。

我的交易为什么彻底消失了? 高负载下的节点可以丢弃一笔交易而不上报。重发相同字节就是防御 —— 这也是"只提交一次"不算策略的原因。

maxRetries 该设成 0 吗? 一般该。不设的话 RPC 会按它自己的节奏重试,而你的循环也在重试 —— 两个组件按不同时钟重发,失败就变得不可复现。

怎么区分 revert 和过期? revert 有一个带 err 的状态对象,意味着它落块了、被某个检查拒绝。过期是在超过区块高度上限后根本没有任何状态,意味着它从没执行。

pending 的交易要花钱吗? pending 期间不花。只有落块才付基础手续费,包括落块并 revert 的情况。过期的交易不消耗手续费,只消耗机会。

能取消一笔 pending 的 Solana 交易吗? 不能。没有取消机制,唯一能终结它的是 blockhash 过期。 durable nonce 的交易是例外,因为烧掉 nonce 就能作废它们。

一笔卡住的交易该记录什么? 签名、提交时的 slot、lastValidBlockHeight,以及每一次状态响应及其时间戳。没有这些,事后就没法重建"到底是过期还是 revert"。

延伸阅读

返回博客列表