协议演进 / BIP/比特币改进提案 / BIP-349
BIP-349
提出 OP_INTERNALKEY,把当前 Taproot 输出的内部公钥压入栈中,供脚本在花费时引用这把未经 tweak 的公钥。本 BIP 目前处于草案状态。
一、档案卡
| 编号 | BIP-349 |
|---|---|
| 标题 | OP_INTERNALKEY |
| 作者 | Brandon Black <[email protected]> Jeremy Rubin <[email protected]> |
| 指定日期 | 2024-11-14 |
| 层级 | Consensus (soft fork)(共识(软分叉)) |
| 类型 | Specification(规范类) |
| 状态 | Draft(草案) |
| 阅读版 | https://bips.dev/349 |
| 官方原文 | https://github.com/bitcoin/bips/blob/master/bip-0349.md |
二、放在哪一族
Tapscript 小原语族,而且是同批提案里最小的一个:整份规范的核心只有一句话。它与以下文档配套:
BIP-341:Taproot。它定义了“内部公钥”(internal key)——构造 Taproot 输出时先定的那把公钥,之后与脚本树一起做 tweak 得到最终输出公钥。本 BIP 压栈的正是这把内部公钥。
BIP-342:Tapscript 验证规则。本 BIP 在其叶子版本 0xc0 语境下替换 OP_SUCCESS 占位操作码。
BIP-348:OP_CHECKSIGFROMSTACK,同作者同批。两者常组合:INTERNALKEY 把内部公钥取出来,CSFS 拿它对某段消息验签。
BIP-118:SIGHASH_ANYPREVOUT。原文 Credits 说明,INTERNALKEY 的想法源自 2022 年 Russell O’Connor 与 Jeremy Rubin 在 Bitcoin Wizards IRC 的讨论,灵感来自 BIP-118 对内部公钥的“双关”用法(key punning)。
读它不需要先懂整套契约理论;它解决的是一个非常具体的别扭:脚本在执行时,明明身处某个 Taproot 输出之内,却读不到这个输出的内部公钥,想引用它只能把 32 字节公钥原文再抄一份进脚本,白花体积。
三、它要解决什么
原文列了三个动机场景:
-
给密钥路径再加脚本条件:构造 Taproot 输出时,尤其是内部公钥是多方聚合公钥的场合,各方可能希望仍用内部公钥协作签名(走密钥路径),但要叠加额外的脚本限制。脚本里引用内部公钥时,用
OP_INTERNALKEY可省 8 vBytes——省的是重复写一遍 32 字节公钥的开销。 -
减轻哈希锁脚本的控制块负担:不打算用密钥路径花费时,内部公钥通常设为一个 NUMS 点(谁都没有对应私钥的点)。带哈希锁的脚本若本来就要在 Tapscript 里写下这个 NUMS 点的字节,改用
OP_INTERNALKEY引用它又可省 8 vBytes。原文附带提醒:内部公钥必须是 SECP256K1 曲线上某点的 X 坐标,所以充当它的哈希值要反复调整直到恰好是一个合法 X 坐标,平均约需试 2 次。 -
换钥时保住默克尔根(re-keying):考虑形如
CTV <X> CSFS <S+1> CLTV的脚本片段(原文称这类片段对 LN-Symmetry 应用有用),嵌在TR(X, {...})里。若内部公钥从 X 换成 Y,叶子里写死的<X>就与新内部公钥对不上了。把叶子改写成引用OP_INTERNALKEY,叶子会自动跟着新内部公钥走:TR(Y, {CTV OP_INTERNALKEY CSFS <S+1> CLTV})等价于把片段里的 X 换成 Y。原文说,这个一般性技巧是一种有用的契约原语:当希望让旧状态的签名失效时(例如假想的操作码 OP_TAPLEAFUPDATEVERIFY 直接改内部公钥来增减参与方),各脚本路径都能统一改用调整后的公钥。
概括起来:它省的字节不多(原文两处都写省 8 vBytes),真正的价值是让脚本里的“这把钥”与输出的内部公钥永远保持同一把,不必靠人工同步两份拷贝。
四、规则本身
4.1 规范全文的核心
在验证 Taproot 脚本路径花费、且叶子版本为 0xc0 时(定义见 BIP-342):
OP_INTERNALKEY替换 OP_SUCCESS203(0xcb);- 执行时,把 Taproot 内部公钥的 32 字节 x-only 表示(即 BIP-341 定义的内部公钥 p)压入栈中。
没有别的参数、条件与失败分支:它不弹栈、不验签、不比较,执行结果就是栈上多出这 32 字节。之后脚本拿它做什么——交给 OP_CHECKSIG 类操作码验签、交给 BIP-348 的 CSFS 验任意消息、或参与哈希比较——都是脚本自己的事。
4.2 几个要点
- 压入的是内部公钥,不是经过 tweak 的最终输出公钥,也不是控制块里携带的任何东西;
- 它是 x-only 表示:只有 32 字节 X 坐标,没有奇偶前缀;
- 参考实现以 Bitcoin Core 拉取请求的形式给出,原文在 Reference Implementation 一节链接了 PR #29269。
五、谁必须动
普通持币人:无直接变化。草案未激活,地址与钱包都不受影响。
钱包与协议开发者:是直接使用者。在脚本里引用内部公钥时可用它省体积、并获得“换钥自动跟随”的性质;但设计时必须想清楚:脚本一旦引用内部公钥,聚合公钥成员或托管安排变更(换内部公钥)时,脚本的实际约束会跟着变——这正是 re-keying 场景想要的,但对不希望变的脚本则是隐患。
全节点与矿工:若将来部署,这是一次共识软分叉:节点在 Tapscript 执行到 OP_SUCCESS203 时改为压入内部公钥。不升级的旧节点按 OP_SUCCESS 旧语义直接放行、不压任何东西,这正是软分叉的兼容方式;原文同样提醒,依赖 OP_SUCCESS203 旧行为的花费会被新规则改变结果。
共识层:改动仅限叶子版本 0xc0 的 Tapscript 语境,其它脚本语境不受影响。
六、现在算不算数,以及一个常见误读
状态为 Draft(草案),原文的部署一节写 TBD(待定)。它未定稿、未激活:今天主网上的 OP_SUCCESS203 只是占位操作码,节点见之直接通过,既不压栈也不报错。任何按 OP_INTERNALKEY 语义写的脚本现在都无法在主网按预期工作。
常见误读:
“OP_INTERNALKEY 会把 Taproot 的私钥或最终收款公钥暴露给脚本。”
不对。它压栈的是内部公钥的 x-only 表示——本来就是公开信息:走脚本路径花费时,控制块里就带着内部公钥,全网可见。它也不是链上最终出现的输出公钥(那把是内部公钥与脚本树共同 tweak 后的结果),更不涉及任何私钥。把这两把公钥混为一谈,会对 Taproot 的花费条件做出完全错误的判断。
七、延伸
- 阅读版:https://bips.dev/349
- 官方原文:https://github.com/bitcoin/bips/blob/master/bip-0349.md
- 相关:BIP-341 Taproot、BIP-342 Tapscript、BIP-348 CHECKSIGFROMSTACK、BIP-118 SIGHASH_ANYPREVOUT
- 仓库总目录:https://github.com/bitcoin/bips
