一、一遍还是两遍,是个设计决定
比特币的命名里就带着哈希:比特币哈希函数(double SHA-256)指对数据连续做两次 SHA-256。区块头挖矿、区块哈希、交易 txid、默克尔树的每个内部节点,走的全是这个双次口径。多烧一遍不是为了”更安全一点”的玄学——论碰撞抗性,多跑一轮对抗碰撞强度的提升可以忽略——而是为了堵一扇结构性的门:SHA-256 这类 Merkle-Damgard 构造的哈希,长度扩展属性让攻击者能在不知道原输入的前提下,合法地算出”原消息再拼接一段数据”后的哈希值。

二、长度扩展是怎么发生的
Merkle-Damgard 结构的内部状态就是一团 256 位的链接变量:哈希函数吃满 512 比特的块就更新一次这团状态,最终输出直接取自状态本身。于是”知道了 H(m) 与 m 的长度”在数学上近似于”知道了消化过程的中间状态”——攻击者把这段中间状态当作起点继续往下消化拼接的数据,就得到 H(m ‖ 填充 ‖ 拼接串),全程不需要知道 m 一个字节。对”以秘密前缀做消息认证”的天真用法,这是致命的:服务器若把 SHA256(secret ‖ message) 当签名,攻击者可以在不改 secret 的情况下伪造一个对应”message 加尾巴”的合法摘要。
三、双哈希顺手关门
把输出再哈希一次,等于切断了”输出即可继续消化的状态”这条捷径:第一次 SHA-256 的 32 字节结果被当作全新输入喂进第二次哈希,其比特在第二次消化前还要再经过一整轮的填充与展开,第一次的状态暴露不再等价于第二次的可续起点。中本聪在协议奠基时选择了全协议统一双哈希,把”要不要在这里防长度扩展”的判断题从每个实现者手里收走,代价只是每次验证多跑 64 轮压缩——对今天每秒亿万次哈希的硬件来说几乎无感。
四、双哈希不是万能胶
要看清边界。对碰撞攻击,双哈希不提供显著增量,密码学上它不是”加强版 SHA-256”。对长度扩展,它是有效且低成本的补救,同类问题在别的体系里另有解法,比如 HMAC 用两次不同填充的消息认证码、或海绵结构的 SHA-3 天生免疫。比特币自己也并非处处双跑:PGP 时间戳、部分脚本内哈希操作如 RIPEMD-160 环节都按各自规范用单次哈希,因为那里的输入结构或用途不暴露可续状态——是否双跑,跟着威胁模型走,而不是跟着习惯走。
五、给读者的迁移教训
这是一个”协议安全源于细节的齐整”的好教材:如果比特币只在交易哈希上双跑、在别处单跑,每个新功能上线时都要重审一遍”这里会不会泄露中间状态”。统一双哈希牺牲了一点优雅,换来了实现者少犯一类错误。今天评估任何新链、新协议时值得问同一类问题:它的哈希用法是逐项威胁建模的结果,还是把”标准做法”囫囵抄来?答得出这一问的设计,多半在别的细节上也经得起追问。
六、在脚本与工具里的痕迹
脚本层的哈希操作码提供了单次的 SHA-256、RIPEMD-160 与 HASH160(后者即先 SHA-256 后 RIPEMD-160,用于地址),它们没有”双跑”变体——双哈希是协议层与工具层的约定,不进脚本。这意味着开发者若把外部协议的数据直接塞进脚本承诺,长度扩展的攻防要按脚本上下文单独评估;好在脚本里的哈希几乎总是配合比较而非续写使用,威胁模型天然错位。另一处痕迹是钱包的扫描逻辑:轻客户端按”双哈希结果即 txid”建索引,任何想改哈希口径的改动都会把全部历史重扫一遍——路径依赖是协议保守主义的经济学解释。给读者的迁移教训再补一条反向的:双哈希堵住了”续写”这一扇门,并没有堵住”碰撞”这扇更大的门——SHA-256 被攻破的那天,比特币要换的是整条哈希流水线加一次硬分叉迁移,而不是把三遍双跑改成三遍单跑。结构防御与算法防御是两条独立的战线,比特币同时押注前者,不意味着后者不重要。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。