协议演进 / BIP/比特币改进提案 / BIP-387

BIP-387 Tapscript 多签输出脚本描述符

为 Tapscript 定义 multi_a() 与 sortedmulti_a():用 CHECKSIGADD 逐个验签计数的门限多签,只能用在 tr() 的脚本树里。本 BIP 目前处于已部署状态。

一、档案卡

档案卡
编号BIP-387
标题Tapscript Multisig Output Script Descriptors
作者Pieter Wuille <[email protected]>, Ava Chow <[email protected]>
指定日期2024-04-17
层级Applications(应用)
类型Specification(规范类)
状态Deployed(已部署)
依赖BIP-380
阅读版https://bips.dev/387
官方原文https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki

二、放在哪一族

输出脚本描述符族中专管 Tapscript 多签的一篇,是 BIP-383 在 Taproot 世界的对应物。

BIP-380:描述符通用语法与操作,本篇依赖它

BIP-383:multi() 与 sortedmulti()——老脚本与 SegWit(wsh())语境下的门限多签

BIP-386:tr() 描述符——本篇的两个表达式只能活在它的脚本树里

BIP-342:Tapscript 验证规则——本篇脚本模板用到的 OP_CHECKSIGADD 正是 Tapscript 新增的操作码

一句话定位:多签的“门限”语义没变,变的是脚本的生成方式——从 OP_CHECKMULTISIG 换成 Tapscript 的逐个验签累加。

三、它要解决什么

原文动机很短:比特币里最常用的复杂脚本就是门限多签(threshold multisig),需要有表达式把多签脚本写成描述符。

为什么不能直接沿用 BIP-383 的 multi()?因为 Tapscript 的验签机制不同。老脚本靠 OP_CHECKMULTISIG 一次核对多个签名,还带着历史遗留的多弹一个栈元素等包袱;Tapscript 改用 OP_CHECKSIG 配合 OP_CHECKSIGADD:逐个密钥验签、把成功次数累加起来、最后与门限比较。脚本模板既然不同,描述符表达式就必须新设一套,并明确限定只能生成 Tapscript、只能出现在 Tapscript 语境中——这正是原文摘要强调的与 BIP-383 的主要区别。

四、规则本身

本篇定义两个新脚本表达式:multi_a() 与 sortedmulti_a(),参数与产出模板相同。

4.1 基本写法与门限

写法为 multi_a(k,KEY_1,KEY_2,...,KEY_n):k 是门限,即脚本有效所需的签名个数;KEY_1 到 KEY_n 是各参与方的密钥表达式;k 必须小于或等于 n。密钥个数上限为 999。

两个表达式都只能用在 BIP-386 的 tr() 描述符内部(即作为 Taproot 脚本树的叶子)。放在顶层、放进 sh() 或 wsh(),都是无效描述符。

4.2 产出的脚本模板

产出脚本取决于 k 的大小。k 不大于 16 时:

KEY_1 OP_CHECKSIG KEY_2 OP_CHECKSIGADD ... KEY_n OP_CHECKSIGADD OP_k OP_NUMEQUAL

k 大于 16 时,模板相同,只是末尾不用 OP_k 这一小整数操作码,而是把数值 k 本身压入栈中,再接 OP_NUMEQUAL:

KEY_1 OP_CHECKSIG KEY_2 OP_CHECKSIGADD ... KEY_n OP_CHECKSIGADD k OP_NUMEQUAL

含义是:第一个密钥用 OP_CHECKSIG 验签,其余每个密钥用 OP_CHECKSIGADD 在已有计数上累加,最后判断成功签名总数是否恰好等于 k。

4.3 sortedmulti_a():先排序再建脚本

sortedmulti_a() 唯一的不同,是在创建输出脚本之前,把各 x-only 公钥按字典序排序。原文特别说明:排序针对的是最终放进脚本的密钥,也就是所有扩展密钥都完成派生之后再排,与 BIP-383 里 sortedmulti() 的思路一致——描述符里密钥的书写顺序不再影响产出脚本。

4.4 多个扩展密钥同步派生

当一个表达式里有多个密钥表达式是扩展密钥时,它们派生子密钥时使用相同的子索引(child index),步调一致地变化。这样,产出脚本就能像单密钥描述符一样按索引编号,收款与找零的地址序列可以成套生成。

4.5 无效情形与实现

原文列出的无效情形包括:门限不是数字、门限为 0、门限大于密钥个数、使用未压缩公钥,以及把表达式放进不支持的语境。原文注明:这两个表达式已由 Bitcoin Core 的 PR #24043 实现,自 24.0 版起可用。产出的是标准脚本,不兼容的只是描述符层的新写法。

五、谁必须动

普通持币人:无直接动作。用 Taproot 多签钱包时,描述符中脚本树叶子上的 multi_a(2,...) 一类写法即出自本篇,认得它是“几把钥匙中至少几把签名”的脚本路径分支即可。

钱包开发者:支持 Taproot 多签必须实现这两个表达式:按 4.2 的模板拼脚本(注意 k 是否大于 16 的分界)、派生后排序(sortedmulti_a())、多扩展密钥同索引派生,并严格拒绝在 tr() 之外使用。

硬件签名设备与协作方:解析 Taproot 描述符时要能识别脚本树中的多签叶子,才能向用户正确展示花费条件;本篇本身不规定多方如何协调签名。

全节点与矿工、共识层:无须因本 BIP 改动。Tapscript 的执行规则由 BIP-342 定,本篇只定描述符写法。

六、现在算不算数,以及一个常见误读

状态为 Deployed,原文称已在 Bitcoin Core 24.0 实现。它让 Taproot 多签钱包之间有了统一的描述符文本:同一份 tr(...,multi_a(...)) 描述符在不同软件里应展开出同一组 P2TR 输出,测试向量即为对齐依据。

常见误读:

“multi_a() 就是把 BIP-383 的 multi() 搬进 Taproot,脚本还是 OP_CHECKMULTISIG 那一套。”

不对。两者只是参数形状相似(门限加一列密钥)。multi_a() 产出的脚本没有 OP_CHECKMULTISIG,而是 OP_CHECKSIG 加一串 OP_CHECKSIGADD 累加计数、末尾 OP_NUMEQUAL 比较门限;而且它只能生成 Tapscript、只能出现在 tr() 的脚本树里,放进 wsh() 或顶层都无效。把老多签描述符原样塞进 Taproot 语境,是用错了文档。

七、延伸

本页内容整理自 bitcoin/bips 仓库与 bips.dev,事实已与仓库核对;解读仅供学习参考。