Taproot

Table of Contents

1. Taproot 简介

Taproot 是 Bitcoin 的一次大升级,在区块 709632 处(2021 年 11 月 12 日)被激活。

2. Pay-to-Taproot (P2TR)

Taproot Output 是版本为 1 的隔离见证 Output,这类 Output 称为 Pay-to-Taproot (P2TR)。

2.1. 和版本 0 隔离见证 Output 的对比

我们知道,版本为 0 的隔离见证 Output 有两类(在 BIP141 中定义):

  1. Pay-to-Witness-Public-Key-Hash (P2WPKH)
  2. Pay-to-Witness-Script-Hash (P2WSH)

这两类版本为 0 的隔离见证 Output 其 scriptPubKey 字段是不同的(不同点参见表 1),所以我们很容易区分某个 Output 到底是 P2WPKH 还是 P2WSH。

但是在版本为 1 的隔离见证 Output(即 Taproot Output)中,统一了这两种形式。也就是说,P2TR 的 Output 的 scriptPubKey 字段是一样的,我们无法从 Output 的格式来得知这个 Output 是由 Schnorr 签名锁定(即 Key Path)还是由脚本锁定(即 Script Path),这样有更好的隐私。

1 是 P2WPKH/P2WSH/P2TR 的 scriptPubKey 字段及花费它们时的 Witness 字段的总结。

Table 1: P2WPKH/P2WSH/P2TR 的 scriptPubKey 字段及花费它们时的 Witness 字段
Type scriptPubKey(锁定时使用) Witness(花费时使用)
P2WPKH 0x0014{20-byte-key-hash} <signature> <pubkey>
P2WSH 0x0020{32-byte-hash} ......
P2TR (Key Path) 0x5120{32-byte-tweaked-public-key} <schnorr-signature>
P2TR (Script Path) 0x5120{32-byte-tweaked-public-key} ...... <script> <control-blok>

2.1.1. 费用比对

下面分析一下 P2TR (Key Path) 和 P2WPKH 的交易大小的变化(越小就越省费用)。

从表 1 中,可以看到创建一个 P2TR (Key Path) Output 时,要比创建 P2WPKH Output 占用更多的空间,因为 P2TR (Key Path) 的 scriptPubKey 直接含有 tweaked public key(32 字节),而 P2WPKH 则是公钥哈希(20 字节)。也就是说, 往 P2TR 转账比往 P2WPKH 地址转账要贵一点点。

不过, 花费 P2TR (Key Path) 比花费 P2WPKH 要省更多的费用, 原因是:花费 P2TR (Key Path) 的 Witness 中不再包含公钥了;而且 P2TR (Key Path) 采用的 Schnorr 签名比 P2WPKH 采用的 DER 格式的 ECDSA 签名要更小。这导致花费 P2TR (Key Path) 需要提供的 Witness 小很多。

综合考虑“创建 Output”和“花费 Output”两个方面,P2TR (Key Path) 比 P2WPKH 更省费用,如表 2 所示。

Table 2: P2WPKH 和 P2TR (Key Path) 手续费对比:综合比较的话,P2TR (Key Path) 手续费少
  P2WPKH P2TR (Key Path)
创建 Output 手续费少一点点(因为 scriptPubKey 小一点点) 手续费多
花费 Output 手续费多 手续费少更多(因为 Witness 更小)

2.1.2. scriptPubKey

对于隔离见证 Output,其 scriptPubKey 的首个字节为 OP_n,表示隔离见证版本,OP_n 可以是:

OP_0: 0x00
OP_1: 0x51
OP_2: 0x52
...

See: https://github.com/bitcoin/bitcoin/blob/v22.0/src/script/script.h#L68

版本 0 隔离见证 Output 的 scriptPubKey 的首个字节是 0x00,而版本 1 隔离见证 Output 的 scriptPubKey 的首个字节是 0x51。

2.2. Witness

Witness 的格式是什么?直接看 BIP141 对其的定义:

The witness is a serialization of all witness data of the transaction. Each txin is associated with a witness field. A witness field starts with a var_int to indicate the number of stack items for the txin. It is followed by stack items, with each item starts with a var_int to indicate the length. Witness data is NOT script.

Witness 的首个字节是 var_int 类型的数,它表示 Witness 元素的个数。

2.2.1. P2WPKH 的 Witness

