Building a Bitcoin P2TR Tx by Hand
Table of Contents
1. 背景介绍
本文介绍如何“纯手工地”构造一个 Bitcoin Pay-to-Taproot (P2TR) Tx。
1.1. 任务描述
在比特币测试网上,Tx 0195fce44d57453a0cdf8ef1e36ed022994f59278a9e915503e04e9a11f06892 的功能是花费了两个旧 UTXO(分别属于地址 tb1pevpm69vf0my4ge9m0ne9m8s056mma2uxpw9u9m76cug2vcsmu6ask9u37q 和地址 tb1plmq7t40y68ve96pppv6mqe78kxyuhaq2hl4anxym5jaywcz00xeqthwx6l),创建了新 UTXO(属于地址 tb1pwxpw8xkd2w9e0d38kvlrzqpnszhnaqwuvcpf0l2nlc3t5fhujgzs58jxsz)。
这个 Tx 对三个地址的余额变化为:
tb1pevpm69vf0my4ge9m0ne9m8s056mma2uxpw9u9m76cug2vcsmu6ask9u37q -0.00003000 tb1plmq7t40y68ve96pppv6mqe78kxyuhaq2hl4anxym5jaywcz00xeqthwx6l -0.00099800 tb1pwxpw8xkd2w9e0d38kvlrzqpnszhnaqwuvcpf0l2nlc3t5fhujgzs58jxsz +0.00102520
其中差值 0.00000280 就是手续费。
本文将介绍这个 Tx 是如何构造出来的。
1.2. 演示帐户
地址 tb1pevpm69vf0my4ge9m0ne9m8s056mma2uxpw9u9m76cug2vcsmu6ask9u37q 对应的私钥和公钥分别为:
taproot tweak seckey: d5d3a71a06e45f9c215f764612b209cc41d086651a0aa62f78551408cc64ec2b taproot tweak pubkey: cb03bd15897ec95464bb7cf25d9e0fa6b7beab860b8bc2efdac710a6621be6bb
地址 tb1plmq7t40y68ve96pppv6mqe78kxyuhaq2hl4anxym5jaywcz00xeqthwx6l 对应的私钥和公钥分别为:
taproot tweak seckey: 9ab7cdd3bfc955da43de564fcebb9b43fbb51d3e31f1b0cfc7875f06d22cd157 taproot tweak pubkey: fec1e5d5e4d1d992e8210b35b067c7b189cbf40abfebd9989ba4ba47604f79b2
地址 tb1pwxpw8xkd2w9e0d38kvlrzqpnszhnaqwuvcpf0l2nlc3t5fhujgzs58jxsz 对应的公钥为:
taproot tweak pubkey: 7182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc9205
2. Segwit Tx 序列化格式
[nVersion][txin_count][txins][txout_count][txouts][nLockTime] # 传统 Tx 的序列化格式
在 BIP144 中,为隔离见证引入了新的 Tx 序列化格式:
[nVersion][marker][flag][txin_count][txins][txout_count][txouts][witnesses][nLockTime] # Segwit Tx 的序列化格式
这两类 Tx 并没有通过 nVersion 来区分,而是通过 marker 来区分的, 当 marker 为 0x00
时就是 Segwit Tx。 因为对于传统的 Tx 来说,txin_count 是 tx input 数量,它不可能是 0x00
。
广播 Tx 0195fce44d57453a0cdf8ef1e36ed022994f59278a9e915503e04e9a11f06892 时,提交的 Tx 为:
0100000000010277db015abc9524b97c87fb40e9ee2ea01bfc350651cf447c82d133690048b7010100000000fdffffff2be860eb3c45b97018350c122c975ce2fbda451a3145e267ca124ac9fe92ec3e0000000000fdffffff0178900100000000002251207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc920501404a43ea0ee1ffef5ddd894cc271c6dce2265f1fe02f066d4f7fdb0c6e18eda325c9e0a6eef6ace0ef2380cc57c655f175cd0ad180a8c58e788756d682c8e21d5c01403bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15eba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d3800000000
按照 Segwit Tx 的序列化格式可以把上面数据分解为下面这种更清晰的表达方式:
+---------------------------------------------+----------------------------------------------------------------------+------------------------+ | Version | 01000000 | 4 bytes | +---------------------------------------------+----------------------------------------------------------------------+------------------------+ | Maker | 00 | 1 byte | +---------------------------------------------+----------------------------------------------------------------------+------------------------+ | Flag | 01 | 1 byte | +---------------------------------------------+----------------------------------------------------------------------+------------------------+ | Number of inputs | 02 | Varies (1/3/5/9 bytes) | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | | Previous output hash (reversed) | 77db015abc9524b97c87fb40e9ee2ea01bfc350651cf447c82d133690048b701 | 32 bytes | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Previous output index | 01000000 | 4 bytes | | Input 0 +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Script length | 00 | Varies (1/3/5/9 bytes) | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Sequence | fdffffff | 4 bytes | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | | Previous output hash (reversed) | 2be860eb3c45b97018350c122c975ce2fbda451a3145e267ca124ac9fe92ec3e | 32 bytes | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Previous output index | 00000000 | 4 bytes | | Input 1 +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Script length | 00 | Varies (1/3/5/9 bytes) | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Sequence | fdffffff | 4 bytes | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | Number of outputs | 01 | Varies (1/3/5/9 bytes) | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | | Value | 7890010000000000 | 8 bytes | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | Output 0 | Script length | 22 | Varies (1/3/5/9 bytes) | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | ScriptPubKey (Locking Script) | 51207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc9205 | | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | | Witness Count | 01 | Varies (1/3/5/9 bytes) | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Witness Length | 40 | Varies (1/3/5/9 bytes) | | Witness 0 +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Schnorr Sig | 4a43ea0ee1ffef5ddd894cc271c6dce2265f1fe02f066d4f7fdb0c6e18eda325 | | | | | c9e0a6eef6ace0ef2380cc57c655f175cd0ad180a8c58e788756d682c8e21d5c | | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | | Witness Count | 01 | Varies (1/3/5/9 bytes) | | +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Witness Length | 40 | Varies (1/3/5/9 bytes) | | Witness 1 +---------------------------------+----------------------------------------------------------------------+------------------------+ | | Schnorr Sig | 3bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15e | | | | | ba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d38 | | +-----------+---------------------------------+----------------------------------------------------------------------+------------------------+ | Locktime | 00000000 | 4 bytes | +---------------------------------------------+----------------------------------------------------------------------+------------------------+
注 1:占用字节长度不固定的整数,其类型是 CompactSize Unsigned Integer,可能的字节长度是 1/3/5/9。
注 2:数值是小端表示,比如新 UTXO 的金额 102520 聪用 8 字节的小端表示就是 0x7890010000000000。
注 3:每个 Input 对应一个 Witness,所有不需要 “Number of witness” 字段。
注 4:大部分字段比较好理解,重点介绍一下 Ouput 0 的 ScriptPubKey 以及 Witness 中两个 Schnorr 签名数据是如何产生的。Output 0 的 ScriptPubKey 为 0x51207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc9205,其中:
- 0x51(OP_1)表示版本 1 的隔离见证;
- 0x20 表示后面内容的长度;
- 0x7182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc9205 则是从 taproot 地址 tb1pwxpw8xkd2w9e0d38kvlrzqpnszhnaqwuvcpf0l2nlc3t5fhujgzs58jxsz 中解码出来的公钥。
2.1. 计算 Schnorr 签名
这个交易中有两个 input,需要进行两次 schnorr 签名。在 BIP341 中规定了如何计算待签名数据。
2.1.1. 第一次签名
首先构造第一次签名的 sig_to_hash 数据:
epoch: 00 hash_type: 00 version: 01000000 lock_time: 00000000 sha_prevouts: df4ab7bc35fec96c25cb97506fd7573ebc2954d8ee7fd62a147763b87a15470c sha_amounts: c86690d960173932b6a3be70f64b7b7e43b554b555f4cb809faf0e3cb621c98f sha_scriptpubkeys: 01331bcaa5e5af1bf32f9f27d5f4197b1fa08e61f6ca5ef2e5c8073fe8b65bdc sha_sequences: 82d397cbbcff87bc5d0c4c70e424f9b830efbad7bf0be479da5d1d1bafdb9798 sha_outputs: 5ad5d604b39841133033667abcdb9628ca335b4b8ff0bde0f920a0edb333c3c5 spend_type: 00 input_index: 00000000
把上面数据组在一起构成了 sig_to_hash,然后对它进行
sig_to_hash = bytes.fromhex("00000100000000000000df4ab7bc35fec96c25cb97506fd7573ebc2954d8ee7fd62a147763b87a15470cc86690d960173932b6a3be70f64b7b7e43b554b555f4cb809faf0e3cb621c98f01331bcaa5e5af1bf32f9f27d5f4197b1fa08e61f6ca5ef2e5c8073fe8b65bdc82d397cbbcff87bc5d0c4c70e424f9b830efbad7bf0be479da5d1d1bafdb97985ad5d604b39841133033667abcdb9628ca335b4b8ff0bde0f920a0edb333c3c50000000000") tag_hash = sha256("TapSighash".encode()) preimage_hash = sha256(tag_hash + tag_hash + sig_to_hash) print(preimage_hash.hex()) # 2e15afa4913b2896c04eced8b240999e468dc1f829f8de01a3d6eb21e53a3b3d
使用 tb1pevpm69vf0my4ge9m0ne9m8s056mma2uxpw9u9m76cug2vcsmu6ask9u37q 的私钥对 preimage_hash(即 2e15afa4913b2896c04eced8b240999e468dc1f829f8de01a3d6eb21e53a3b3d)进行 Schnorr 签名后,得到第一次的签名结果(注:Schnorr 并不是确定的,同个数据多次签名时,每次结果都不一样):
4a43ea0ee1ffef5ddd894cc271c6dce2265f1fe02f066d4f7fdb0c6e18eda325c9e0a6eef6ace0ef2380cc57c655f175cd0ad180a8c58e788756d682c8e21d5c
2.1.2. 第二次签名
构造第二次签名的 sig_to_hash 数据和前面过程基本一样,只是计算 sig_to_hash 时,所使用的 input_index 需要修改为 01000000(因为它是第 2 个 input):
epoch: 00 hash_type: 00 version: 01000000 lock_time: 00000000 sha_prevouts: df4ab7bc35fec96c25cb97506fd7573ebc2954d8ee7fd62a147763b87a15470c sha_amounts: c86690d960173932b6a3be70f64b7b7e43b554b555f4cb809faf0e3cb621c98f sha_scriptpubkeys: 01331bcaa5e5af1bf32f9f27d5f4197b1fa08e61f6ca5ef2e5c8073fe8b65bdc sha_sequences: 82d397cbbcff87bc5d0c4c70e424f9b830efbad7bf0be479da5d1d1bafdb9798 sha_outputs: 5ad5d604b39841133033667abcdb9628ca335b4b8ff0bde0f920a0edb333c3c5 spend_type: 00 input_index: 01000000
把上面数据组在一起构成了 sig_to_hash,然后对它进行
sig_to_hash = bytes.fromhex("00000100000000000000df4ab7bc35fec96c25cb97506fd7573ebc2954d8ee7fd62a147763b87a15470cc86690d960173932b6a3be70f64b7b7e43b554b555f4cb809faf0e3cb621c98f01331bcaa5e5af1bf32f9f27d5f4197b1fa08e61f6ca5ef2e5c8073fe8b65bdc82d397cbbcff87bc5d0c4c70e424f9b830efbad7bf0be479da5d1d1bafdb97985ad5d604b39841133033667abcdb9628ca335b4b8ff0bde0f920a0edb333c3c50001000000") tag_hash = sha256("TapSighash".encode()) preimage_hash = sha256(tag_hash + tag_hash + sig_to_hash) print(preimage_hash.hex()) # 1ad32af10c85c270393742c33b56087632186431aaa012323abe13d0d180f2d7
使用 tb1plmq7t40y68ve96pppv6mqe78kxyuhaq2hl4anxym5jaywcz00xeqthwx6l 的私钥对 preimage_hash(即 1ad32af10c85c270393742c33b56087632186431aaa012323abe13d0d180f2d7)进行 Schnorr 签名后,得到第二次的签名结果(注:Schnorr 并不是确定的,同个数据多次签名时,每次结果都不一样):
3bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15eba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d38
3. 附录
3.1. 完整代码
完整的 Python 代码:
#!/usr/bin/env python # -*- coding: utf-8 -*- import hashlib import schnorr_lib def convertbits(data, frombits, tobits, pad=True): """General power-of-2 base conversion.""" # From https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py acc = 0 bits = 0 ret = [] maxv = (1 << tobits) - 1 max_acc = (1 << (frombits + tobits - 1)) - 1 for value in data: if value < 0 or (value >> frombits): return None acc = ((acc << frombits) | value) & max_acc bits += frombits while bits >= tobits: bits -= tobits ret.append((acc >> bits) & maxv) if pad: if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): return None return ret def bech32_decode(bech): """Validate a Bech32/Bech32m string, and determine HRP and data.""" # From https://github.com/sipa/bech32/blob/master/ref/python/segwit_addr.py CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech)): return (None, None) bech = bech.lower() pos = bech.rfind('1') if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: return (None, None) if not all(x in CHARSET for x in bech[pos + 1:]): return (None, None) hrp = bech[:pos] data = [CHARSET.find(x) for x in bech[pos + 1:]] return (hrp, data[:-6]) def p2tr_address_to_scriptpubkey(address: str) -> bytes: """Convert p2tr address into scriptpubkey (locking script)""" if not any(address.startswith(item) for item in ["tb1p", "bc1p", "bcrt1p"]): raise Exception("invalid p2tr address") hrp, data = bech32_decode(address) if hrp is None or data is None: raise Exception("bech32_decode failed") decoded = convertbits(data[1:], 5, 8, False) if decoded is None: raise Exception("convertbits failed") binary_str = "" for number in decoded: binary_str += bin(number)[2:].zfill(8) # Convert the integer to binary and pad with zeros to 8 digits hex_str = hex(int(binary_str, 2))[2:] # Convert binary to hexadecimal and remove the prefix '0x' return bytes.fromhex("5120" + hex_str) def sha256(data: bytes) -> bytes: """One round of SHA256""" return hashlib.sha256(data).digest() def reverse_byte_order(input: bytes) -> bytes: """Reverse byte order""" return input[::-1] def varint_len(data: bytes) -> bytes: """returns the length of the input as a variable integer""" l = len(data) if l < int('fd', 16): varint = l.to_bytes(1, byteorder="little", signed=False) elif l < int('ffff', 16): varint = bytes.fromhex("fd") + l.to_bytes(2, byteorder="little", signed=False) else: raise Exception("This function only handles up to 0xffff bytes") return varint # Private keys # Please make sure the private keys is related to utxos_to_spend private_keys = [ "d5d3a71a06e45f9c215f764612b209cc41d086651a0aa62f78551408cc64ec2b", # tb1pevpm69vf0my4ge9m0ne9m8s056mma2uxpw9u9m76cug2vcsmu6ask9u37q "9ab7cdd3bfc955da43de564fcebb9b43fbb51d3e31f1b0cfc7875f06d22cd157", # tb1plmq7t40y68ve96pppv6mqe78kxyuhaq2hl4anxym5jaywcz00xeqthwx6l ] utxos_to_spend = [ { # https://api.blockcypher.com/v1/btc/test3/txs/01b748006933d1827c44cf510635fc1ba02eeee940fb877cb92495bc5a01db77 "txid": "01b748006933d1827c44cf510635fc1ba02eeee940fb877cb92495bc5a01db77", "vout": 1, "value": 3000, "scriptpubkey": "5120cb03bd15897ec95464bb7cf25d9e0fa6b7beab860b8bc2efdac710a6621be6bb" # i.e. locking script }, { # https://api.blockcypher.com/v1/btc/test3/txs/3eec92fec94a12ca67e245311a45dafbe25c972c120c351870b9453ceb60e82b "txid": "3eec92fec94a12ca67e245311a45dafbe25c972c120c351870b9453ceb60e82b", "vout": 0, "value": 99800, "scriptpubkey": "5120fec1e5d5e4d1d992e8210b35b067c7b189cbf40abfebd9989ba4ba47604f79b2" # i.e. locking script } ] tx_outputs = [ { "value": 102520, "scriptpubkey_address": "tb1pwxpw8xkd2w9e0d38kvlrzqpnszhnaqwuvcpf0l2nlc3t5fhujgzs58jxsz" # receive address } ] # data for the tx I want to create version: bytes = (1).to_bytes(4, byteorder="little", signed=False) # version 1, 01000000 sequence: bytes = 0xfffffffd.to_bytes(4, byteorder="little", signed=False) # enables replace-by-fee and absolute lock-time but disables relative lock-time lock_time: bytes = bytes.fromhex("00000000") hash_type: bytes = bytes.fromhex("00") # SIGHASH_DEFAULT # ---------- # CONSTRUCTING SIGHASH x INPUT (taproot) # ---------- # sha_prevouts (32) = SHA256(serialization of all input outpoints) prevouts: bytes = b'' for utxo_to_spend in utxos_to_spend: prevouts += reverse_byte_order(bytes.fromhex(utxo_to_spend["txid"])) prevouts += utxo_to_spend["vout"].to_bytes(4, byteorder="little", signed=False) sha_prevouts = sha256(prevouts) # sha_amounts (32): the SHA256 of the serialization of all spent output amounts amounts: bytes = b'' for utxo_to_spend in utxos_to_spend: amounts += utxo_to_spend["value"].to_bytes(8, byteorder="little", signed=False) sha_amounts = sha256(amounts) # sha_scriptpubkeys (32): the SHA256 of all spent outputs' scriptPubKeys, serialized as script inside CTxOut scriptpubkeys: bytes = b'' for utxo_to_spend in utxos_to_spend: if not (utxo_to_spend["scriptpubkey"].startswith("5120") and len(utxo_to_spend["scriptpubkey"]) == 68): raise Exception("invalid scriptpubkey, only p2tr is supported") scriptpubkey: bytes = bytes.fromhex(utxo_to_spend["scriptpubkey"]) scriptpubkeys += varint_len(scriptpubkey) scriptpubkeys += scriptpubkey sha_scriptpubkeys: bytes = sha256(scriptpubkeys) # sha_sequences (32): the SHA256 of the serialization of all input nSequence. sequences: bytes = b'' for utxo_to_spend in utxos_to_spend: sequences += sequence sha_sequences: bytes = sha256(sequences) # sha_outputs (32): the SHA256 of the serialization of all outputs in CTxOut format. outputs: bytes = b'' for tx_output in tx_outputs: scriptpubkey: bytes = p2tr_address_to_scriptpubkey(tx_output["scriptpubkey_address"]) outputs += tx_output["value"].to_bytes(8, byteorder="little", signed=False) outputs += varint_len(scriptpubkey) outputs += scriptpubkey sha_outputs: bytes = sha256(outputs) # spend_type (1): equal to (ext_flag * 2) + annex_present, where annex_present is 0 if no annex is present, # or 1 otherwise (the original witness stack has two or more witness elements, # and the first byte of the last element is 0x50) spend_type = bytes.fromhex("00") preimage_hashes = [] for i, _ in enumerate(utxos_to_spend): # input_index (4): index of this input in the transaction input vector. Index of the first input is 0 input_index: bytes = i.to_bytes(4, byteorder="little", signed=False) # https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki sig_to_hash = b'\x00' + hash_type + version + lock_time \ + sha_prevouts + sha_amounts + sha_scriptpubkeys + sha_sequences \ + sha_outputs + spend_type + input_index print("sig_to_hash =", sig_to_hash.hex()) tag_hash = sha256("TapSighash".encode()) preimage_hash = sha256(tag_hash + tag_hash + sig_to_hash) print("preimage_hash =", preimage_hash.hex()) preimage_hashes.append(preimage_hash) # ---------- # SIGNING # ---------- assert (len(preimage_hashes) == len(private_keys)) signatures = [] for i, private_key in enumerate(private_keys): sig = schnorr_lib.schnorr_sign(preimage_hashes[i], private_key) signatures.append(sig) # ---------- # CONSTRUCTING WITNESS # ---------- witnesses: bytes = b'' for witness_sig in signatures: witness_count = (1).to_bytes(1, byteorder="little", signed=False) # Witness Count 1 means P2TR (Key Path) witness_sig_len: bytes = varint_len(witness_sig) witnesses += witness_count witnesses += witness_sig_len witnesses += witness_sig # ---------- # TX READY # ---------- marker = bytes.fromhex("00") flag = bytes.fromhex("01") txin_count: bytes = len(utxos_to_spend).to_bytes(1, byteorder="little", signed=False) txout_count: bytes = len(tx_outputs).to_bytes(1, byteorder="little", signed=False) txins = b'' for utxo_to_spend in utxos_to_spend: txins += reverse_byte_order(bytes.fromhex(utxo_to_spend["txid"])) txins += utxo_to_spend["vout"].to_bytes(4, byteorder="little", signed=False) txins += bytes.fromhex("00") txins += sequence # witness serialization format for tx: see https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki tx = version \ + marker + flag \ + txin_count + txins \ + txout_count + outputs \ + witnesses \ + lock_time # Since the signature data is different every time, tx hex may be different every time raw_tx = tx.hex() # this tx can be broadcast print("raw_tx =", raw_tx) # 0100000000010277db015abc9524b97c87fb40e9ee2ea01bfc350651cf447c82d133690048b7010100000000fdffffff2be860eb3c45b97018350c122c975ce2fbda451a3145e267ca124ac9fe92ec3e0000000000fdffffff0178900100000000002251207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc920501404a43ea0ee1ffef5ddd894cc271c6dce2265f1fe02f066d4f7fdb0c6e18eda325c9e0a6eef6ace0ef2380cc57c655f175cd0ad180a8c58e788756d682c8e21d5c01403bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15eba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d3800000000 # result tx_id: # https://mempool.space/testnet/tx/0195fce44d57453a0cdf8ef1e36ed022994f59278a9e915503e04e9a11f06892
其中依赖的 schnorr_lib.py 如下:
from typing import Tuple, Optional from binascii import unhexlify import hashlib import os # From: https://github.com/BitPolito/schnorr-sig/blob/master/schnorr_lib.py # Elliptic curve parameters p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) # Points are tuples of X and Y coordinates # the point at infinity is represented by the None keyword Point = Tuple[int, int] # Get bytes from an int def bytes_from_int(a: int) -> bytes: return a.to_bytes(32, byteorder="big") # Get bytes from a point def bytes_from_point(P: Point) -> bytes: return bytes_from_int(x(P)) # Get an int from bytes def int_from_bytes(b: bytes) -> int: return int.from_bytes(b, byteorder="big") # Get an int from hex def int_from_hex(a: hex) -> int: return int.from_bytes(unhexlify(a), byteorder="big") # Get x coordinate from a point def x(P: Point) -> int: return P[0] # Get y coordinate from a point def y(P: Point) -> int: return P[1] # Point addition def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: if P1 is None: return P2 if P2 is None: return P1 if (x(P1) == x(P2)) and (y(P1) != y(P2)): return None if P1 == P2: lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p else: lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p x3 = (lam * lam - x(P1) - x(P2)) % p return x3, (lam * (x(P1) - x3) - y(P1)) % p # Point multiplication def point_mul(P: Optional[Point], d: int) -> Optional[Point]: R = None for i in range(256): if (d >> i) & 1: R = point_add(R, P) P = point_add(P, P) return R # Note: # This implementation can be sped up by storing the midstate # after hashing tag_hash instead of rehashing it all the time # Get the hash digest of (tag_hashed || tag_hashed || message) def tagged_hash(tag: str, msg: bytes) -> bytes: tag_hash = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(tag_hash + tag_hash + msg).digest() # Check if a point is at infinity def is_infinity(P: Optional[Point]) -> bool: return P is None # Get xor of bytes def xor_bytes(b0: bytes, b1: bytes) -> bytes: return bytes(a ^ b for (a, b) in zip(b0, b1)) # Get a point from bytes def lift_x_square_y(b: bytes) -> Optional[Point]: x = int_from_bytes(b) if x >= p: return None y_sq = (pow(x, 3, p) + 7) % p y = pow(y_sq, (p + 1) // 4, p) if pow(y, 2, p) != y_sq: return None return x, y def lift_x_even_y(b: bytes) -> Optional[Point]: P = lift_x_square_y(b) if P is None: return None else: return x(P), y(P) if y(P) % 2 == 0 else p - y(P) # Check if an int is square def is_square(a: int) -> bool: return int(pow(a, (p - 1) // 2, p)) == 1 # Check if a point has even y coordinate def has_even_y(P: Point) -> bool: return y(P) % 2 == 0 # Generate auxiliary random of 32 bytes def get_aux_rand() -> bytes: return os.urandom(32) # Extract R_x int value from signature def get_int_R_from_sig(sig: bytes) -> int: return int_from_bytes(sig[0:32]) # Extract s int value from signature def get_int_s_from_sig(sig: bytes) -> int: return int_from_bytes(sig[32:64]) # Generate Schnorr signature def schnorr_sign(msg: bytes, privateKey: str) -> bytes: if len(msg) != 32: raise ValueError('The message must be a 32-byte array.') d0 = int_from_hex(privateKey) if not (1 <= d0 <= n - 1): raise ValueError( 'The secret key must be an integer in the range 1..n-1.') P = point_mul(G, d0) assert P is not None d = d0 if has_even_y(P) else n - d0 t = xor_bytes(bytes_from_int(d), tagged_hash("BIP0340/aux", get_aux_rand())) k0 = int_from_bytes(tagged_hash("BIP0340/nonce", t + bytes_from_point(P) + msg)) % n if k0 == 0: raise RuntimeError('Failure. This happens only with negligible probability.') R = point_mul(G, k0) assert R is not None k = n - k0 if not has_even_y(R) else k0 e = int_from_bytes(tagged_hash("BIP0340/challenge", bytes_from_point(R) + bytes_from_point(P) + msg)) % n sig = bytes_from_point(R) + bytes_from_int((k + e * d) % n) if not schnorr_verify(msg, bytes_from_point(P), sig): raise RuntimeError('The created signature does not pass verification.') return sig # Verify Schnorr signature def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: if len(msg) != 32: raise ValueError('The message must be a 32-byte array.') if len(pubkey) != 32: raise ValueError('The public key must be a 32-byte array.') if len(sig) != 64: raise ValueError('The signature must be a 64-byte array.') P = lift_x_even_y(pubkey) r = get_int_R_from_sig(sig) s = get_int_s_from_sig(sig) if (P is None) or (r >= p) or (s >= n): return False e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n R = point_add(point_mul(G, s), point_mul(P, n - e)) if (R is None) or (not has_even_y(R)): # print("Please, recompute the sign. R is None or has even y") return False if x(R) != r: # print("There's something wrong") return False return True
3.2. Tx Hash 计算
Bitcoin 中 Tx Hash(或者称 Tx Id)是一个 Tx 的标识。它的计算规则对于传统 Tx 和 Segwit Tx 是不一样的,具体规则如下:
- 对于传统 Tx(即非 Segwit Tx)来说,Tx Hash 是 Tx 序列化数据的两次 SHA256 哈希;
- 对于 Segwit Tx 来说,Tx Hash 是 Tx 序列化数据去除 marker/flag/witness 这三部分内容后,再计算两次 SHA256 哈希。
节 2 中介绍了 Segwit Tx 的序列化格式,即:
[nVersion][marker][flag][txin_count][txins][txout_count][txouts][witnesses][nLockTime] # Segwit Tx 的序列化格式
所以,Segwit Tx 的 Tx Hash 的计算公式为:
SHA256(SHA256([nVersion][txin_count][txins][txout_count][txouts][nLockTime]))
下面以节 2 中介绍的 Segwit Tx 0195fce44d57453a0cdf8ef1e36ed022994f59278a9e915503e04e9a11f06892 为例,介绍一下这个 Segwit Tx 的 Tx Hash 是如何计算的:
The input tx: 0100000000010277db015abc9524b97c87fb40e9ee2ea01bfc350651cf447c82d133690048b7010100000000fdffffff2be860eb3c45b97018350c122c975ce2fbda451a3145e267ca124ac9fe92ec3e0000000000fdffffff0178900100000000002251207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc920501404a43ea0ee1ffef5ddd894cc271c6dce2265f1fe02f066d4f7fdb0c6e18eda325c9e0a6eef6ace0ef2380cc57c655f175cd0ad180a8c58e788756d682c8e21d5c01403bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15eba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d3800000000 Step 1: Remove the marker/flag/witness, get: 010000000277db015abc9524b97c87fb40e9ee2ea01bfc350651cf447c82d133690048b7010100000000fdffffff2be860eb3c45b97018350c122c975ce2fbda451a3145e267ca124ac9fe92ec3e0000000000fdffffff0178900100000000002251207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc920500000000 Step 2: SHA256(SHA256(Step 1 Output)), get: 9268f0119a4ee00355919e8a27594f9922d06ee3f18edf0c3a45574de4fc9501 Step 3: Tx hash is displayed in reverse order, get: 0195fce44d57453a0cdf8ef1e36ed022994f59278a9e915503e04e9a11f06892