OP_CODESEPARATOR 今日还有效吗:FindAndDelete 与 CONST_SCRIPTCODE 关掉的旧路 图 1
OP_CODESEPARATOR 今日还有效吗:FindAndDelete 与 CONST_SCRIPTCODE 关掉的旧路 · 图 1

比特币脚本里有一个早已名存实亡的操作码:OP_CODESEPARATOR。它不在被禁用列表里,语法上随时能写进脚本,但今天任何走标准校验路径的交易,只要在隔离见证之前的脚本形态里用到它,就会被直接拒收。这个结局背后是一段签名可延展性的暗角历史,牵出一个叫 FindAndDelete 的函数和一个叫 CONST_SCRIPTCODE 的校验开关。本文全部机制以比特币核心 v31.0 源码为准。

它本来想解决什么

OP_CODESEPARATOR 的设计意图是切分脚本:验签时,签名承诺的脚本内容只取最后一个已执行的分隔符之后的部分。多签或条件复杂的脚本里,这允许不同签名各自覆盖脚本的一段。意图不算坏,问题出在配套的实现上。

OP_CODESEPARATOR 今日还有效吗:FindAndDelete 与 CONST_SCRIPTCODE 关掉的旧路 图 2
OP_CODESEPARATOR 今日还有效吗:FindAndDelete 与 CONST_SCRIPTCODE 关掉的旧路 · 图 2

FindAndDelete:验签前先改脚本

旧版(隔离见证之前)签名摘要算法有一步特殊处理:计算某条输入该签什么之前,先把脚本里所有已出现的签名字节序列删掉,避免签名互相承诺彼此。v31.0 源码 interpreter.cpp 里这个删字节的函数就是 FindAndDelete,它至今还在,为历史兼容路径服务。麻烦在于这个先删再算的规则创造了一个可动手脚:签名字节在脚本里的排布方式,可以影响删除后剩下的脚本长什么样,进而影响摘要。加上 OP_CODESEPARATOR 本身可以塞在脚本任意位置,一个第三方可延展性家族就此成形——不改签名、只挪字节,交易编号就可能变化。这正是交易延展性问题在脚本层的两个角落。

CONST_SCRIPTCODE:把整条路焊死

后来社区没有选择修补删除逻辑,而是直接封路。v31.0 的 interpreter.h 里有校验标志 SCRIPT_VERIFY_CONST_SCRIPTCODE,注释写得很干脆:让 OP_CODESEPARATOR 和 FindAndDelete 在任何非隔离见证脚本上失败。解释器执行到分隔符操作码时,如果脚本是旧式形态且该标志打开,直接报 SCRIPT_ERR_OP_CODESEPARATOR,连未执行的分支都不放过。而在 policy/policy.h 的标准校验标志集合里,这个标志是常开的——也就是说,只要你的交易想按标准规则被节点接受和验证,非隔离见证脚本里的分隔符就是死路一条。

隔离见证路径本身不受这条影响:见证脚本的摘要算法从一开始就不做删签名这种手术,分隔符在那里没有用武之地,也没有危害。

对今天的读者意味着什么

第一,看到教程教你在旧式脚本里加分隔符来控制签名范围的,可以直接判定过时。第二,写脚本审计工具时,遇到含 OP_CODESEPARATOR 的非隔离见证脚本,正确结论是这笔交易在现行标准政策下无法中继与验证,而不是尝试模拟删除语义。第三,这段历史是理解 BIP-143 隔离见证签名算法设计价值的一个切面:新版摘要把脚本内容明确为直接参与哈希的字段,不做事前删改,从根上取消了这类歧义。

历史脚本、旧工具生成的半成品交易,偶尔仍会在这道闸门前被弹回。报错归因清楚,能省掉很多对着脚本逐字节排错的夜晚。

把三个概念一次分清楚

最后用三句话收拢容易混淆的三层规则:共识层负责交易是否合法,分隔符在旧式脚本上的命运属于这一层的校验标志组合;标准政策层负责交易值不值得中继与打包进块,标准校验标志正是按标准交易的口径常开这个开关;钱包与工具层负责生成与修补脚本,遇到该操作码时应做的是拒绝而不是模拟。历史脚本、旧工具生成的半成品交易,偶尔仍会在这道闸门前被弹回,归因清楚,能省掉很多逐字节排错的夜晚。

一段可以复述的结论

把整件事压缩成三句可以复述的话:分隔符的操作码仍然存在,但标准校验路径已经切断它在非隔离见证脚本里的生路;切断的原因是签名摘要对脚本字节的依赖会产生不想要的不确定性;修补的方式不是继续修旧算法,而是让旧算法不再可达。理解了这个结构,再去读隔离见证签名的十项摘要清单,会发现它是对同一个问题的正面回答。

风险提示:本文只讨论协议与脚本机制,不构成任何投资建议;涉及资金的操作请在测试网验证。