脚本收尾后再执行下一段:BIP-117 尾调用语义与广义 MAST 图 1
脚本收尾后再执行下一段:BIP-117 尾调用语义与广义 MAST · 图 1

程序设计语言里“尾调用”指的是一个函数把活儿交给下一个函数后自己彻底退场,中间不留调用栈。比特币脚本本来没有这种结构:一段脚本执行完,结果留在栈上,交易验证就到此为止。BIP-117 想把尾调用搬进脚本——脚本收尾时,如果栈顶恰好能被读成另一段脚本,解释器接着执行那段脚本,之前所有栈内容原样保留。这份提案 2017 年 8 月起草,作者与 BIP-116 是同一批人,状态同样是 Draft。

先给它划一个准确的边界。BIP-117 不新增任何操作码,改的是脚本“执行结束后”的规则,所以它是一个纯靠语义放宽实现的软分叉,这是它设计上最讨巧的地方:没有新字节要分配,也没有新指令要教节点认识。

执行条件必须逐项说清。脚本正常走完、准备收尾的那一刻,解释器检查栈是否“非干净”:主栈多于一项,或者主栈恰好一项而 altstack 还不空。满足的话,再看主栈顶端的布尔值——必须为真,且这项数据要么不是单字节,要么虽是单字节但落在 0x51 到 0x60 的范围之外。全部满足,就把这一项弹出、按序列化的脚本语法解析,作为下一段脚本执行;其余主栈和 altstack 元素原地不动,充当新脚本的输入。注意“为真”这个前提:结果为假的失败路径不会触发跳转,垃圾数据也不会被随便当脚本跑,因为解析失败就是一次彻底的失败。

第二种情况是特例。如果栈顶正好是一个单字节、值在 OP_1(0x51)到 OP_16(0x60)之间,它就只是一个数量记号:表示接下来要从主栈加 altstack 里再弹出 N 个元素(N 从 2 数到 17),第一个弹出的元素当脚本,后面的当参数。这一支的设计动机是省字节:脚本本身和它要吃的参数可以打包在一起交接,而不是提前把脚本塞死在上一段代码里。

这套语义单独的用处有限,真正的威力和 BIP-116 咬合在一起。设想一个分支极多的合约:过去得写一大段 if 嵌套链,靠 witness 里的选择值一层层往下 cascade,所有分支代码必须同时在场。换成广义 MAST 的布局,每条分支被拆成一条扁平、无分支的小脚本,全部压进一棵默克尔树,锁定的输出里只留树根。花费时先在见证里验一次包含性证明(MBV 负责),验证通过时栈顶正是那条被选中的小脚本,收尾规则(BIP-117 负责)把它顺势执行掉。两段合起来,链上只见用过的那条路径,路径数量在理论上近乎不设上限。

和 BIP-114 的 MAST 提案比,差别在“谁来规定布局”。BIP-114 直接定义一种新输出类型,钱包写入时自动建树,验证时自动拆树,用起来省心,但共识层要多认识一套结构。BIP-116 与 BIP-117 走拼装路线,共识层只加“验分支”和“尾调用”两条小规则,分支布局由合约作者自己设计。2017 年前后这两条路线在扩容讨论里都火过一阵,也都在同一年代被 SegWit 路线吸收掉了注意力,最终全部停在草案书架上。

常见误区先列三个。其一,把脚本层的尾调用和 EVM 里的 JUMPF 划等号:以太坊那个在函数跳转指令层面做类似的事,比特币这个发生在“脚本已经跑完”的时刻,语义位置完全不同。其二,以为任何栈顶数据都会被当脚本执行——布尔为真是硬前提。其三,以为 OP_1 到 OP_16 那一支改变了数字压栈的含义:没有,只是收尾规则多看了它一眼。

快速问答。问:BIP-117 单独启用有意义吗?答:几乎没有,没有 MBV 之类的“把脚本送上栈”的机制,收尾时很难合法地出现“真值加脚本”的形状。问:旧节点会怎么对待这种交易?答:这是软分叉,旧节点看到的是普通脚本的收尾,只是规则收紧后新节点可能拒绝旧节点接受的某些交易形态。问:今天有什么钱包或合约在用?答:没有,它是纯文档层面的设计。

风险提示:本文内容为技术史与机制科普,不构成投资建议,也不构成对任何协议的采用性背书;在真实链上部署脚本前请区分已激活规则与未激活提案。