我们如何发现并修复因 nonce 空缺而丢失的交易
在一个四验证者网络的持续负载下,我们发现,一个连续提交交易的发送方可能会丢失其中的大部分。在一次 90 秒的运行中,291 笔转账只有 14 笔得到确认。共识从未面临风险:每个节点上的区块、状态根和余额都完全一致。丢失的是用户的交易。
我们如何发现它
我们把网络测试移进了代码仓库(tools/quorum-net)。每次运行都从创世启动四个验证者,并保留其参数、每个节点的日志和一份摘要。在干净构建上的第一次运行没有任何错误。只有一系列重复运行才表明,这个缺陷是间歇性的,而且比正在进行的工作更早就存在。日志给出了确切的图景:nonce 为 N 的交易尚未到达区块提议者,而 N+1、N+2……已经进入区块,因 nonce 校验失败而被从交易池中丢弃。
第一个原因:如何为区块挑选交易
内存池按 nonce 对发送方的交易排序,却从不检查它们是否紧接已提交的 nonce。我们研究了 go-ethereum(其 pending 与 queued 集合)、Cosmos EVM、Aptos,以及遇到过几乎相同缺陷的 Polygon Bor 是如何处理的。它们共享同一条规则,我们也采纳了它:只有连续的一段 nonce 才能进入区块,位于空缺之后的交易留在池中等待。此后的每一次运行,nonce 错误都为零。
第二个原因,由修复揭示:传输中的丢失
现在,一个从未收到交易 N 的验证者会老老实实地等待,于是它产出的区块是空的。测量显示,每次运行中每个对等节点都会缺失 2–5 笔交易。每条消息都会单独建立一个 TCP 连接,而我们的 DoS 防护——每个地址的连接数限制——切断了其中一部分。修复借鉴了 Bitcoin(反复广播直至送达)和 Solana(每两秒重发一次):节点会将其待处理交易批量重发,直到它们被打包进区块。
第三步:对等节点之间的持久连接
同一限制也在切断共识消息,因此大约一半的高度只能在第二轮才达成决定,停顿最长达 12 秒。借鉴 CometBFT 和 libp2p,现在每个节点与每个对等节点保持一条长连接。DoS 防护保持不变;诚实的对等节点只是不再需要建立数百个连接。
结果
在相同限制下:296 笔转账中 296 笔确认,最长停顿从 12 秒降至 1.8 秒,且没有任何连接被断开。下一步是在对等节点之间引入以 ML-DSA 签名的握手,使限制作用于验证者的身份,而不是 IP 地址。