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,然后对它进行 \(\text{hash}_{\text{TapSighash}}\) 计算后,得到第一个待签名数据 preimage_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 签名后,得到第一次的签名结果(注:节 3.1 实现的 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,然后对它进行 \(\text{hash}_{\text{TapSighash}}\) 计算后,得到第二个待签名数据 preimage_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 签名后,得到第二次的签名结果(注:节 3.1 实现的 Schnorr 签名并不是确定的,同个数据多次签名时,每次结果都不一样):
3bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15eba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d38
3. 附录
3.1. 完整代码
#!/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