树状脚本装进一行表达式:BIP-386 的 tr() 描述符怎么描述 Taproot 输出 图 1
树状脚本装进一行表达式:BIP-386 的 tr() 描述符怎么描述 Taproot 输出 · 图 1

描述符语言的职责,是把钱包的一整套输出脚本写成一行可读的文本。比特币钱包描述符是什么? 讲过这套语言的基本结构,一把钥匙四种地址:combo() 描述符为什么是扫描老钱包的默认答案 讲过 combo() 如何一行覆盖四种历史格式。但 Taproot 引入的 P2TR 输出比其他脚本多一个部件:一棵可选的脚本树。树怎么装进描述符?2021 年 6 月立项的 BIP-386 给出了 tr() 表达式,状态 Deployed,比特币核心自 22.0 版起实现。

tr() 单参数形态:没有脚本路径的 P2TR

tr(KEY) 只接一把密钥,产出不带脚本路径的 P2TR 输出脚本。按 BIP-341 的要求,如果花费条件不需要脚本路径,输出密钥应当承诺一个不可花费的脚本路径,计算式是 Q = P + int(hash_TapTweak(bytes(P)))G——先把内部密钥提升为曲线点,再叠加一次以该密钥自身为输入的哈希调整值。落到脚本层面,就是一条例外的见证程序:版本 1 加 32 字节输出密钥。

tr() 对密钥有一条比其他描述符更严的规矩:内部所有键表达式都必须产出 x-only 公钥。压缩公钥会被隐式转换,未压缩公钥直接判为非法,同时它额外允许一种只在 tr() 内部有效的写法——一串 64 位十六进制字符直接表示 x-only 公钥。

脚本树怎么用花括号写出来

tr(KEY, TREE) 的第二个参数是一棵脚本树。树表达式的写法是自嵌套的:单独一个脚本表达式算一棵树,两个树表达式用花括号包起来、中间放一个逗号,也构成一棵树,逐层递归到叶子。叶子能用的脚本表达式有限——提案写作时只有 pk() 可用,后来补充的包括 Miniscript 的 pkh() 片段和 multi_a()、sortedmulti_a()(多签换了个写法:multi_a 与 sortedmulti_a 描述符里的逐把验签 专门讲过这一对)。

带树的计算式与单密钥只差一步:内部密钥不变,先对整棵树算出 HashTapBranch 层的默克尔根,再把这个根拼接进 TapTweak 哈希。输出密钥因此同时承诺了内部密钥和脚本树:链上只能看到一个 32 字节密钥,看不到树里有几片叶子。

两种非法嵌套与一条结构课

提案的测试向量里明确列了两例非法:sh(tr(...)) 与 wsh(tr(...))。tr() 只能站在描述符的最顶层,不能再被包进 P2SH 或 P2WSH。原因值得展开:P2TR 本身就是见证版本 1 的顶层输出,它的密钥承诺机制(TapTweak)假定自己面对的是最终脚本;再套一层哈希封装,脚本路径花费时给不出正确的承诺。结构课是:描述符语言不是任意积木,每一层能接什么由规范逐条画线。

恢复与核对时看什么

对使用者的落点有两个。第一,从备份恢复 Taproot 钱包时,描述符字符串里除了密钥还要带上树的完整形状——叶子脚本的排列顺序参与默克尔根计算,顺序记错,地址就全变。第二,tr() 表达式带 BIP-380 校验和,导入钱包或节点前先用校验工具核对一遍,抄错一个字符都能当场发现。

树改动即地址改动:一个容易踩的口径

描述符钱包的身份证就是这一行字符串,而 tr() 让”改动”的颗粒度变细了:不换任何密钥,只在花括号里调换两片叶子的左右位置、增删一个分支、或给某片叶子换一条脚本,默克尔根都会变化,派生出的输出密钥随之全部作废重算。这与单密钥描述符”密钥不动地址就不动”的直觉相反,是 Taproot 描述符特有的雷区。协作场景里尤其要核对:协调器、硬件钱包、节点三方各自拼出的 tr() 字符串必须逐字节一致,任何一方手工”整理”过花括号层级,都会得到一个双方都扫不到对方账单的钱包。稳妥做法是把描述符原文(含校验和)作为协作文件的固定部分传输,导入前逐字比对而不是靠记忆重敲。

最后划一条边界:无论是单密钥的 tr(KEY) 还是带复杂树的 tr(KEY, TREE),产出都是同一种 P2TR 输出,链上观察不到树的规模差异。树越复杂,链上形状反而越省——这也是 Taproot 常被引用的一条隐私特性,本文只描述其结构含义,不引申任何使用建议。

风险提示:本文是钱包机制科普,不构成投资建议;描述符与脚本树参数一旦记错可能导致资金难以找回,重要金额请先用小额走通完整的备份、导入与花费流程。