Substrate 节点的交易池并不是 EVM 的 mempool:每个外部交易都会根据运行时提供的有效性进行校验,因此它可能处于 ready、future、in-block 状态,也可能因 nonce 过期、tag 被禁用或 mortality 有效期已过而被丢弃。author_pendingExtrinsics 返回当前池中的外部交易,以 SCALE 编码的十六进制表示,且不保证顺序,因此你必须依据运行时元数据解码,而不能把它们当作地址来读取。system_accountNextIndex 返回该账户接下来应使用的 nonce,它反映的是池中已排队加上已上链的部分,而链上 System.Account 的 nonce 只反映已上链的部分。比较两者,并在每次轮询时重新读取交易池,是区分真正等待中的外部交易与被拒绝或丢弃的外部交易最经济的方式。本文记录了读取路径、三种失败特征,以及一个可在你自己的端点运行的有界诊断循环。
为什么 Substrate 交易池不是 EVM 的 mempool
在基于账户的 EVM 链上,交易通常进入 mempool 并等待打包;交易池在很大程度上只是一个按手续费排序的暂存区。Substrate 的工作方式不同。当外部交易被提交时,节点会请求运行时对其进行校验,运行时返回一个有效性判定,其中包含优先级、一组 tag 以及一个 longevity(era)窗口。随后交易池将该外部交易归类为 ready、future、in-block,或者直接将其丢弃。这意味着外部交易可能在交易池边界就被拒绝,原因与 gas 价格毫无关系。
实际后果是,只轮询自己提交哈希的调用方对大部分发生的事情一无所知。该外部交易可能因 nonce 缺口而滞留在 future 队列中,可能因 mortality 有效期已过而被丢弃,也可能因具有相同 tag 的兄弟外部交易失败而被禁用。仅凭提交响应无法看到这些状态中的任何一种。关于该模型的权威描述见 Substrate 交易格式文档,其中定义了交易池所依据的 nonce、mortality 和有效性字段。
因此,直接读取交易池,而不是从自己的提交中推断,才是正确的诊断姿态。交易池读取告诉你节点当前持有什么;账户索引告诉你节点接下来期望的 nonce。两者结合,回答了提交教程从未回答的问题:我的外部交易是在等待、已打包,还是已经消失?
- Ready:当前有效,有资格被打包进下一个区块。
- Future:只有在更早的 nonce 被打包后才有效,因此它滞留在缺口之后等待。
- In-block:已被选入正在构建或最近导入的区块。
- Dropped:因无效、过期或被失败的兄弟交易禁用而被移除。
使用 author_pendingExtrinsics 读取交易池
主要的读取方法是 author_pendingExtrinsics,记录在 polkadot.js 的 author 方法 JSON-RPC 参考中。它返回当前池中的外部交易,形式为 SCALE 编码的十六进制字符串数组。它不返回地址、nonce 或人类可读的调用数据,且数组顺序不保证反映打包顺序。请将响应视为不透明字节,必须依据你所查询的那条链的运行时元数据进行解码。
由于编码是 SCALE,解码需要链的元数据,这也是为什么该读取天然需要配合一个解码步骤。如果你需要把十六进制转换为签名者、nonce 和调用,配套文章解码 Polkadot 外部交易提交错误详细介绍了解码路径。具体到交易池读取,你关心的字段是签名者账户、nonce 和 era,因为这三者驱动了下文所有诊断。
在构建任何上层逻辑之前,一个最小的 curl 调用就足以确认该方法在你的端点上可达。响应是一个 JSON-RPC 2.0 结果对象,其 result 是十六进制字符串数组;空数组表示交易池当前为空,这在空闲链上是有效且常见的状态。
curl -s https://your-polkadot-endpoint.example \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "author_pendingExtrinsics",
"params": []
}'system_accountNextIndex 与链上 nonce
第二个读取方法是 system_accountNextIndex,它接收一个 SS58 地址并返回该账户接下来应使用的 nonce。其语义是整个诊断的关键。从 System.Account 存储中读取的链上 nonce 反映的是已打包进区块的内容。system_accountNextIndex 反映的是交易池已排队加上已打包的内容。当交易处于待处理状态时,这两个值合理地不同,而这种差异并不是错误。
这是区分卡住的交易与从未提交的交易最经济的方式。如果 accountNextIndex 大于链上 nonce,节点认为它为该账户排队了工作。如果两者相等,节点没有排队任何内容,这意味着你的外部交易要么从未被接受,要么已经被丢弃。你可以通过 Polkadot state_queryStorageAt 存储变更中描述的存储路径读取链上 nonce,也可以通过任何暴露 System.Account 的客户端读取。
一个值得内化的细节:accountNextIndex 是一个感知交易池的视图,因此如果排队的外部交易被丢弃,它可能回退。不要缓存它并假设其单调递增。在每次轮询时与交易池一起重新读取它,并记录两个值及时间戳,这样当发生丢弃时你能看到状态转变。
curl -s https://your-polkadot-endpoint.example \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "system_accountNextIndex",
"params": ["15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5"]
}'三种失败特征及其区分方法
一旦你掌握了交易池内容和账户索引,三种特征几乎覆盖了所有卡住外部交易的报告。第一种是真正等待中的外部交易:它出现在 author_pendingExtrinsics 中,且其解码后的 nonce 等于链上 nonce,意味着它是下一个,只是尚未被打包。这是正常的,除非链停滞,否则会自行解决,你可以通过 Polkadot 系统健康与同步状态监控来检查。
第二种是被拒绝或丢弃的外部交易:它既不在交易池中,也不在链上。这是最让调用方困惑的特征,因为提交调用可能返回了成功。该外部交易被接纳入交易池后又因某种原因被移除,最常见的是其 mortality 有效期已过,或者具有相同 tag 的兄弟交易失败导致该 tag 被禁用。第三种是派发失败:外部交易被打包进区块,但其事件显示失败,这是运行时执行问题而非交易池问题,属于派发错误解码路径。
一旦你拥有全部三种读取,区分就是机械性的。存在于交易池且 nonce 相等意味着等待。既不在交易池也不在链上意味着在检查 nonce 后用新的 era 重新提交。已打包但事件失败意味着解码派发错误并修复调用。
- 等待中:在交易池中,解码后的 nonce 等于链上 nonce。
- 已丢弃或被拒绝:不在交易池中,不在链上,accountNextIndex 可能已回退。
- 派发失败:已打包进区块,存在失败事件,参见派发错误文章。
- Nonce 缺口:以 future 状态在交易池中,解码后的 nonce 大于链上 nonce 加一。
Mortality、过期 tag 与静默消失
Mortality 是长期排队的外部交易静默消失的常见原因。每个外部交易都携带一个 era,限定它在多少个区块内保持有效。当该窗口过去后,交易池会丢弃该外部交易,而由于这种丢弃不是对你原始提交的错误响应,没有任何东西会通知你。该外部交易只是不再出现在 author_pendingExtrinsics 中,accountNextIndex 则向链上 nonce 回退。
Substrate 交易格式文档定义了产生此行为的 era 和有效性模型。操作上的要点是:长期待处理的外部交易不是稳定状态;它有一个截止时间。如果你在构建重试循环,era 是你必须尊重的时钟,而交易池读取是你观察截止时间过去的方式。一个已经待处理跨越多个区块的 mortal 外部交易是过期的候选,而不是耐心等待的候选。
过期 tag 相关但不同:它是在交易池重新校验外部交易时该交易不再有效而施加的,原因可能超出 era 过期,包括 nonce 已被另一条路径消耗。无论哪种情况,可观察到的现象相同:外部交易离开交易池,却没有对应的打包。
带结果表的有界诊断循环
可靠的模式是一个有界循环,为每个外部交易记录一个小元组,并在每次轮询时重新读取交易池和账户索引。记录外部交易哈希、解码后的 nonce、首次出现时间戳、当前 accountNextIndex 和链上 nonce。在固定轮询次数或固定墙钟预算后停止,这样循环就不会对停滞的链无限运行。
在你自己的端点上运行此代码,并用你自己的观察填写表格。不要依赖任何文章中的数字,包括本文;交易池行为是链特定且负载特定的。该表格是测量工具,不是基准。
- 结果表列:轮询标签、时间戳、交易池大小、外部交易哈希、解码后的 nonce、accountNextIndex、链上 nonce、判定。
- 判定值:waiting、future、dropped、included-ok、included-failed。
- 用你自己的端点填写表格;将任何已发布的数字仅视为示例。
const ENDPOINT = 'https://your-polkadot-endpoint.example';
const ADDRESS = '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5';
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;
}
async function poll(tag) {
const [pool, nextIndex] = await Promise.all([
rpc('author_pendingExtrinsics', []),
rpc('system_accountNextIndex', [ADDRESS])
]);
console.log(JSON.stringify({
tag,
at: new Date().toISOString(),
poolSize: pool.length,
accountNextIndex: nextIndex
}));
}
(async () => {
for (let i = 0; i < 10; i++) {
await poll('poll-' + i);
await new Promise(r => setTimeout(r, 6000));
}
})();依据运行时元数据解码交易池条目
author_pendingExtrinsics 返回十六进制,而十六进制只有依据你所查询链的元数据解码后才有意义。一个常见错误是试图把十六进制解析为地址或 JSON 对象;它两者都不是。解码会得到签名者、nonce、era、tip 和调用,而驱动上述诊断的正是 nonce 和 era。
如果你已经在解码提交错误,那么你已拥有大部分机制。解码 Polkadot 外部交易提交错误一文介绍了元数据驱动的解码,同样的方法适用于交易池条目。唯一的区别是,从你的角度看,交易池条目在你解码签名者之前是未签名的,而提交错误是在你已提交的上下文中到达的。
一个操作注意事项:元数据会随运行时升级而变化。固定到旧元数据版本的解码器在升级后可能误读字段。将你的解码固定到读取时 state_getMetadata 返回的元数据,或按计划刷新它。
将交易池读取与手续费和权重上下文配合
交易池状态和费用估算属于不同关注点,但当外部交易因缺口而处于 future 队列时它们会相互作用。如果你在决定是提高 tip 还是重新提交,权重与手续费的上下文很重要,使用 payment_queryInfo 进行 Polkadot 权重到手续费估算一文介绍了如何在提交前为调用定价。交易池读取告诉你调用是否在等待;payment_queryInfo 告诉你替换它的成本。
不要把两者混为一谈。高 tip 不会让 future 队列中的外部交易越过 nonce 缺口;缺口必须先被填补。交易池读取揭示缺口,账户索引确认缺口。只有当外部交易处于 ready 状态且你在竞争打包时,费用上下文才变得相关。
交易池读取的局限与权衡
交易池读取是时间点快照且节点本地的。author_pendingExtrinsics 反映的是你所查询的特定节点的交易池,而不是全网视图。同一负载均衡器后面的两个节点可能返回不同的交易池内容,而正在同步的节点可能返回不反映当前链头的交易池。这是该方法的已记录行为,不是缺陷,但这意味着你不应把单次读取视为对全网的权威。
该方法也不返回顺序保证,也不返回关于外部交易为何在池中的元数据。仅凭响应你无法判断某个条目是 ready 还是 future;你通过 nonce 比较来推断。而且由于交易池受节点配置限制,繁忙节点可能在压力下驱逐条目,这是另一条静默消失的路径,从外部看与 mortality 过期完全相同。
最后,提供商行为各不相同。一些托管端点限制或限流 author_* 方法,一些只暴露子集。请将 author_pendingExtrinsics 的可用性视为有文档但依赖提供商,并在构建对其的依赖之前先在你的端点上验证。
- 节点本地而非全网:两个端点可能不一致。
- 响应中无顺序保证,也无 ready/future 标签。
- 压力下的交易池驱逐,若无额外上下文,与过期无法区分。
- author_* 的可用性和速率限制因提供商而异。
排查常见的交易池读取问题
如果 author_pendingExtrinsics 返回错误而不是数组,该方法很可能在该端点上不可用或受限。对照 polkadot.js JSON-RPC 参考确认方法名和参数,然后针对另一个端点测试。如果数组为空但你预期有条目,检查节点是否已完全同步;正在同步的节点可能没有你预期的交易池状态,这在 Polkadot 系统健康与同步状态监控中有介绍。
如果 accountNextIndex 与链上 nonce 相等,但你确信有外部交易待处理,那么该外部交易不在交易池中。重新检查 era 和你签名时使用的 nonce。低于链上 nonce 的 nonce 是过期 nonce,会在交易池边界被拒绝。远高于链上 nonce 的 nonce 会造成缺口,把外部交易停在 future 队列中。
如果交易池包含你无法解码的条目,你的元数据很可能已过期或来自错误的链。刷新元数据并重试。如果解码成功但 nonce 看起来不对,确认你读取的是签名者字段,而不是调用数据中的某个偏移。
生产监控的后续步骤
自然的下一步是把有界循环变成受监控的信号。将元组(外部交易哈希、nonce、首次出现时间、accountNextIndex、链上 nonce)发送到你的指标系统,并对从 waiting 到 dropped 的转变告警,而不是对绝对交易池大小告警。仅交易池大小噪声很大;状态转变才是可操作的事件。
关于端点选择和方法可用性,请从 Polkadot RPC 端点(RPC Assistant)页面和 Polkadot 网络页面开始。如果你要为生产流量做容量规划,RPC 定价和 API 服务页面描述了托管访问的结构。更多类似的基础设施操作手册位于 OnFinality Learn 中心。
一个合理的生产姿态是:以固定间隔轮询交易池和账户索引,依据新获取的元数据解码,将每个被跟踪的外部交易归类为三种特征之一,并将派发失败路由到解码路径。这完整覆盖了交易池的读取侧,只把提交和费用侧留给配套文章。