一个签名类型怎么会牵出一段共识史
比特币签名末尾带一个标志字节,告诉验证方”这个签名对交易的哪些部分有效”。SIGHASH_SINGLE 的设计意图是:只锁定与所签输入同序号的那一个输出,其余输出随意。设想一张两输入两输出的交易,第一个输入签 SIGHASH_SINGLE,就锁定了第一个输出——常见于”合约找零对”这类结构。但如果输入序号在输出里找不到对应项呢?比如交易只有一个输出,第二个输入却用了 SIGHASH_SINGLE。
早期实现(中本聪时期的 SignatureHash 函数)遇到这种”序号越界”时,走了一条错误分支:函数返回常数 1(今天的源码里写作 uint256::ONE),而 CHECKSIG 验证逻辑照常拿这个返回值当”待签消息”。任何针对十六进制常数 01 序列的有效签名,都会被节点当作合法签名接受。这个签名与交易内容毫无关联,相当于一个可以无限复用的图章——Matt Corallo 在 2012 年 8 月的开发者邮件列表里专门发出警告:如果有人构造这种畸形交易,签名者的其他UTXO可能被任何人搬走。Peter Todd 2014 年的帖子进一步复盘:同一漏洞还能让不同实现(比如把该分支当异常抛出的替代客户端)对同一个区块意见相左,直接分叉。

后续版本各自怎么补这个洞
这个行为无法用硬分叉抹掉——链上已有数据,共识规则一旦改动就是把历史区块变成无效。社区的处理方式是逐层加围栏。隔离见证(SegWit v0)重写了签名哈希算法:越界的 SIGHASH_SINGLE 不再落到常数 1 的分支,输出承诺字段保持全零。签名仍然绑定 prevout 等交易数据、不再是对常数 1 的可复用图章,但它承诺不了任何输出——等于放弃了”锁定收款对象”这层保护。Taproot(v1)的处理更彻底:在计算签名消息时,若输入序号找不到同号输出,直接判定无效——今天的 Bitcoin Core 源码里对应一行”越界即返回失败”的判断。三代规则摆在一起:legacy 签常数 1、segwit v0 签全零输出哈希、taproot 直接拒绝。同一字节的三种命运,是比特币”不可改历史、只能加规则”治理方式的活标本。
普通用户和开发者各自的防御边界
普通用户的钱包自动选择签名类型,正常收款转账不会触发这种交易。风险集中在两类人身上:手工拼装 raw transaction 的进阶用户,和使用第三方”签名服务”的 multisig 场景。历史上确有攻击者利用对方钱包对 SIGHASH_SINGLE 的滥用,诱导受害者签出可复用的图章签名。防御动作有三条。第一,给任何外部系统提供的待签数据做”验尸”:确认每个输入的 sighash 类型,看到 SIGHASH_SINGLE 就问一句”同序号输出存在吗”。第二,凡是自己构造交易,确认输入数不超过输出数时避免使用 SINGLE。第三,优先选择走隔离见证或 Taproot 地址的钱包实现,两种新格式都切断了常数 1 路径。
还有一条更底层的教训值得所有工程团队抄录:共识关键代码里的错误分支,其代价不是修个 bug 发个补丁,而是要由全生态在未来十几年里不断绕行。写校验逻辑时,对”永远不可能发生”的输入显式返回失败,永远好过默认走下去。
这段历史还教会社区的三件事
第一,实现差异本身就是共识风险。同一个函数返回 1 还是抛异常,让 Bitcoin Core 与替代实现分处两条链——这正是”一个规范、多个实现”时代最尖锐的教训,也解释了后来为什么测试网修正、参考测试向量与跨实现对账被反复强调。第二,错误分支必须显式处理。Peter Todd 复盘里的判断标准值得所有开发者背下来:凡是”看起来到不了”的分支,返回失败并让调用方可见,永远不要依赖调用方检查返回值。第三,修复的形态是分层设防而非一刀切:政策层把越界 SINGLE 标记为不标准、钱包层拒绝签、新地址格式从算法上绕开,三层各自独立生效,任何一层失守都不会全线失守。普通用户看不到这些层,但每次用隔离见证地址付款,都在享用这三层防线的其中一层。
风险提示:本文是协议历史与技术机制科普,不构成投资建议。涉及签名操作的任何异常请求都应中止并复核;对 sighash 类型的更完整梳理可参考钱包文档中 SIGHASH_ALL、NONE、ANYONECANPAY 各档位语义,本文只聚焦越界这一个角落。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。