对于 P2WPKH 来说,Witness 由“signature”和“pubkey”两个元素组成。

Tx 9d86b83297aaf232446e5ab41b603027fb37ad85f5259b78d6ff6fefe7cada9d 是花费 P2WPKH 的例子,它的 Witness 为:

02483045022100f1622d4147f1856cadc312df066e5a19b382b20a45d6a16efbf912b94eac5ebb022036cfe4f3a1e2342c185aa0168dbe0e388c3199d6d0d74ded414bc3787e41669f012102cdcfe7b2facd6a068fa0fb0ac5aa02fee40995ef6a7bd06a54d6c758988ad8fa

下面解释下它每个字节的含义:

0070: 02 .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. vin0 Witness Count: 2
0070: .. 48 .. .. .. .. .. .. .. .. .. .. .. .. .. .. vin0 Witness 0 Length: 0x48(72)
0070: .. .. 30 .. .. .. .. .. .. .. .. .. .. .. .. .. DER start
0070: .. .. .. 45 .. .. .. .. .. .. .. .. .. .. .. .. DER length: 69
0070: .. .. .. .. 02 .. .. .. .. .. .. .. .. .. .. .. DER int
0070: .. .. .. .. .. 21 .. .. .. .. .. .. .. .. .. .. DER R length: 33
0070: .. .. .. .. .. .. 00 f1 62 2d 41 47 f1 85 6c ad
0080: c3 12 df 06 6e 5a 19 b3 82 b2 0a 45 d6 a1 6e fb
0090: f9 12 b9 4e ac 5e bb .. .. .. .. .. .. .. .. .. DER R: 00f1622d4147f1856cadc312df066e5a19b382b20a45d6a16efbf912b94eac5ebb
0090: .. .. .. .. .. .. .. 02 .. .. .. .. .. .. .. .. DER int
0090: .. .. .. .. .. .. .. .. 20 .. .. .. .. .. .. .. DER S length: 32
0090: .. .. .. .. .. .. .. .. .. 36 cf e4 f3 a1 e2 34
00a0: 2c 18 5a a0 16 8d be 0e 38 8c 31 99 d6 d0 d7 4d
00b0: ed 41 4b c3 78 7e 41 66 9f .. .. .. .. .. .. .. DER S: 36cfe4f3a1e2342c185aa0168dbe0e388c3199d6d0d74ded414bc3787e41669f
00b0: .. .. .. .. .. .. .. .. .. 01 .. .. .. .. .. .. DER sighash type byte: 1
00b0: .. .. .. .. .. .. .. .. .. .. 21 .. .. .. .. .. vin0 Witness 1 Length: 0x21(33)
00b0: .. .. .. .. .. .. .. .. .. .. .. 02 cd cf e7 b2 sender public key
00c0: fa cd 6a 06 8f a0 fb 0a c5 aa 02 fe e4 09 95 ef
00d0: 6a 7b d0 6a 54 d6 c7 58 98 8a d8 fa

2.2.2. P2WSH 的 Witness

Tx 90d6525d315dae372550f0d0bdee0081f5eff165173e4fe705003e21b0c3333d 是花费 P2WSH 的例子(它是一个 2-of-3 多签),它的 Witness 为:

0400483045022100e671f305e2bfb42531e7a03f220e3f9be53ec58de318154866ae40fe73c3b1ca0220604ed76ff68afca4926b74f00841c1da9d4a5a993615d792e8e8f6e03c76c80401473044022004aaa8c83cf0a3ae69145bcf5ed596f191a2fa8affd78091220b187bf404e64e022060957cb42da6df3fbde0d7780d8e9ee67644b1760433424d6362cc86a8bc04e50169522103b1d7b531a4a9ca4a701cae7b1260bbe47b0b063ca40de3a3015f073d64ce614f21020166885dc4c7f6989e377eaff4bc87e0b4491efb61e28a4175f4c967a0d0917421023d60b07d04326b1277cf84a4a17f6683de8f798e0efb570be76223bbb63faef053ae

下面解释下它每个字节的含义:

