本文详细介绍了比特币Taproot升级,包括P2TR输出的格式、Witness结构、Key Path和Script Path的使用,以及Taproot地址的推导过程。Taproot通过更好的隐私性和更低的交易费用优化了比特币交易,并统一了P2WPKH和P2WSH两种形式。
Created: <2021-11-12 Fri>
Last updated: <2024-07-21 Sun>
Taproot 是 Bitcoin 的一次大升级,在区块 709632 处(2021 年 11 月 12 日)被激活。
Taproot Output 是版本为 1 的隔离见证 Output,这类 Output 称为 Pay-to-Taproot (P2TR)。
我们知道,版本为 0 的隔离见证 Output 有两类(在 BIP141 中定义):
Pay-to-Witness-Public-Key-Hash (P2WPKH)
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 字段的总结。
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> |
Table 1: P2WPKH/P2WSH/P2TR 的 scriptPubKey 字段及花费它们时的 Witness 字段
下面分析一下 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 所示。
P2WPKH | P2TR (Key Path) | |
---|---|---|
创建 Output | 手续费少一点点(因为 scriptPubKey 小一点点) | 手续费多 |
花费 Output | 手续费多 | 手续费少更多(因为 Witness 更小) |
Table 2: P2WPKH 和 P2TR (Key Path) 手续费对比:综合比较的话,P2TR (Key Path) 手续费少
对于隔离见证 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。
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 元素的个数。
对于 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
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
如果在花费 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 字节的例子。
如果在花费 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 public key P
774e148e9209baf3f1656a46986d5f38ddf4e20912c6ac28f48d6bf747469fb1 # hash e
Witness 倒数第二个元素是 Script:
20febe583fa77e49089f89b78fa8c116710715d6e40cc5f5a075ef1681550dd3c4ad20d0fa46cb883e940ac3dc5421f05b03859972639f51ed2eccbf3dc5a62e2e1b15ac
## 对应 Script:
febe583fa77e49089f89b78fa8c116710715d6e40cc5f5a075ef1681550dd3c4 OP_CHECKSIGVERIFY d0fa46cb883e940ac3dc5421f05b03859972639f51ed2eccbf3dc5a62e2e1b15 OP_CHECKSIG
Witness 倒数第二个元素之前的所有元素都是“Script 的参数”,在这个 2-of-2 多签的例子中,它是两个 Schnorr 签名。
如何校验这个 Witness 是合法的呢?具体规则在 bip341 中,简单地总结有两点:
检查 Script 确实在 MAST 上;
脚本 Script 执行完成后,检查栈上留下 true。
这个 2-of-2 多签例子中,脚本 Script = <P1> OP_CHECKSIGVERIFY <P2> OP_CHECKSIG,输入参数是 [sig(P2), sig(P1)],只要签名是正确的,则执行完成后,栈上留下的就是 true。
节 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)的源码。
Figure 1: Merkelized Alternative Script Tree (MAST)
完整的 Control Block 如下:
<control byte with leaf version and parity bit> <internal public key P> <C> <E> <AB>
节 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 地址。
Figure 2: Tweaked Public Key Q=P+tG 。这仅是一个简化公式,严格来说它是错误的。正确的公式为 Q=lift_x(P)+tG ,其中 lift_x(P) 的功能是返回椭圆曲线上一个点,它的 Y 坐标一定是偶数,而 X 坐标和 P 的 X 坐标相同
有 4 种方式可以花费图 2 所指定的 Output:
使用 x+t 作为私钥(即 Tweaked Private Key,也可能是 (secp256k1 order−x)+t ,参考节 3.1)进行 Schnorr-Bip340 签名;
公布 script A 和哈希 B 和哈希 C;
公布 script B 和哈希 A 和哈希 C;
公布 script C 和哈希 AB。
图 2 的例子中只给出了 3 个脚本,当然你可以根据需求删除脚本或者添加更多的脚本。
具体关于如何花费 P2TR Output,可以参考: https://github.com/bitcoin-core/btcdeb/blob/master/doc/tapscript-example.md
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)
从上面代码中,我们可以知道:
在 Tweak 公钥时,对 Internal Public Key 进行了 lift_x 操作。
在 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 坐标为偶数的那个密钥对。
如果我们不需要 Script Path,则可以去掉 Script 相关的哈希,即去掉图 3 中黑线圈起来的部分。
Figure 3: 只使用 Key Path(这是个人钱包最常见的场景)
也就是说,计算没有 Script Path 的 Tweaked Public Key 的方法为: Q=P+tG=P+TaggedHash('TapTweak', P)G 对 Tweaked Public Key Q 的 X 坐标进行 bech32m 编码就得到了个人钱包的 taproot 地址。
如果我们不需要 Key Path,则可以把 Internal Public Key 设置为一个没人知道对应私钥的公钥,如图 4 所示。
Figure 4: 只使用 Script Path(把 Internal Public Key 设置为一个没人知道对应私钥的公钥即可)
BIP 341 中推荐的 NUMS Point(一个不知道对应私钥的公钥)为:
x = 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
y = 31d3c6863973926e049e637cb1b5f40a36dac28af1766968c30c2313f3a38904
它的构造方法是计算 secp256k1 曲线的 Base Point G 的 sha256 哈希,即:
50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 = sha256(0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8)
节 3 中提到了,花费图 2 所指定的 Output 有 4 种方式:
使用 x+t 作为私钥(即 Tweaked Private Key,也可能是 (secp256k1 order−x)+t ,参考节 3.1)进行 Schnorr-Bip340 签名;
公布 script A 和哈希 B 和哈希 C;
公布 script B 和哈希 A 和哈希 C;
公布 script C 和哈希 AB。
显然,第 4 种方式要比第 2/3 种方式更加省手续费。因为除脚本本身外,第 4 种方式只需要再额外提供一个哈希值,而第 2/3 种方式需要额外提供两个哈希值。
这给我们一个启发:如果我们知道花费 Output 时最可能使用哪个脚本,则把这个脚本放到离 Merkle Root 比较近的位置,这样可以省手续费。这就是 Huffman Encoding 的思路。
下面是构造图 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
Taproot 主要有下面优点:
隐私性更好,通过地址无法直接区分它是 Key Path,还是 Script Path;使用 Script Path 花费时,不用公开所有 Script,只需要公开一个 Script,参考节 2.2.5。
更省交易费,参考节 2.1.1。
Taproot Output 生成和花费 的完整例子可参考: https://github.com/Eunovo/taproot-with-bitcoinjs
更多 Taproot 例子可参考: https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/test/integration/taproot.spec.ts
- 本文转载自: aandds.com/blog/bitcoin-... , 如有侵权请联系管理员删除。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!