区块被拆下来之后,交易先进这个两千万字节的暂存池 图 1
区块被拆下来之后,交易先进这个两千万字节的暂存池 · 图 1

区块被重组挤出主链时,里面那些交易不会直接回到内存池。它们先进一个独立的小容器,等整段重组处理完,再统一决定去留。这个容器叫 DisconnectedBlockTransactions,v31.0 给它的体积上限是两千万字节。

为什么要中间这一步

源码注释把这个设计交代得很清楚。重组期间,理想情况是让旧链上已确认的交易重新变得可挖,因为新链不一定包含它们。但把每拆一个块就立刻往内存池里塞一笔的做法很昂贵:内存池的接受流程包含脚本验证、策略检查、费率与集群限额判定,成本不低。而经验上大多数已确认交易会在新链里再次出现,也就是说这些重录工作大部分是白做的。

于是实现选择是:先按顺序把交易存起来,等新链处理完再一次性处理剩下的。注释里的措辞很直白——等重组结束再统一处理仍未确认的交易。

这个容器还有一个不那么显眼的作用:它是内存池与链状态之间的缓冲。拆块的过程中内存池处于不一致状态,源码在 DisconnectTip 的注释里特意提醒调用方:调用之后内存池是不一致的,被拆块的交易已经进了暂存池,你必须持有主锁再调用一次恢复函数把内存池理顺。

队列顺序不是随便排的

容器内部是一条双向链表加一张按交易哈希索引的表。它的排序规则是:链表前端是最近被确认的交易,也就是靠近链顶那个块的最后一笔;越往后越早。这个顺序不是审美选择,它有两个功能。

第一,内存超限时从前端裁。两千万字节的预算用尽时,最先被请出去的是最近确认的那批交易。这个方向选得很妙:越靠近链顶被确认的交易,在新链里再次出现的概率越低——新链是从分叉点往后另起炉灶的,越靠近旧链顶的部分越可能被整段替换掉。

第二,回灌时从链表尾部开始。恢复函数取出整条队列后倒着遍历,先处理最早被确认的交易。注释解释了这个方向的原因:这样往内存池加时不容易撞上”父交易还不存在”的情况,因为更早确认的交易,其父交易通常也在更早的位置、更早被处理。

超限时不止丢一笔

被体积裁掉的交易不只是从暂存队列里消失。DisconnectTip 里有一段明确的处理:把因超限被逐出的交易连同它们的后代,一起用递归移除的方式从内存池清掉,移除原因标记为重组。

这条规则值得琢磨。被拆块里的交易如果挤爆了预算,节点不仅放弃它,还顺手把它在内存池里孵化出来的孩子也一起删掉——因为父都不在了,孩子在池里就是等着变孤儿的悬空状态。也就是说这个 20 MB 的预算不是”少存几笔旧交易”的软限制,它会向下游传导成真实的删除。

另一边,如果新链的某个块里包含了暂存池中的同一笔交易,恢复流程会把它从队列里剔除:既然新链已经确认了它,就没必要再往内存池塞一遍。

回灌时并不强求成功

重组结束时节点会带一个”要不要放回内存池”的开关调用恢复函数。正常完成重组的路径上传的是肯定值,出错的分支——比如断开某个块失败、或者底层数据库报错——传的是否定值,此时只求把内存池状态对齐,不做恢复。

放回时有几条硬规矩。币基交易一律不回内存池,它们天生不可广播。其余交易重新走一遍接受流程,但会绕过限额检查:这些交易此前已在池中活过,不该因为池容量水位而被这条本为外部输入设的闸门挡回来。接受失败的交易被直接丢弃,注释写的是忽略这些复活交易上的验证错误——重组后本来就可能因为双花、失效或时间锁原因不再合法,节点不会为它们额外做什么。

最后还有一次收尾:把所有成功复活的交易交给内存池的祖先后代关系刷新流程,重建它们在池内的依赖记账。这一步不做,后续替换、打包、按费率截断都会算错。

两千万字节意味着什么

对运营者来说,这个数字的实用意义在于判断”重组会不会让我丢交易”。两千万字节的预算大约能装下三四十兆字节的旧链交易体积,正常规模的小重组——几个块、几十笔交易——远远用不完。真会触发裁剪的场景是罕见的深重组叠加大流量时段,此时你从监控上看到的不是”内存池变空”这种整齐现象,而是某条交易链上的父子一起消失,因为祖先被裁会导致后代被递归删除。

钱包侧看到的现象也要放在一起理解:交易从一到零确认、又可能变成冲突状态,是重组的正常表现。暂存池只是让节点自己在恢复期尽量少做无用功,它不承担”替你把交易重新广播出去”的义务。想让交易回到待打包状态,正确的做法仍然是重新提交,而不是等节点替你安排。

风险提示:本文依据比特币核心 v31.0 源码撰写,容器容量与处理顺序可能随版本调整;重组期间的确认状态变化属于协议行为,本文不构成任何投资建议。