0140: .. .. 04 .. .. .. .. .. .. .. .. .. .. .. .. .. vin0 Witness Count: 4
0140: .. .. .. 00 .. .. .. .. .. .. .. .. .. .. .. .. vin0 Witness 0 Length:0
0140: .. .. .. .. 48 .. .. .. .. .. .. .. .. .. .. .. vin0 Witness 1 Length:72
0140: .. .. .. .. .. 30 .. .. .. .. .. .. .. .. .. .. DER start
0140: .. .. .. .. .. .. 45 .. .. .. .. .. .. .. .. .. DER length: 69
0140: .. .. .. .. .. .. .. 02 .. .. .. .. .. .. .. .. DER int
0140: .. .. .. .. .. .. .. .. 21 .. .. .. .. .. .. .. DER R length: 33
0140: .. .. .. .. .. .. .. .. .. 00 e6 71 f3 05 e2 bf DER R: 00e671f305e2bfb42531e7a03f220e3f9be53ec58de318154866ae40fe73c3b1ca
0150: b4 25 31 e7 a0 3f 22 0e 3f 9b e5 3e c5 8d e3 18
0160: 15 48 66 ae 40 fe 73 c3 b1 ca .. .. .. .. .. ..
0160: .. .. .. .. .. .. .. .. .. .. 02 .. .. .. .. .. DER int
0160: .. .. .. .. .. .. .. .. .. .. .. 20 .. .. .. .. DER S length: 32
0160: .. .. .. .. .. .. .. .. .. .. .. .. 60 4e d7 6f DER S: 604ed76ff68afca4926b74f00841c1da9d4a5a993615d792e8e8f6e03c76c804
0170: f6 8a fc a4 92 6b 74 f0 08 41 c1 da 9d 4a 5a 99
0180: 36 15 d7 92 e8 e8 f6 e0 3c 76 c8 04 .. .. .. ..
0180: .. .. .. .. .. .. .. .. .. .. .. .. 01 .. .. .. DER sighash type byte: 1
0180: .. .. .. .. .. .. .. .. .. .. .. .. .. 47 .. .. vin0 Witness 2 Length:71
0180: .. .. .. .. .. .. .. .. .. .. .. .. .. .. 30 .. DER start
0180: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 44 DER length: 68
0190: 02 .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. DER int
0190: .. 20 .. .. .. .. .. .. .. .. .. .. .. .. .. .. DER R length: 32
0190: .. .. 04 aa a8 c8 3c f0 a3 ae 69 14 5b cf 5e d5 DER R: 04aaa8c83cf0a3ae69145bcf5ed596f191a2fa8affd78091220b187bf404e64e
01a0: 96 f1 91 a2 fa 8a ff d7 80 91 22 0b 18 7b f4 04
01b0: e6 4e .. .. .. .. .. .. .. .. .. .. .. .. .. ..
01b0: .. .. 02 .. .. .. .. .. .. .. .. .. .. .. .. .. DER int
01b0: .. .. .. 20 .. .. .. .. .. .. .. .. .. .. .. .. DER S length: 32
01b0: .. .. .. .. 60 95 7c b4 2d a6 df 3f bd e0 d7 78 DER S: 60957cb42da6df3fbde0d7780d8e9ee67644b1760433424d6362cc86a8bc04e5
01c0: 0d 8e 9e e6 76 44 b1 76 04 33 42 4d 63 62 cc 86
01d0: a8 bc 04 e5 .. .. .. .. .. .. .. .. .. .. .. ..
01d0: .. .. .. .. 01 .. .. .. .. .. .. .. .. .. .. .. DER sighash type byte: 1
01d0: .. .. .. .. .. 69 .. .. .. .. .. .. .. .. .. .. vin0 Witness 3 script: 105
01d0: .. .. .. .. .. .. 52 .. .. .. .. .. .. .. .. .. ***OP_2
01d0: .. .. .. .. .. .. .. 21 03 b1 d7 b5 31 a4 a9 ca ***PUSHDATA 33 bytes: 03b1d7b531a4a9ca4a701cae7b1260bbe47b0b063ca40de3a3015f073d64ce614f
01e0: 4a 70 1c ae 7b 12 60 bb e4 7b 0b 06 3c a4 0d e3
01f0: a3 01 5f 07 3d 64 ce 61 4f .. .. .. .. .. .. ..
01f0: .. .. .. .. .. .. .. .. .. 21 02 01 66 88 5d c4 ***PUSHDATA 33 bytes: 020166885dc4c7f6989e377eaff4bc87e0b4491efb61e28a4175f4c967a0d09174
0200: c7 f6 98 9e 37 7e af f4 bc 87 e0 b4 49 1e fb 61
0210: e2 8a 41 75 f4 c9 67 a0 d0 91 74 .. .. .. .. ..
0210: .. .. .. .. .. .. .. .. .. .. .. 21 02 3d 60 b0 ***PUSHDATA 33 bytes: 023d60b07d04326b1277cf84a4a17f6683de8f798e0efb570be76223bbb63faef0
0220: 7d 04 32 6b 12 77 cf 84 a4 a1 7f 66 83 de 8f 79
0230: 8e 0e fb 57 0b e7 62 23 bb b6 3f ae f0 .. .. ..
0230: .. .. .. .. .. .. .. .. .. .. .. .. .. 53 .. .. ***OP_3
0230: .. .. .. .. .. .. .. .. .. .. .. .. .. .. ae .. ***OP_CHECKMULTISIG

