diff --git a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json index f6d76d07ca..1fb08f0790 100644 --- a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json +++ b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json @@ -2,8 +2,8 @@ "version": 1, "test_vectors": [ { - "id": "p2tr_using_v2_witness_version_error", - "objective": "Tests that a P2TR v2 scriptPubKey fails with use of witness version 2", + "id": "p2mr_misuse_v2_witness_version_with_pubkey_error", + "objective": "Tests that P2MR fails with use of internal_pubkey", "given": { "internalPubkey": "d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d", "scriptTree": null @@ -15,12 +15,12 @@ }, "expected": { "scriptPubKey": "522053a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343", - "error": "P2TR requires witness version of 1" + "error": "P2MR does not support internal pubkeys" } }, { "id": "p2mr_missing_leaf_script_tree_error", - "objective": "Tests P2MR with missing leaf script tree", + "objective": "Tests P2MR with null or missing script tree", "given": { "script_tree": "" }, @@ -57,7 +57,7 @@ }, { "id": "p2mr_different_version_leaves", - "objective": "Tests P2MR with two script leaves of different versions. TO-DO: currently ignores given leaf version and over-rides. Probably better to throw error", + "objective": "Tests P2MR with two script leaves of different versions. An unknown leaf version should be accepted and the P2MR output constructed successfully.", "given": { "scriptTree": [ { @@ -87,7 +87,7 @@ "bip350Address": "bc1zdskuzp4ts94h87ws0c7drmev3sf9dagewj8qsylyahfyqhf800hsam4d6e", "scriptPathControlBlocks": [ "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", - "c18ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + "fb8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" ] } }, @@ -123,8 +123,8 @@ "scriptPubKey": "522041646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b", "bip350Address": "bc1zg9jxlrqlu25kmkkh74r3h387uldfs72wlrz95n60c6j4n4svna4s4lhfhe", "scriptPathControlBlocks": [ - "c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9", - "c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42" + "c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42", + "c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9" ] } }, @@ -168,7 +168,6 @@ "id": "p2mr_three_leaf_complex", "objective": "Tests P2MR with a complex three-leaf script tree structure, demonstrating nested script paths and multiple verification options", "given": { - "internalPubkey": "e0dfe2300b0dd746a3f8674dfd4525623639042569d829c7f0eed9602d263e6f", "scriptTree": [ { "id": 0, @@ -205,8 +204,8 @@ "bip350Address": "bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq", "scriptPathControlBlocks": [ "c1ffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", - "c1ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", - "c19e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + "c19e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c1ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" ] } }, @@ -214,7 +213,6 @@ "id": "p2mr_three_leaf_alternative", "objective": "Tests another variant of P2MR with three leaves arranged in a different tree structure, showing alternative script path spending options", "given": { - "internalPubkey": "55adf4e8967fbd2e29f20ac896e60c3b0f1d5b0efa9d34941b5958c7b0a0312d", "scriptTree": [ { "id": 0, @@ -251,8 +249,53 @@ "bip350Address": "bc1z9a4jc5uhkmtgegvwpx3lq5tpv68layaf3pvz64wx7paatvejnhhsv52lcv", "scriptPathControlBlocks": [ "c13cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", - "c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", - "c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + "c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ] + } + }, + { + "id": "p2mr_duplicate_leaves", + "objective": "Ensure P2MR control blocks can be constructed correctly even when duplicate script leaves coexist in the same tree.", + "given": { + "scriptTree": [ + { + "id": 0, + "script": "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "asm": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "leafVersion": 192 + }, + [ + { + "id": 1, + "script": "20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac", + "asm": "d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748 OP_CHECKSIG", + "leafVersion": 192 + }, + { + "id": 2, + "script": "2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac", + "asm": "71981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2 OP_CHECKSIG", + "leafVersion": 192 + } + ] + ] + }, + "intermediary": { + "leafHashes": [ + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711", + "f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + ], + "merkleRoot": "3ab4f80153012398a7df2df273b0bd3cdb839883320ff06d62e8cc1ecb351ba7" + }, + "expected": { + "scriptPubKey": "52203ab4f80153012398a7df2df273b0bd3cdb839883320ff06d62e8cc1ecb351ba7", + "bip350Address": "bc1z8260sq2nqy3e3f7l9he88v9a8ndc8xyrxg8lqmtzarxpaje4rwnsxxww7h", + "scriptPathControlBlocks": [ + "c13a9adbb72f00022f76909e0f4bbab37dacafadb59618a4a13a123e13cb6f3019", + "c1f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9df154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" ] } } diff --git a/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json b/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json index 86a7f2fca7..75caf80812 100644 --- a/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json +++ b/bip-0360/ref-impl/common/tests/data/p2mr_pqc_construction.json @@ -71,8 +71,8 @@ "scriptPubKey": "52201619ce6d22a46dea045c4adf7f5f33d6810d00d0e9c8a4c7ba35db37b915c604", "bip350Address": "bc1zzcvuumfz53k75pzuft0h7hen66qs6qxsa8y2f3a6xhdn0wg4cczq0h84sj", "scriptPathControlBlocks": [ - "c13bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc", - "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a" + "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", + "c13bb0db8c6adcd87330a4a8c91be0fe1b23da3c151b6f2fb4f269429c43b8d8bc" ] } }, @@ -110,8 +110,8 @@ "scriptPubKey": "52202794771cd51f215ba3a19fbcdf08c771edb7de782a0c34457e0e9be5d0e4008f", "bip350Address": "bc1zy728w8x4rus4hgapn77d7zx8w8km0hnc9gxrg3t7p6d7t58yqz8sg0nccq", "scriptPathControlBlocks": [ - "c1cfd5fc07ac39947cba799e14f933f20e7c233dea72dc2792f5547c58cdce743e", - "c1a9745ac96d4f3702b78751f1e08f3040fbe6347e7b4f520d22d3f907730cbb7e" + "c1a9745ac96d4f3702b78751f1e08f3040fbe6347e7b4f520d22d3f907730cbb7e", + "c1cfd5fc07ac39947cba799e14f933f20e7c233dea72dc2792f5547c58cdce743e" ] } }, @@ -147,8 +147,8 @@ "scriptPubKey": "52205112b3edfd2c0b717491e9d4888ed2d5dfeaa25115143540e0a08516b68c008c", "bip350Address": "bc1z2yft8m0a9s9hzay3a82g3rkj6h074gj3z52r2s8q5zz3dd5vqzxqngpk2w", "scriptPathControlBlocks": [ - "c19de7eeded7832c28c6f80de76904dd79f98fd302747823b5bc5be440186b0c6d", - "c12cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb" + "c12cb2b90daa543b544161530c925f285b06196940d6085ca9474d41dc3822c5cb", + "c19de7eeded7832c28c6f80de76904dd79f98fd302747823b5bc5be440186b0c6d" ] } }, @@ -194,9 +194,9 @@ "scriptPubKey": "5220eaf8f557fdb9673de7bb9bad7e7452da9f44a3e65133fdadf2849c55cfb3cf5b", "bip350Address": "bc1zatu024lah9nnmeamnwkhuazjm205fglx2yelmt0jsjw9tnaneadszq7wg7", "scriptPathControlBlocks": [ - "c1837ef6677aeb0df2b0de47f45024684cc6ca03bda10fa30bb5bc05a94beb8dd1b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a", + "c118781f42f664d67acaf0ce7c6826437e5440eb1789f232af05e9a09fdf547903", "c10840c39e59eda6c9deee687a480cb169130c2f053ed2eb3134511ec1cfd8a2c7b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a", - "c118781f42f664d67acaf0ce7c6826437e5440eb1789f232af05e9a09fdf547903" + "c1837ef6677aeb0df2b0de47f45024684cc6ca03bda10fa30bb5bc05a94beb8dd1b2a5304f678cc5a2ed51feb377dd0a609bd22ec979cc608bfcf884d0f8e6f93a" ] } }, @@ -242,9 +242,9 @@ "scriptPubKey": "522051e3c1151ba73d9efce801837773331bf9030977242f62dfeb6756795f482409", "bip350Address": "bc1z283uz9gm5u7eal8gqxphwuenr0usxzthyshk9hltvat8jh6gysys28twnc", "scriptPathControlBlocks": [ - "c1dcef3ce86cc8cea78c9e00f3d9ef58360cb6ed3cb90ec62efe00b9703854ba5cddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361", + "c1b45680a7821e4b9450096ab38adbc3c99225af8f6c7ec121a0a5f1ae02893ba3", "c152e9326c2bf04d926b7e9f6c7645dd853f3f007b870201de9b814952750c9310ddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361", - "c1b45680a7821e4b9450096ab38adbc3c99225af8f6c7ec121a0a5f1ae02893ba3" + "c1dcef3ce86cc8cea78c9e00f3d9ef58360cb6ed3cb90ec62efe00b9703854ba5cddb521a44e33ff4974e618d8b8b7794275b7dc754d847c537404f84330454361" ] } } diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py new file mode 100644 index 0000000000..f5439136cd --- /dev/null +++ b/bip-0360/ref-impl/python/p2mr.py @@ -0,0 +1,427 @@ +""" +Simple example of construction for Pay-to-Merkle-Root (P2MR) outputs and control blocks. + +Usage: python -m p2mr +""" + +from enum import Enum +from typing import Any, Dict, List, Union + +import binascii +import hashlib +import json + + +class Encoding(Enum): + """enum type to list supported encodings""" + + BECH32 = 1 + BECH32M = 2 + + +BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2BC830A3 +MAX_COMPACT_SIZE = 2**64 - 1 + +# A script tree node is either a leaf (dict) or a branch (list of nodes) +ScriptTree = Union[Dict[str, Any], List["ScriptTree"]] + + +# +# Utility Functions +# +def sha256(b: bytes) -> bytes: + """sha256 hash function""" + return hashlib.sha256(b).digest() + + +def tagged_hash(tag: str, data: bytes) -> bytes: + """Compute tagged hash of data as per BIP-340""" + tag_hash = sha256(tag.encode()) + return sha256(tag_hash + tag_hash + data) + + +def h2b(h: str) -> bytes: + """hex-to-byte converter""" + return binascii.unhexlify(h) + + +def s2w(script: str) -> List[int]: + """Convert a script/witprog hex string to a List[int] of its bytes""" + return list(h2b(script)) + + +def get_compact_size(n: int) -> bytes: + """Get the compact size byte for given script""" + if not isinstance(n, int) or not (0 <= n <= MAX_COMPACT_SIZE): + raise ValueError( + "get_compact_size: out of bounds! must be 0 <= n <= 0xffffffffffffffff" + ) + if n < 0xFD: # single-byte case when size < 0xffff + return bytes([n]) + elif n <= 0xFFFF: + return b"\xfd" + n.to_bytes(2, "little") + elif n <= 0xFFFFFFFF: + return b"\xfe" + n.to_bytes(4, "little") + else: # n > 0xffffffff + return b"\xff" + n.to_bytes(8, "little") + + +def serialize_varbytes(b: bytes) -> bytes: + """Serialize variably-sized data as: compact-size byte || data bytes.""" + return get_compact_size(len(b)) + b + + +# +# P2MR-specific Functions +# +def tapleaf_hash(script: bytes, tapleaf_ver: int = 0xc0) -> bytes: + """Hash function for tree leaves""" + if not script: + raise ValueError("tapleaf_hash: script is required") + leaf = bytes([tapleaf_ver]) + serialize_varbytes(script) + return tagged_hash("TapLeaf", leaf) + + +def tapbranch_hash(left: bytes, right: bytes) -> bytes: + """Hash function for tree branches""" + return tagged_hash("TapBranch", b"".join(sorted((left, right)))) + + +def compute_merkle_root(tree: ScriptTree) -> bytes: + """Recursively compute script tree merkle root""" + if isinstance(tree, dict): # Leaf + version = tree["leafVersion"] + script = h2b(tree["script"]) + return tapleaf_hash(script=script, tapleaf_ver=version) + + elif isinstance(tree, list): # Branch + # Script trees are treated strictly as binary trees; each branch node should have + # exactly 2 children. This isn't a general n-ary fold, and combining + # more than 2 children sequentially would not produce a valid P2MR merkle root. + assert len(tree) == 2, f"expected binary branch, got {len(tree)} children" + left, right = compute_merkle_root(tree[0]), compute_merkle_root(tree[1]) + return tapbranch_hash(left, right) + + else: # badbadnotgood + raise ValueError("Invalid tree node") + + +def compute_control_block(path: int, tree: ScriptTree) -> bytes: + """ + Compute the control block for a script leaf at a given position in the script tree. + The `path` argument encodes the position as follows. + + Starting at depth zero, follow the branches of the script tree until reaching a leaf. + When we encounter a branch at any depth `d` (steps from the root), we look at the bit + `(path >> d) & 1` to decide whether to take the left or right branch. + """ + if isinstance(tree, dict): + return bytes([tree["leafVersion"] | 1]) + assert isinstance(tree, list) and len(tree) == 2 + + control_block = b"" + + while isinstance(tree, list): + assert len(tree) == 2 + sibling = tree[(path & 1) ^ 1] + tree = tree[(path & 1)] + control_block = compute_merkle_root(sibling) + control_block + path >>= 1 + + assert isinstance(tree, dict) + return bytes([tree["leafVersion"] | 1]) + control_block + + +# +# Bech32/Bech32m Encoding +# +# Bech32 encoding code is taken from sipa (BIP-0350), and has been tested against the test vectors therein: +# https://github.com/sipa/bech32/blob/master/ref/python/tests.py +# +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] + chk = 1 + for v in values: + top = chk >> 25 + chk = (chk & 0x1FFFFFF) << 5 ^ v + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data chars.""" + if not data: + raise ValueError("bech32 data portion must be provided") + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + if not data: + raise ValueError("bech32 data portion must be provided") + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + "1" + "".join([BECH32_CHARSET[c] for c in combined]) + + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( + bech.lower() != bech and bech.upper() != bech + ): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind("1") + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(c in BECH32_CHARSET for c in bech[pos + 1 :]): + return (None, None, None) + hrp = bech[:pos] + data = [BECH32_CHARSET.find(c) for c in bech[pos + 1 :]] + spec = bech32_verify_checksum(hrp, data) + if not spec: + return (None, None, None) + return (hrp, data[:-6], spec) + + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion""" + acc = 0 + bits = 0 + ret: List[int] = [] + 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 decode(hrp, addr): + """Decode a SegWit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if ( + data[0] == 0 + and spec != Encoding.BECH32 + or data[0] != 0 + and spec != Encoding.BECH32M + ): + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a SegWit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret + + +# +# BIP-360 Test Code +# +def collect_leaf_hashes(tree: ScriptTree) -> List[bytes]: + """Recursively collect leaf hashes in order (for verification)""" + if isinstance(tree, dict): # Leaf + version = tree["leafVersion"] + script = h2b(tree["script"]) + return [tapleaf_hash(script=script, tapleaf_ver=version)] + + elif isinstance(tree, list): # Branch: recurse on children + hashes: List[bytes] = [] + for sub in tree: + hashes.extend(collect_leaf_hashes(sub)) + return hashes + + else: + raise ValueError("Invalid tree node") + + +def walk_script_tree_paths( + script_tree: ScriptTree, path: int = 0, depth: int = 0 +) -> List[int]: + """Walk through a script tree and produce a list of the bit-encoded traversal paths for each leaf. + Used for testing compute_control_block.""" + if isinstance(script_tree, dict): + return [path] + assert isinstance(script_tree, list) and len(script_tree) == 2 + lchild_paths = walk_script_tree_paths(script_tree[0], path, depth + 1) + rchild_paths = walk_script_tree_paths( + script_tree[1], path | (1 << depth), depth + 1 + ) + return lchild_paths + rchild_paths + + +def collect_control_blocks(script_tree: ScriptTree) -> List[bytes]: + """Return control blocks for all leaves in tree declaration order. + Note: This ordering is for testing purposes. In practice, you would + compute the control block for a specific leaf at spend-time using + `compute_control_block(path, tree)`.""" + leaf_node_paths: List[int] = walk_script_tree_paths(script_tree) + return [compute_control_block(path, script_tree) for path in leaf_node_paths] + + +def extract_test_data(v: Dict[str, Any]) -> Dict[str, Any]: + """Extract test data from a test vector, returning None for missing keys""" + given = v.get("given", {}) + intermediary = v.get("intermediary", {}) + expected = v.get("expected", {}) + + return { + "id": v["id"], + "objective": v["objective"], + "script_tree": given.get("scriptTree"), + "leaf_hashes": intermediary.get("leafHashes"), + "merkle_root": intermediary.get("merkleRoot"), + "script_pubkey": expected.get("scriptPubKey"), + "bip350_address": expected.get("bip350Address"), + "script_path_control_blocks": expected.get("scriptPathControlBlocks"), + "error": expected.get("error"), + "has_internal_pubkey": "internalPubkey" in given, + } + + +def run_single_test(v: Dict[str, Any], test_num: int) -> bool: + """Run a single test vector. Returns True if passed.""" + print(f"\nBIP-360 Test Vector {test_num}\n{'-' * 25}") + + v = extract_test_data(v) + + try: + # Error Case: P2MR misuse / presence of internal pubkey + if v["has_internal_pubkey"]: + assert v["error"], "expected an error message" + print(f"Error: {v['error']}") + + # Error Case: Null/missing tree + elif v["script_tree"] is None: + assert ( + v["merkle_root"] is None + ), f"expected merkle_root None for null tree, got {v['merkle_root']}" + assert ( + v["leaf_hashes"] is None + ), f"expected leaf_hashes None for null tree, got {v['leaf_hashes']}" + assert ( + v["script_pubkey"] is None + ), f"expected script_pubkey None for null tree, got {v['script_pubkey']}" + assert v["error"], "expected an error message" + print(f"Error: {v['error']}") + + # General Case: Single- and Multi-Leaf script trees + else: + # test script leaf hashing + derived_leaf_hashes = [ + h.hex() for h in collect_leaf_hashes(v["script_tree"]) + ] + assert derived_leaf_hashes == v["leaf_hashes"], ( + f"leaf hash mismatch:\n" + f" derived: {derived_leaf_hashes}\n" + f" expected: {v['leaf_hashes']}" + ) + print("Leaf Hashes: [\n" + ",\n".join(derived_leaf_hashes) + "\n]") + + # test merkle root computation + derived_merkle_root = compute_merkle_root(v["script_tree"]).hex() + assert derived_merkle_root == v["merkle_root"], ( + f"merkle root mismatch: " + f"derived={derived_merkle_root}, expected={v['merkle_root']}" + ) + print(f"Merkle Root: {derived_merkle_root}") + + # test scriptPubkey formation + derived_scriptPubkey = f"5220{derived_merkle_root}" + assert derived_scriptPubkey == v["script_pubkey"], ( + f"scriptPubKey mismatch: " + f"derived={derived_scriptPubkey}, expected={v['script_pubkey']}" + ) + print(f"ScriptPubkey: {derived_scriptPubkey}") + + # test address encoding + if v["bip350_address"]: + derived_bip350_address = encode( + hrp="bc", witver=2, witprog=s2w(derived_merkle_root) + ) + assert derived_bip350_address == v["bip350_address"], ( + f"bip350 address mismatch: " + f"derived={derived_bip350_address}, expected={v['bip350_address']}" + ) + print(f"BIP350 Address: {derived_bip350_address}") + + # test control block derivation + if v["script_path_control_blocks"]: + derived_control_blocks = [ + cb.hex() for cb in collect_control_blocks(v["script_tree"]) + ] + assert derived_control_blocks == v["script_path_control_blocks"], ( + f"control blocks mismatch:\n" + f" derived: {derived_control_blocks}\n" + f" expected: {v['script_path_control_blocks']}" + ) + print( + "ScriptPathControlBlocks: [\n" + + ",\n".join(derived_control_blocks) + + "\n]" + ) + + print(f"\nPASSED '{v['id']}' with objective '{v['objective']}'") + return True + + except AssertionError as e: + print(f"FAILED '{v['id']}': {e}") + return False + + +def BIP360_tests() -> None: + """Run all BIP-360 Test Vectors.""" + print("\nRunning BIP-0360 Pay-to-Merkle-Root (P2MR) Tests...") + + with open("../common/tests/data/p2mr_construction.json", "r") as f: + test_vectors = json.load(f)["test_vectors"] + + passed = sum(run_single_test(v, i + 1) for i, v in enumerate(test_vectors)) + print(f"\n{passed}/{len(test_vectors)} BIP-360 tests passed successfully.") + + +if __name__ == "__main__": + BIP360_tests() diff --git a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs index 38d790d104..d0afc21b6e 100644 --- a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs +++ b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::HashMap; use bitcoin::{Network, ScriptBuf}; use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash}; use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo}; @@ -65,6 +65,9 @@ fn test_p2mr_single_leaf_script_tree() { process_test_vector_p2mr(test_vector).unwrap(); } +/// Verifies that P2MR construction succeeds when leaves carry non-standard leaf versions (e.g. 0xfa). +/// Unknown leaf versions are accepted: the TapLeaf hash is computed using the supplied version, +/// and the resulting merkle root and control blocks are valid. #[test] fn test_p2mr_different_version_leaves() { @@ -136,7 +139,7 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { // Use of TaprootBuilder avoids user error in constructing branches manually and ensures Merkle tree correctness and determinism let mut p2mr_builder: P2mrBuilder = P2mrBuilder::new(); - let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new(); + let mut script_to_id: HashMap = HashMap::new(); // 1) traverse test vector script tree and add leaves to P2MR builder if let Some(script_tree) = tv_script_tree { @@ -144,13 +147,12 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { script_tree.traverse_with_right_subtree_first(0, Direction::Root,&mut |node, depth, direction| { if let TVScriptTree::Leaf(tv_leaf) = node { - + let tv_leaf_script_bytes = hex::decode(&tv_leaf.script).unwrap(); - - // NOTE: IOT to execute script_info.control_block(..), will add these to a vector + let tv_leaf_script_buf = ScriptBuf::from_bytes(tv_leaf_script_bytes.clone()); let tv_leaf_version = LeafVersion::from_consensus(tv_leaf.leaf_version).unwrap(); - control_block_data.push((tv_leaf_script_buf.clone(), tv_leaf_version)); + script_to_id.insert(tv_leaf_script_buf.clone(), tv_leaf.id); let mut modified_depth = depth + 1; if direction == Direction::Root { @@ -194,14 +196,10 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { ); debug!("just passed merkle root validation: {}", test_vector_merkle_root); - let test_vector_leaf_hashes_vec: Vec = test_vector.intermediary.leaf_hashes.clone(); - let test_vector_leaf_hash_set: HashSet = test_vector_leaf_hashes_vec.iter().cloned().collect(); - let test_vector_control_blocks_vec = &test_vector.expected.script_path_control_blocks; - let test_vector_control_blocks_set: HashSet = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect(); + let expected_control_blocks = test_vector.expected.script_path_control_blocks.as_ref().unwrap(); let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap(); let script_leaves: ScriptLeaves = tap_tree.script_leaves(); - // TO-DO: Investigate why the ordering of script leaves seems to be reverse of test vectors. // 3) Iterate through leaves of derived script tree and verify both script leaf hashes and control blocks for derived_leaf in script_leaves { @@ -211,34 +209,26 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { let derived_leaf_hash: TapLeafHash = TapLeafHash::from_script(script, version); let leaf_hash = hex::encode(derived_leaf_hash.as_raw_hash().to_byte_array()); - assert!( - test_vector_leaf_hash_set.contains(&leaf_hash), - "Leaf hash not found in expected set for {}", leaf_hash - ); - debug!("just passed leaf_hash validation: {}", leaf_hash); - - // Each leaf in the script tree has a corresponding control block. - // Specific to P2TR, the 3 sections of the control block (control byte, public key & merkle path) are highlighted here: - // https://learnmeabitcoin.com/technical/upgrades/taproot/#script-path-spend-control-block - // The control block, which includes the Merkle path, must be 33 + 32 * n bytes, where n is the number of Merkle path hashes (n ≥ 0). - // There is no consensus limit on n, but large Merkle trees increase the witness size, impacting block weight. - // NOTE: Control blocks could have also been obtained from spend_info.control_block(..) using the data in control_block_data - debug!("merkle_branch nodes: {:?}", merkle_branch); - let derived_control_block: P2mrControlBlock = P2mrControlBlock{ - merkle_branch: merkle_branch.clone(), - }; - let serialized_control_block = derived_control_block.serialize(); - debug!("derived_control_block: {:?}, merkle_branch size: {}, control_block size: {}, serialized size: {}", - derived_control_block, - merkle_branch.len(), - derived_control_block.size(), - serialized_control_block.len()); - let derived_serialized_control_block = hex::encode(serialized_control_block); - assert!( - test_vector_control_blocks_set.contains(&derived_serialized_control_block), - "Control block mismatch: {}, expected: {:?}", derived_serialized_control_block, test_vector_control_blocks_set + + let leaf_id = script_to_id.get(script) + .unwrap_or_else(|| panic!("leaf script not found in script_to_id map: {}", hex::encode(script.as_bytes()))); + + // BIP341 control byte layout: bits 7..1 = leaf_version, bit 0 = parity. + // `& 0xfe` (11111110) masks off bit 0, isolating the leaf version in the upper 7 bits. + // `| 0x01` sets bit 0 to 1: P2MR has no key-spend path, so parity is always 1. + let control_byte = (version.to_consensus() & 0xfe) | 0x01u8; + let mut cb_buf = vec![control_byte]; + merkle_branch + .encode(&mut cb_buf) + .expect("encode should not fail"); + let derived_serialized_control_block = hex::encode(&cb_buf); + + let expected_cb = &expected_control_blocks[*leaf_id as usize]; + assert_eq!( + derived_serialized_control_block, *expected_cb, + "Control block mismatch for leaf id {}: derived {}, expected {}", leaf_id, derived_serialized_control_block, expected_cb ); - debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block); + debug!("leaf_id: {}, leaf_hash: {}, derived_serialized_control_block: {}", leaf_id, leaf_hash, derived_serialized_control_block); } diff --git a/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs index a5ca855cad..22f13ae411 100644 --- a/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs +++ b/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::HashMap; use bitcoin::{Network, ScriptBuf}; use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash}; use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo}; @@ -52,6 +52,9 @@ fn test_p2mr_pqc_single_leaf_script_tree() { process_test_vector_p2mr(test_vector).unwrap(); } +/// Verifies that P2MR construction succeeds when leaves carry non-standard leaf versions (e.g. 0xfa). +/// Unknown leaf versions are accepted: the TapLeaf hash is computed using the supplied version, +/// and the resulting merkle root and control blocks are valid. #[test] fn test_p2mr_pqc_different_version_leaves() { @@ -114,7 +117,7 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { // Use of TaprootBuilder avoids user error in constructing branches manually and ensures Merkle tree correctness and determinism let mut p2mr_builder: P2mrBuilder = P2mrBuilder::new(); - let mut control_block_data: Vec<(ScriptBuf, LeafVersion)> = Vec::new(); + let mut script_to_id: HashMap = HashMap::new(); // 1) traverse test vector script tree and add leaves to P2MR builder if let Some(script_tree) = tv_script_tree { @@ -122,13 +125,12 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { script_tree.traverse_with_right_subtree_first(0, Direction::Root,&mut |node, depth, direction| { if let TVScriptTree::Leaf(tv_leaf) = node { - + let tv_leaf_script_bytes = hex::decode(&tv_leaf.script).unwrap(); - - // NOTE: IOT to execute script_info.control_block(..), will add these to a vector + let tv_leaf_script_buf = ScriptBuf::from_bytes(tv_leaf_script_bytes.clone()); let tv_leaf_version = LeafVersion::from_consensus(tv_leaf.leaf_version).unwrap(); - control_block_data.push((tv_leaf_script_buf.clone(), tv_leaf_version)); + script_to_id.insert(tv_leaf_script_buf.clone(), tv_leaf.id); let mut modified_depth = depth + 1; if direction == Direction::Root { @@ -172,14 +174,10 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { ); debug!("just passed merkle root validation: {}", test_vector_merkle_root); - let test_vector_leaf_hashes_vec: Vec = test_vector.intermediary.leaf_hashes.clone(); - let test_vector_leaf_hash_set: HashSet = test_vector_leaf_hashes_vec.iter().cloned().collect(); - let test_vector_control_blocks_vec = &test_vector.expected.script_path_control_blocks; - let test_vector_control_blocks_set: HashSet = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect(); + let expected_control_blocks = test_vector.expected.script_path_control_blocks.as_ref().unwrap(); let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap(); let script_leaves: ScriptLeaves = tap_tree.script_leaves(); - // TO-DO: Investigate why the ordering of script leaves seems to be reverse of test vectors. // 3) Iterate through leaves of derived script tree and verify both script leaf hashes and control blocks for derived_leaf in script_leaves { @@ -189,34 +187,21 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { let derived_leaf_hash: TapLeafHash = TapLeafHash::from_script(script, version); let leaf_hash = hex::encode(derived_leaf_hash.as_raw_hash().to_byte_array()); - assert!( - test_vector_leaf_hash_set.contains(&leaf_hash), - "Leaf hash not found in expected set for {}", leaf_hash - ); - debug!("just passed leaf_hash validation: {}", leaf_hash); - - // Each leaf in the script tree has a corresponding control block. - // Specific to P2TR, the 3 sections of the control block (control byte, public key & merkle path) are highlighted here: - // https://learnmeabitcoin.com/technical/upgrades/taproot/#script-path-spend-control-block - // The control block, which includes the Merkle path, must be 33 + 32 * n bytes, where n is the number of Merkle path hashes (n ≥ 0). - // There is no consensus limit on n, but large Merkle trees increase the witness size, impacting block weight. - // NOTE: Control blocks could have also been obtained from spend_info.control_block(..) using the data in control_block_data - debug!("merkle_branch nodes: {:?}", merkle_branch); + + let leaf_id = script_to_id.get(script) + .unwrap_or_else(|| panic!("leaf script not found in script_to_id map: {}", hex::encode(script.as_bytes()))); + let derived_control_block: P2mrControlBlock = P2mrControlBlock{ merkle_branch: merkle_branch.clone(), }; - let serialized_control_block = derived_control_block.serialize(); - debug!("derived_control_block: {:?}, merkle_branch size: {}, control_block size: {}, serialized size: {}", - derived_control_block, - merkle_branch.len(), - derived_control_block.size(), - serialized_control_block.len()); - let derived_serialized_control_block = hex::encode(serialized_control_block); - assert!( - test_vector_control_blocks_set.contains(&derived_serialized_control_block), - "Control block mismatch: {}, expected: {:?}", derived_serialized_control_block, test_vector_control_blocks_set + let derived_serialized_control_block = hex::encode(derived_control_block.serialize()); + + let expected_cb = &expected_control_blocks[*leaf_id as usize]; + assert_eq!( + derived_serialized_control_block, *expected_cb, + "Control block mismatch for leaf id {}: derived {}, expected {}", leaf_id, derived_serialized_control_block, expected_cb ); - debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block); + debug!("leaf_id: {}, leaf_hash: {}, derived_serialized_control_block: {}", leaf_id, leaf_hash, derived_serialized_control_block); }