一笔交易复制一份,区块哈希就能变:比特币默克尔树的先天缺陷与三道补丁
比特币区块头里那个 32 字节的默克尔根,长期背负着一段著名公案:2012 年的 CVE-2012-2459。问题的根源藏在 Bitcoin Core 计算默克尔根的算法里——当一层的哈希个数是奇数时,最后一个哈希会被复制一份再两两配对。这个”补一个尾巴”的做法在普通默克尔树里很少见,它让两笔不同的交易列表可以算出同一个根,进而让同一个区块存在多种合法编码。本文全部以 Bitcoin Core v31.0 源码为准,拆解这个缺陷为什么存在、链上历史为什么不能被直接修掉、以及节点用哪三道防御把它按住。
奇数复制为什么会出事
src/consensus/merkle.cpp 的源码注释里画了两棵树一模一样的对照图:交易列表 [1,2,3,4,5,6] 和 [1,2,3,4,5,6,5,6] 会算出同一个根。原因是第二棵树在把 6 个哈希配对之后只剩 3 个,第一棵树只剩 4 个;树继续往下长,第一棵树的 4 个变成 2 个、2 个变成 1 个,第二棵树的 3 个则先复制尾巴变成 4 个——接下来两棵树的输入序列完全相同。更糟的是”复制最后一份”规则还意味着:把某一层最后两个相同哈希合并的运算,和把单个哈希自己和自己合并的运算结果一样。攻击者因此可以拿着一份交易被悄悄增删过的区块广播:默克尔根没变、区块头没变、区块哈希也没变,但交易列表变了。当年如果接收节点把这种校验失败的区块直接标记为永久无效,还会顺手拒收后来传来的那份未经修改的正版区块——这是拒绝服务的一半伤害。
历史不能改,规则只能补
直接取消奇数复制,会改变历史上所有奇数交易数区块的根哈希吗?不会更糟的是另一面:早期客户端生成的某些区块,其默克尔根本身就是按这套带缺陷的算法算出来写进链里的,任何规则改动都要先过”已共识区块必须继续合法”这一关。所以社区的路线是不动计算函数、在验证侧加检测。ComputeMerkleRoot 多了一个出参 mutated:每层配对前先扫描是否存在相邻两个哈希相同,有则置位。src/validation.cpp 里区块检查一旦发现 mutated 为真,就以 bad-txns-duplicate 的理由、BLOCK_MUTATED 分类拒绝这个区块——不再接受”根对得上但交易列表被动过手脚”的块。
隔离见证之后的两道新检查
缺陷在隔离见证时代又长出了新芽:交易的见证数据不参与 txid 计算,一份带见证的交易可以任意改写见证里的垃圾数据而不改 txid,默克尔树层面的去重检查就够不着它了。v31.0 源码里的 IsBlockMutated 函数把防御继续加宽。其一,若区块里没有合法的 coinbase 交易(本来就已无效的场景),任何一笔无见证序列化长度恰好等于 64 字节的交易都会被直接判为变异块——源码注释引用了 2019 年邮件列表那篇《比特币默克尔树构造的弱点》,指出 64 字节交易是奇数复制攻击的最小原料。其二,CheckWitnessMalleation 校验见证承诺:coinbase 输出里的承诺必须等于”区块见证默克尔根与 32 字节保留值”的二次哈希,保留值尺寸不对报 bad-witness-nonce-size,承诺对不上报 bad-witness-merkle-match,无见证承诺的区块里混进任何见证数据同样按变异处理。注意这些检查的效力边界:源码注释明确 64 字节那条不是共识规则,因为它只作用于本来就无 coinbase 的无效块。
压缩区块路径上的同一道门
变异检测不止在完整区块校验里。节点之间走 BIP152 压缩区块时,接收方要把短交易 ID 还原成完整交易再重算默克尔根,src/blockencodings.cpp 在重建区块后同样调用 IsBlockMutated(带 segwit 激活状态决定要不要查见证根)。两条路径共用一个函数,意味着无论攻击者递的是整块还是压缩块,“根相同、内容被换”的把戏都会在同一层被识破。
普通人该关心什么
对只跑节点不写代码的读者,这件事的意义是一条排障线索:日志里出现 Block mutated 或上述短代号时,问题不在你的磁盘或网络,而在对方递来的区块本身;节点拒的是那个块,不是那条链。对设计新链的人,这是一课反面教材:默克尔树”奇数补尾巴”的便利,换来的是整条验证链都要背着变异检测走——现代方案更常见的是在树结构里直接编码叶子位置,让 [A,A] 与 [A] 不可能同根。
风险提示:本文为共识机制科普,函数名与拒绝理由以 Bitcoin Core 当前版本源码为准,可能随版本调整;不构成投资建议。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。