2.2.3. P2TR (Key Path) 的 Witness

如果在花费 P2TR UTXO 时,Witness 只包含一个元素,则是 P2TR (Key Path)。

Tx dbef583962e13e365a2069d451937a6de3c2a86149dc6a4ac0d84ab450509c91 是花费 P2TR (Key Path) 的例子,它的 witness 为:

0141e6e1fe41524e65e3040bc3d080a136345c2c806eb7f336dd6a7a79e9054b0d1fc6a8d836667ef6e9f2188cd1270ab28e5e0eb642eac89f2ec50a32ca54aaf9d601

下面解释下它每个字节的含义:

0050: .. 01 .. .. .. .. .. .. .. .. .. .. .. .. .. .. vin0 Witness Count: 1
0050: .. .. 41 .. .. .. .. .. .. .. .. .. .. .. .. .. vin0 Witness 0 Length:65, schnorr_sig (64 bytes) + sig_hash (1 bytes)
0050: .. .. .. e6 e1 fe 41 52 4e 65 e3 04 0b c3 d0 80 schnorr_sig
0060: a1 36 34 5c 2c 80 6e b7 f3 36 dd 6a 7a 79 e9 05
0070: 4b 0d 1f c6 a8 d8 36 66 7e f6 e9 f2 18 8c d1 27
0080: 0a b2 8e 5e 0e b6 42 ea c8 9f 2e c5 0a 32 ca 54
0090: aa f9 d6
0090: .. .. .. 01                                     sig_hash: SIGHASH_ALL (0x01)

注:上面例子中 signature 占 65 字节。当 sig_hash 为 SIGHASH_DEFAULT(0x00)时,sig_hash 可以省略,这时 signature 只占 64 字节,比如 Tx 37777defed8717c581b4c0509329550e344bdc14ac38f71fc050096887e535c8 首个 Input 就是 signature 只占 64 字节的例子。

2.2.4. P2TR (Script Path) 的 Witness

如果在花费 P2TR UTXO 时,Witness 至少包含两个元素,则是 P2TR (Script Path)。

也就是说, 在花费一个 P2TR UTXO 时,是通过 Witness 中元素的个数来决定使用 Key Path(Witness 元素个数为 1)还是 Script Path(Witness 元素个数大于等于 2)。

Tx 905ecdf95a84804b192f4dc221cfed4d77959b81ed66013a7e41a6e61e7ed530 是花费 P2TR (Script Path) 的例子(它是一个 2-of-2 多签脚本),它的 Witness 为:

044123b1d4ff27b16af4b0fcb9672df671701a1a7f5a6bb7352b051f461edbc614aa6068b3e5313a174f90f3d95dc4e06f69bebd9cf5a3098fde034b01e69e8e788901400fd4a0d3f36a1f1074cb15838a48f572dc18d412d0f0f0fc1eeda9fa4820c942abb77e4d1a3c2b99ccf4ad29d9189e6e04a017fe611748464449f681bc38cf394420febe583fa77e49089f89b78fa8c116710715d6e40cc5f5a075ef1681550dd3c4ad20d0fa46cb883e940ac3dc5421f05b03859972639f51ed2eccbf3dc5a62e2e1b15ac41c02e44c9e47eaeb4bb313adecd11012dfad435cd72ce71f525329f24d75c5b9432774e148e9209baf3f1656a46986d5f38ddf4e20912c6ac28f48d6bf747469fb1

下面解释下它每个字节的含义:

