from enum import Enum
from typing import Any, Dict, List, Optional, Union
import binascii
import hashlib
import json
import os
class Encoding(Enum):
BECH32 = 1
BECH32M = 2
BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32M_CONST = 0x2BC830A3
MAX_COMPACT_SIZE = 2**64 - 1
ScriptTree = Union[Dict[str, Any], List["ScriptTree"]]
def sha256(b: bytes) -> bytes:
return hashlib.sha256(b).digest()
def tagged_hash(tag: str, data: bytes) -> bytes:
tag_hash = sha256(tag.encode())
return sha256(tag_hash + tag_hash + data)
def h2b(h: str) -> bytes:
return binascii.unhexlify(h)
def s2w(script: str) -> List[int]:
return list(h2b(script))
def get_compact_size(n: int) -> bytes:
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: 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: return b"\xff" + n.to_bytes(8, "little")
def serialize_varbytes(b: bytes) -> bytes:
return get_compact_size(len(b)) + b
def tapleaf_hash(script: bytes, tapleaf_ver: int = 0xc0) -> bytes:
if not script:
raise ValueError("tapleaf_hash: script is required")
leaf = bytes([tapleaf_ver & 0xfe]) + serialize_varbytes(script)
return tagged_hash("TapLeaf", leaf)
def tapbranch_hash(left: bytes, right: bytes) -> bytes:
return tagged_hash("TapBranch", b"".join(sorted((left, right))))
def compute_merkle_root(tree: ScriptTree) -> bytes:
if isinstance(tree, dict): version = tree["leafVersion"]
script = h2b(tree["script"])
return tapleaf_hash(script=script, tapleaf_ver=version)
elif isinstance(tree, list): 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: raise ValueError("Invalid tree node")
def compute_control_block(path: int, tree: ScriptTree) -> bytes:
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
def bech32_polymod(values):
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):
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32_verify_checksum(hrp, data):
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):
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):
combined = data + bech32_create_checksum(hrp, data, spec)
return hrp + "1" + "".join([BECH32_CHARSET[c] for c in combined])
def bech32_decode(bech):
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):
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):
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):
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
def collect_leaf_hashes(tree: ScriptTree) -> List[bytes]:
if isinstance(tree, dict): version = tree["leafVersion"]
script = h2b(tree["script"])
return [tapleaf_hash(script=script, tapleaf_ver=version)]
elif isinstance(tree, list): 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]:
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]:
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]:
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:
print(f"\nBIP-360 Test Vector {test_num}\n{'-' * 25}")
v = extract_test_data(v)
try:
if v["has_internal_pubkey"]:
assert v["error"], "expected an error message"
print(f"Error: {v['error']}")
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']}")
else:
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]")
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}")
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}")
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}")
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 default_fixtures_dir() -> str:
root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
return os.path.join(root, "tests", "vectors", "p2mr", "fixtures")
def load_construction_vectors(path: Optional[str] = None) -> list:
if path is None:
path = os.path.join(default_fixtures_dir(), "p2mr_construction.json")
with open(path, "r", encoding="utf-8") as f:
return json.load(f)["test_vectors"]
def BIP360_tests(fixture_path: Optional[str] = None) -> int:
print("\nRunning BIP-0360 Pay-to-Merkle-Root (P2MR) Tests...")
test_vectors = load_construction_vectors(fixture_path)
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.")
return len(test_vectors) - passed
if __name__ == "__main__":
import sys
failures = BIP360_tests()
pqc = os.path.join(default_fixtures_dir(), "p2mr_pqc_construction.json")
if os.path.isfile(pqc):
print("\n--- p2mr_pqc_construction.json ---")
failures += BIP360_tests(pqc)
sys.exit(1 if failures else 0)