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 序列化格式

在隔离见证之前,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                                                             |
+---------------------------------------------+----------------------------------------------------------------------+
| Maker                                       | 00                                                                   |
+---------------------------------------------+----------------------------------------------------------------------+
| Flag                                        | 01                                                                   |
+---------------------------------------------+----------------------------------------------------------------------+
| Number of inputs                            | 02                                                                   |
+-----------+---------------------------------+----------------------------------------------------------------------+
|           | Previous output hash (reversed) | 77db015abc9524b97c87fb40e9ee2ea01bfc350651cf447c82d133690048b701     |
|           +---------------------------------+----------------------------------------------------------------------+
|           | Previous output index           | 01000000                                                             |
| Input 0   +---------------------------------+----------------------------------------------------------------------+
|           | Script length                   | 00                                                                   |
|           +---------------------------------+----------------------------------------------------------------------+
|           | Sequence                        | fdffffff                                                             |
+-----------+---------------------------------+----------------------------------------------------------------------+
|           | Previous output hash (reversed) | 2be860eb3c45b97018350c122c975ce2fbda451a3145e267ca124ac9fe92ec3e     |
|           +---------------------------------+----------------------------------------------------------------------+
|           | Previous output index           | 00000000                                                             |
| Input 1   +---------------------------------+----------------------------------------------------------------------+
|           | Script length                   | 00                                                                   |
|           +---------------------------------+----------------------------------------------------------------------+
|           | Sequence                        | fdffffff                                                             |
+-----------+---------------------------------+----------------------------------------------------------------------+
| Number of outputs                           | 01                                                                   |
+-----------+---------------------------------+----------------------------------------------------------------------+
|           | Value                           | 7890010000000000                                                     |
|           +---------------------------------+----------------------------------------------------------------------+
| Output 0  | Script length                   | 22                                                                   |
|           +---------------------------------+----------------------------------------------------------------------+
|           | ScriptPubKey (Locking Script)   | 51207182e39acd538b97b627b33e31003380af3e81dc660297fd53fe22ba26fc9205 |
+-----------+---------------------------------+----------------------------------------------------------------------+
|           | Witness Count                   | 01                                                                   |
|           +---------------------------------+----------------------------------------------------------------------+
|           | Witness Length                  | 40                                                                   |
| Witness 0 +---------------------------------+----------------------------------------------------------------------+
|           | Schnorr Sig                     | 4a43ea0ee1ffef5ddd894cc271c6dce2265f1fe02f066d4f7fdb0c6e18eda325     |
|           |                                 | c9e0a6eef6ace0ef2380cc57c655f175cd0ad180a8c58e788756d682c8e21d5c     |
+-----------+---------------------------------+----------------------------------------------------------------------+
|           | Witness Count                   | 01                                                                   |
|           +---------------------------------+----------------------------------------------------------------------+
|           | Witness Length                  | 40                                                                   |
| Witness 1 +---------------------------------+----------------------------------------------------------------------+
|           | Schnorr Sig                     | 3bfe16498e4712951bb1659c21d7be53e63c28df59d19f76065e412f67adf15e     |
|           |                                 | ba76fce9b595342f4949572135c8502f8586cf7e6d2c9ddb9c9bdfc43ead9d38     |
+-----------+---------------------------------+----------------------------------------------------------------------+
| Locktime                                    | 00000000                                                             |
+---------------------------------------------+----------------------------------------------------------------------+

数值是小端表示,比如新 UTXO 的金额 102520 聪用 8 字节的小端表示就是 0x7890010000000000。

大部分字段比较好理解,重点介绍一下 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 签名后,得到第一次的签名结果(注: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 签名后,得到第二次的签名结果(注: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

4. 参考

Author: cig01

Created: <2023-12-10 Sun>

Last updated: <2023-12-18 Mon>

Creator: Emacs 27.1 (Org mode 9.4)