0070: .. .. .. .. .. .. 04 .. .. .. .. .. .. .. .. .. vin0 Witness Count: 4
0070: .. .. .. .. .. .. .. 41 23 b1 d4 ff 27 b1 6a f4 vin0 Witness 0 Length:65 (0x41)
0080: b0 fc b9 67 2d f6 71 70 1a 1a 7f 5a 6b b7 35 2b
0090: 05 1f 46 1e db c6 14 aa 60 68 b3 e5 31 3a 17 4f
00a0: 90 f3 d9 5d c4 e0 6f 69 be bd 9c f5 a3 09 8f de
00b0: 03 4b 01 e6 9e 8e 78 89 01 .. .. .. .. .. .. ..
00b0: .. .. .. .. .. .. .. .. .. 40 0f d4 a0 d3 f3 6a vin0 Witness 1 Length:64 (0x40)
00c0: 1f 10 74 cb 15 83 8a 48 f5 72 dc 18 d4 12 d0 f0
00d0: f0 fc 1e ed a9 fa 48 20 c9 42 ab b7 7e 4d 1a 3c
00e0: 2b 99 cc f4 ad 29 d9 18 9e 6e 04 a0 17 fe 61 17
00f0: 48 46 44 49 f6 81 bc 38 cf 39 .. .. .. .. .. ..
00f0: .. .. .. .. .. .. .. .. .. .. 44 20 fe be 58 3f vin0 Witness 2 Length:68 (0x44)
0100: a7 7e 49 08 9f 89 b7 8f a8 c1 16 71 07 15 d6 e4
0110: 0c c5 f5 a0 75 ef 16 81 55 0d d3 c4 ad 20 d0 fa
0120: 46 cb 88 3e 94 0a c3 dc 54 21 f0 5b 03 85 99 72
0130: 63 9f 51 ed 2e cc bf 3d c5 a6 2e 2e 1b 15 ac ..
0130: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 41 vin0 Witness 3 Length:65 (0x41)
0140: c0 2e 44 c9 e4 7e ae b4 bb 31 3a de cd 11 01 2d
0150: fa d4 35 cd 72 ce 71 f5 25 32 9f 24 d7 5c 5b 94
0160: 32 77 4e 14 8e 92 09 ba f3 f1 65 6a 46 98 6d 5f
0170: 38 dd f4 e2 09 12 c6 ac 28 f4 8d 6b f7 47 46 9f
0180: b1

这个 Witness 中一共有 4 个元素。

Witness 最后一个元素是 control block:

c02e44c9e47eaeb4bb313adecd11012dfad435cd72ce71f525329f24d75c5b9432774e148e9209baf3f1656a46986d5f38ddf4e20912c6ac28f48d6bf747469fb1

可以分解为:

c0                                                               # leaf version and parity bit
2e44c9e47eaeb4bb313adecd11012dfad435cd72ce71f525329f24d75c5b9432 # internal key P
774e148e9209baf3f1656a46986d5f38ddf4e20912c6ac28f48d6bf747469fb1 # hash e

Witness 倒数第二个元素是 Script:

20febe583fa77e49089f89b78fa8c116710715d6e40cc5f5a075ef1681550dd3c4ad20d0fa46cb883e940ac3dc5421f05b03859972639f51ed2eccbf3dc5a62e2e1b15ac

# 对应 Script:
febe583fa77e49089f89b78fa8c116710715d6e40cc5f5a075ef1681550dd3c4 OP_CHECKSIGVERIFY d0fa46cb883e940ac3dc5421f05b03859972639f51ed2eccbf3dc5a62e2e1b15 OP_CHECKSIG

Witness 倒数第二个元素之前的所有元素都是“Script 的参数”,在这个 2-of-2 多签的例子中,它是两个 Schnorr 签名。

如何校验这个 Witness 是合法的呢?具体规则在 bip341 中,简单地总结有两点:

  1. 检查 Script 确实在 MAST 上;
  2. 脚本 Script 执行完成后,检查栈上留下 true。

这个 2-of-2 多签例子中,脚本 Script = <P1> OP_CHECKSIGVERIFY <P2> OP_CHECKSIG,输入参数是 [sig(P2), sig(P1)],只要签名是正确的,则执行完成后,栈上留下的就是 true。

2.2.5. Control Block 实例

2.2.4 中介绍了,花费 P2TR 时,如果 Witness 元素大于等于 2 个,则是 Script Path。这时,最后一个 Witness 是 Control Block。

