比特币脚本有个古老的分岔路口:收款条件能不能「等花的时候再揭晓」?今天的答案是 P2SH——地址里锁一个脚本哈希,花费时把真正的脚本连同签名一起端上来。但在 2011 年,这条路线的竞争者是 BIP-12 的 OP_EVAL,一个比 P2SH 更激进的设计。它最终状态 Closed,可它提出的问题至今还在脚本语言演进里回响。
OP_EVAL 由 Gavin Andresen 在 2011 年 10 月立项,属于共识层软分叉提案。它的语义一句话:脚本执行到 OP_EVAL 时,弹出栈顶元素,把这段数据反序列化成脚本并立即执行。换句话说,它把「数据当代码」引入了比特币脚本。配套的标准收款形态是 DUP HASH160 {20字节哈希} EQUALVERIFY OP_EVAL:付款方只需知道一段脚本的哈希;花费方在 scriptSig 里给出签名,再附上序列化的脚本本体,旧客户端看到哈希对上就放行,新客户端则继续把那段脚本执行完,真正的消费条件由此展开。
设计上有三处值得细看。第一处是兼容路径:OP_EVAL 直接重定义 OP_NOP1,旧节点把它当空操作,只看哈希校验,因此升级顺序可控。第二处是安全护栏:反序列化出的脚本里禁止出现 OP_CODESEPARATOR;OP_EVAL 允许嵌套但递归深度限为 2;最妙的一条防御是「反向校验」——如果某个含 OP_EVAL 的交易在把 OP_EVAL 当空操作的解释下也会失败,那它必须在新旧两种解释下都失败才算安全,这直接堵住了「新规则放行、旧规则拒绝」造成的分叉。第三处是部署时间表:新实现先把 OP_EVAL 当空操作执行到 2012 年 2 月 1 日,期间请矿工在自己挖的块里嵌字符串「OP_EVAL」以统计算力支持率,若 1 月 15 日前支持率不过半就推迟,明确不过半则放弃。
反对意见集中在两点,而且都写进了文档。「复杂是安全的敌人,把数据解释成代码有悠久的出事故史」——这是最常被引用的一句。文档的辩护其实很坦率:比特币脚本本来就是全网每份实现都要解析解释的小语言,OP_EVAL 只是把被解释的数据换了个位置,「如果脚本语言本身是聪明的还是愚蠢的」这个问题不是它新增的。另一点是一确认攻击:旧客户端把 OP_EVAL 当空操作,攻击者可以造一笔「旧客户端看合法、新客户端看非法」的交易,自己挖一个块,在里面同时放入这笔 OP_EVAL 消费和付给受害商户的下游交易,商户如果按一确认就发货,接下来全网作废攻击者的区块时就顺带吞掉商户那笔钱——即所谓「双花商户」。这个攻击的代价是攻击者要明知故犯地造一个注定被孤立的块,而且本来就违反「大额交易不该收一确认」的常识,所以分析结论是昂贵且难落地。
历史的选择是另一条路线:BIP-16 的 P2SH 同样让收款地址只承诺脚本哈希,但花费时校验的是「这段脚本的哈希等于地址里的哈希」,脚本本身按标准格式解析执行,不存在把任意数据升格为代码的递归解释。P2SH 于 2012 年 2 月以截止时间加哈希率投票的方式激活,OP_EVAL 路线就此出局,BIP-12 关闭。它的精神遗产则留了下来:多签费用从付款方转到收款方的思路、由哈希承诺隐藏脚本的结构,都在 P2SH 与后来的隔离见证里延续;而「数据当代码是否危险」的辩论,每隔几年就会在 OP_CAT、CTV 甚至 Tapscript 的讨论里重新开庭。
常见误区两条。其一,把 OP_EVAL 与 P2SH 说成同一件事:两者都「藏脚本露哈希」,但前者真的执行栈上数据、有递归语义,后者只是哈希等式加一次普通脚本执行,攻击面完全不同。其二,以为 OP_EVAL 失败是因为有漏洞被证实:文档记录的是风险权衡与共识选择,反对者从未演示出实际失陷,它输在「不够必要的复杂性」这一判断上。
快速问答。问:OP_EVAL 重定义的是哪个操作码?答:OP_NOP1,这让旧节点天然向后兼容。问:今天还有什么它的影子吗?答:语义上最接近的现代回声是 Tapscript 里对脚本数据的处理与各种 covenants 提案,但没有哪个现役操作码等同于 OP_EVAL。问:一确认攻击对新节点有效吗?答:无效,新节点会正确拒绝那笔非法消费,攻击赌的是旧客户端商户的宽松验收政策。
风险提示:本文为协议史科普,不构成投资建议;理解任何收款格式的攻击模型后再决定确认策略,大额收款不应依赖一个确认。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。