NULLDUMMY 与多签哑元素:一个空字节如何堵住交易可延展 图 1
NULLDUMMY 与多签哑元素:一个空字节如何堵住交易可延展 · 图 1

比特币脚本里藏着一个从 2010 年就写进共识规则的缺陷:多签操作码 OP_CHECKMULTISIG 在校验完签名后,会额外从栈上弹出一个元素——而这个元素的内容脚本本身从不检查。多年里,这个“哑元素”装过 1、装过任意字节、装过各种恶作剧,它不影响交易有效性,却会改变交易哈希。BIP147(NULLDUMMY 规则)在隔离见证同一轮升级里把这条通道封死:哑元素必须是空字节,否则脚本直接判假。本文按缺陷的一生来写:出生、作恶方式、封修补丁与补刀动机。

缺陷的出生:一次为了对称性的设计

早期实现为了让多签脚本的栈操作在“零个签名”的边界情形下不崩,让 OP_CHECKMULTISIG 在弹出 N 个签名之外再多弹一个占位元素——这是中本聪时代代码留下的实现怪癖,规范文档里它被描述为“历史遗留,需要保持行为一致”。从此每条多签脚本的前面都要塞一个哑值占位,主流钱包塞的是空字节 OP_0;但共识层面从来没有任何规则禁止塞别的东西。

NULLDUMMY 与多签哑元素:一个空字节如何堵住交易可延展 图 2
NULLDUMMY 与多签哑元素:一个空字节如何堵住交易可延展 · 图 2

哑元素如何泄漏交易哈希

比特币交易 ID 是对序列化的交易字节做双重 SHA-256。多签输入的脚本签名字段里,哑元素是可自由替换的字节:把 OP_0 换成 0x01,脚本执行结果不变(那个值反正被丢弃),但序列化字节变了、txid 变了。对接收方而言这是标准的第三方 txid 可变性:未确认交易在内存池里能被中继者动手术,付款凭证上的哈希与最终上链的不一致;更隐蔽的问题是下游把 txid 当唯一键的系统(交易所对账、闪电早期的通道状态)会因此错账。这段历史在闪电网络里催生了一整套 workaround(先冻结交易再交换签名),也推动社区承认“共识层能堵的洞就该共识层堵”。

BIP147 的修法与部署细节

BIP147(作者 Johnson Lau,2016 年 9 月分配)给 OP_CHECKMULTISIG 与 OP_CHECKMULTISIGVERIFY 增加一条新规则:被丢弃的哑元素必须是空字节数组,任何其它内容立即让脚本求值为假。软分叉意味着旧脚本输出依然兼容——本来就塞空字节的钱包毫无感知;部署搭的是隔离见证(BIP9 名 segwit、bit 1)同一班车,主网窗口自 2016 年 11 月 15 日 UTC 零点开始。两条规则分工清楚:隔离见证管的是“签名挪出 txid 计算视野”,NULLDUMMY 管的是“留在 txid 视野里的那个自由字节必须定值”,同一轮升级把多签可变性的两个来源各堵一端。

为什么有了隔离见证还要补这一刀

隔离见证把见证数据挪出 txid 的计算,从结构上解决了大部分“改签名不改语义”的问题——但注意适用范围:不走隔离见证路径的旧式多签,其脚本签名字节仍然在 txid 计算之内,哑元素照样可改。软分叉激活的那段过渡期里,两种交易在网络里并存。更关键的是心智问题:只要还有一条“改字节不改语义”的通道存在,所有依赖 txid 的系统就得永远提防它。BIP147 因此被归类为“交易健全性(transaction soundness)”类规则:防的不是盗窃,是依赖假设崩塌。

快速问答

问:我自己写的多签脚本会受影响吗? 答:正常工具生成的脚本都填空字节,规则生效后无感知;只有手工魔改脚本、故意塞非空哑值才会被拒。

问:它和 BIP66(严格 DER)是同类吗? 答:同类思路:都是“把脚本里语义无关但字节可变的自由度收窄”的软分叉,分别管签名编码与多签哑元素。

问:怎么验证链上某笔多签是否合规? 答:解析脚本签名字段第一个元素是否为空即可;2016 年末以后的网络里,非空哑值基本只出现在历史区块。

风险提示

txid 可变性问题的历史教训是:任何把未确认交易哈希当最终凭证的系统都应设计替换容忍。涉及多签与钱包工程时,请以脚本与共识规则的规范文档为准,勿以本文为实现依据。本文不构成投资建议。