下面再举例说明一下 Control Block。对于图 1(摘自 https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki )所示的 MAST,其中一共有 5 个叶子节点(即 5 个脚本),如果花费这个 P2TR 时,想使用脚本 D,除公开脚本 D 源码外,只用提供 C/E/AB 三个哈希值就可以证明脚本 D 确实在 MAST 上了。我们并不需要公开其它脚本(即 A/B/C/E)的源码。

taproot_control_block.gif

Figure 1: Merkelized Alternative Script Tree (MAST)

完整的 Control Block 如下:

<control byte with leaf version and parity bit> <internal key P> <C> <E> <AB>

3. 创建 P2TR Output(Taproot 地址推导)

2.1 中提到了创建 P2TR Output 时,需要指定 scriptPubKey 为 0x5120{32-byte-tweaked-public-key},其中的 32-byte-tweaked-public-key 就是图 2 中的 Tweaked Public Key Q 的 X 坐标。我们对这个 Tweaked Public Key Q 的 X 坐标进行 Bech32m 编码就可以得到 Taproot 地址。

taproot_tweak.gif

Figure 2: Tweaked Public Key \(Q=P+tG\) 。这仅是一个简化公式,严格来说它是错误的。正确的公式为 \(Q=\text{lift_x}(P) + tG\) ,其中 \(\text{lift_x}(P)\) 的功能是返回椭圆曲线上一个点,它的 \(Y\) 坐标一定是偶数,而 \(X\) 坐标和 \(P\) 的 \(X\) 坐标相同

有 4 种方式可以花费图 2 所指定的 Output:

  1. 使用 \(x + t\) 作为私钥(即 Tweaked Private Key,也可能是 \((\text{secp256k1 order}-x)+t\) ,参考节 3.1)进行 Schnorr-Bip340 签名;
  2. 公布 script A 和哈希 B 和哈希 C;
  3. 公布 script B 和哈希 A 和哈希 C;
  4. 公布 script C 和哈希 AB。

2 的例子中只给出了 3 个脚本,当然你可以根据需求删除脚本或者添加更多的脚本。

具体关于如何花费 P2TR Output,可以参考:https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example.md

3.1. Tweak 公钥时为什么要 lift_x

BIP341 中有 Tweak 公钥和 Tweak 私钥的 Python 示例代码:

## 代码来源于 https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki

def taproot_tweak_pubkey(pubkey, h):  # 输入为 Internal Public Key,输出为 Tweaked Public Key
    t = int_from_bytes(tagged_hash("TapTweak", pubkey + h))
    if t >= SECP256K1_ORDER:
        raise ValueError
    P = lift_x(int_from_bytes(pubkey))  # lift_x(pubkey) 的功能是返回椭圆曲线上一个点,它的 Y 坐标一定是偶数,而 X 坐标和 pubkey 的 X 坐标相同
    if P is None:
        raise ValueError
    Q = point_add(P, point_mul(G, t))
    return 0 if has_even_y(Q) else 1, bytes_from_int(x(Q))

def taproot_tweak_seckey(seckey0, h):  # 输入为 Internal Private Key,输出为 Tweaked Private Key
    seckey0 = int_from_bytes(seckey0)
    P = point_mul(G, seckey0)
    seckey = seckey0 if has_even_y(P) else SECP256K1_ORDER - seckey0
    t = int_from_bytes(tagged_hash("TapTweak", bytes_from_int(x(P)) + h))
    if t >= SECP256K1_ORDER:
        raise ValueError
    return bytes_from_int((seckey + t) % SECP256K1_ORDER)

从上面代码中,我们可以知道:

  1. 在 Tweak 公钥时,对 Internal Public Key 进行了 lift_x 操作。
  2. 在 Tweak 私钥时,如果 Internal Private Key 对应的公钥的 \(Y\) 坐标为偶数,则对 Internal Private Key 进行了取逆操作。

为什么要进行上面的处理呢?这是因为在 BIP340 中提到 Schnorr 公钥会采用 XOnly 形式。也就是说上面函数 taproot_tweak_pubkey 接收的参数 pubkey 是 32 字节的 XOnly 公钥。 对于 Secp256k1 来说,只知道 \(X\) 坐标是无法唯一确定公钥的。 具体来说,当只知道 \(X\) 坐标时,存在两个密钥对,其公钥的 \(X\) 坐标是一样的,并且这两个公钥的 \(Y\) 坐标一定是一奇一偶的(这里不证明):

Private Key:        Public Key:
seckey              (x1, y1)                   # y1 为奇数时 y2 一定为偶数;y1 为偶数时 y2 一定为奇数
n-seckey            (x2=x1, y2=p-y1)

所以,只使用 XOnly 公钥时有歧义。我们需要消除歧义,采取的策略就是:总是选择公钥 \(Y\) 坐标为偶数的那个密钥对。

3.2. 只使用 Key Path 时

如果我们不需要 Script Path,则可以去掉 Script 相关的哈希,即去掉图 3 中黑线圈起来的部分。

taproot_key_path.gif

Figure 3: 只使用 Key Path(这是个人钱包最常见的场景)

也就是说,计算没有 Script Path 的 Tweaked Public Key 的方法为: \[Q= P+tG = P+ \text{TaggedHash('TapTweak', P)} G\] 对 Tweaked Public Key Q 的 X 坐标进行 bech32m 编码就得到了个人钱包的 taproot 地址。

3.3. 只使用 Script Path 时

如果我们不需要 Key Path,则可以把 Internal Public Key 设置为一个没人知道对应私钥的公钥,如图 4 所示。

taproot_script_path.gif

Figure 4: 只使用 Script Path(把 Internal Public Key 设置为一个没人知道对应私钥的公钥即可)

BIP 341 中推荐的 NUMS Point(一个不知道对应私钥的公钥)为:

x = 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
y = 31d3c6863973926e049e637cb1b5f40a36dac28af1766968c30c2313f3a38904

它的构造方法是计算 secp256k1 曲线的 Base Point G 的 sha256 哈希,即:

50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 = sha256(0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8)

3.4. Huffman Encoding

3 中提到了,花费图 2 所指定的 Output 有 4 种方式:

  1. 使用 \(x + t\) 作为私钥(即 Tweaked Private Key,也可能是 \((\text{secp256k1 order}-x)+t\) ,参考节 3.1)进行 Schnorr-Bip340 签名;
  2. 公布 script A 和哈希 B 和哈希 C;
  3. 公布 script B 和哈希 A 和哈希 C;
  4. 公布 script C 和哈希 AB。

显然,第 4 种方式要比第 2/3 种方式更加省手续费。因为除脚本本身外,第 4 种方式只需要再额外提供一个哈希值,而第 2/3 种方式需要额外提供两个哈希值。

这给我们一个启发:如果我们知道花费 Output 时最可能使用哪个脚本,则把这个脚本放到离 Merkle Root 比较近的位置,这样可以省手续费。这就是 Huffman Encoding 的思路。

3.5. Taproot 地址计算实例

下面是构造图 2 所示 Taproot Output(即计算 Taproot 地址)的实例:

#!/usr/bin/env python3

# test_framework functions can be got from: https://github.com/bitcoinops/taproot-workshop
from test_framework.address import program_to_witness
from test_framework.key import ECPubKey, generate_bip340_key_pair
from test_framework.script import CScript, OP_CHECKSIG

import struct
import hashlib


def ser_compact_size(l):
    r = b""
    if l < 253:
        r = struct.pack("B", l)
    elif l < 0x10000:
        r = struct.pack("<BH", 253, l)
    elif l < 0x100000000:
        r = struct.pack("<BI", 254, l)
    else:
        r = struct.pack("<BQ", 255, l)
    return r


def ser_string(s):
    return ser_compact_size(len(s)) + s


def sha256(s):
    return hashlib.new('sha256', s).digest()


def tagged_hash(tag, data):
    ss = sha256(tag.encode('utf-8'))
    ss += ss
    ss += data
    return sha256(ss)


# Method: Returns tapbranch hash. Child hashes are lexographically sorted and then concatenated.
# l: tagged hash of left child
# r: tagged hash of right child
def tapbranch_hash(l, r):
    return tagged_hash("TapBranch", b''.join(sorted([l,r])))


TAPSCRIPT_VER = bytes([0xc0])  # See tapscript chapter for more details.
internalPrivkey, internalPubkey = generate_bip340_key_pair()
print('Internal Private Key (x):', internalPrivkey.get_bytes().hex())
print('Internal Public Key (P):', internalPubkey.get_bytes(bip340=True).hex())

# Derive pay-to-pubkey scripts
privkeyA, pubkeyA = generate_bip340_key_pair()
print('Private Key A:', privkeyA.get_bytes().hex())
privkeyB, pubkeyB = generate_bip340_key_pair()
print('Private Key B:', privkeyB.get_bytes().hex())
privkeyC, pubkeyC = generate_bip340_key_pair()
print('Private Key C:', privkeyC.get_bytes().hex())

scriptA = CScript([pubkeyA.get_bytes(bip340=True), OP_CHECKSIG])  # 可根据需要修改为其它脚本
print('Script A:', scriptA.hex())
scriptB = CScript([pubkeyB.get_bytes(bip340=True), OP_CHECKSIG])  # 可根据需要修改为其它脚本
print('Script B:', scriptB.hex())
scriptC = CScript([pubkeyC.get_bytes(bip340=True), OP_CHECKSIG])  # 可根据需要修改为其它脚本
print('Script C:', scriptC.hex())

# 1) Compute TapLeaves A, B and C
# Method: ser_string(data) is a function which adds compactsize to input data.
hash_inputA = TAPSCRIPT_VER + ser_string(scriptA)
hash_inputB = TAPSCRIPT_VER + ser_string(scriptB)
hash_inputC = TAPSCRIPT_VER + ser_string(scriptC)
taggedhash_leafA = tagged_hash("TapLeaf", hash_inputA)
taggedhash_leafB = tagged_hash("TapLeaf", hash_inputB)
taggedhash_leafC = tagged_hash("TapLeaf", hash_inputC)

# 2) Compute Internal node TapBranch AB
# Method: use tapbranch_hash() function
internal_nodeAB = tapbranch_hash(taggedhash_leafA, taggedhash_leafB)

# 3) Compute TapTweak
rootABC = tapbranch_hash(internal_nodeAB, taggedhash_leafC)
taptweak = tagged_hash("TapTweak", internalPubkey.get_bytes(bip340=True) + rootABC)
print("Tweak Value (t):", taptweak.hex())

# 4) Derive the segwit output address
taproot_pubkey_b = internalPubkey.tweak_add(taptweak).get_bytes(bip340=True)
print("Tweaked Publib Key (Q):", taproot_pubkey_b.hex())
segwit_address = program_to_witness(1, taproot_pubkey_b, main=True)  # For mainnet
print('Segwit address:', segwit_address)

上面脚本执行的输出实例(注:由于私钥随机生成,每次运行的输出都会不一样):

Internal Private Key (x): 197f84101343f9e799aaa73b6b2d4732e6ac711855f618729c20b55e1b1c1cf0
Internal Public Key (P): 86aa78a95914cd36e45431693f1c1077b722a49a5074f4b22098d9bbefcf90c6
Private Key A: 6f33ada384b7e27ede03bf46d2d8fdd5757bb4dac2f7fa5813c9f6bb31814b2e
Private Key B: a1d2069d9e4710bc3a52d8d4b7f9c434c0e4731d5e2a35f25c8b9c1d537ed6a0
Private Key C: 958e7e3cd3b6de611b6aba36d9cb6f32866b9ccecfaedf4a47c1ba518548ef23
Script A: 20769a05e97434e37e53f93b607906bd4c903004d38aca8e5c4398f393675963aeac
Script B: 206d3d912e429b3322766a38e1b4edf5bd1c4fb425f50afa97eb99f7d603db8150ac
Script C: 205174b0e4b603c49cbbe2d8294f34a5aa2bbd33c4c223c04e087d8c4823ee585eac
Tweak Value (t): 866c46294e5812c6139e50e19bb3b19a0be6f31f897c651f2af1d78b6254f49b
Tweaked Publib Key (Q): ce19f48f0bee9ac04c5cbfb3894886014fa5694c85781e4826cb1ea11ba80d1d
Segwit address: bc1pecvlfrcta6dvqnzuh7ecjjyxq986262vs4upujpxev02zxagp5wsqfyv9c

4. Taproot 优点

Taproot 主要有下面优点:

  1. 隐私性更好,通过地址无法直接区分它是 Key Path,还是 Script Path;使用 Script Path 花费时,不用公开所有 Script,只需要公开一个 Script,参考节 2.2.5
  2. 更省交易费,参考节 2.1.1

5. 参考

Author: cig01

Created: <2021-11-12 Fri>

Last updated: <2024-07-21 Sun>

Creator: Emacs 27.1 (Org mode 9.4)