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 8601b562..1fb08f07 100644 --- a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json +++ b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json @@ -253,6 +253,51 @@ "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/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index 1a6f4484..ac6bf35e 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -130,52 +130,30 @@ def compute_merkle_root(tree: ScriptTree) -> str: raise ValueError("Invalid tree node") -def compute_control_block( - leaf: Dict[str, Any], tree: ScriptTree, path: Optional[List[str]] = None -) -> Optional[str]: - """Compute the control block for a given leaf in a given tree""" - if path is None: - path = [] +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): - if tree == leaf: - version_byte = (leaf["leafVersion"] | 1) & 0xFF - return f"{version_byte:02x}" + "".join(path) + return bytes([tree["leafVersion"] | 1]) + assert isinstance(tree, list) and len(tree) == 2 - return None + control_block = b"" - if isinstance(tree, list): - for i, child in enumerate(tree): - # build a list of sibling roots at this level - siblings: List[str] = [] - for j, sib in enumerate(tree): - if j != i: - siblings.append(compute_merkle_root(sib)) - # try this child; if it (or a descendant) matches, we get a result - result = compute_control_block(leaf, child, siblings + path) - if result: - return result + while isinstance(tree, list): + assert len(tree) == 2 + sibling = tree[(path & 1) ^ 1] + tree = tree[(path & 1)] + control_block = bytes.fromhex(compute_merkle_root(sibling)) + control_block + path >>= 1 - return None - - -def collect_control_blocks(script_tree: ScriptTree) -> List[str]: - """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(leaf, tree)`.""" - leaf_nodes: List[Dict[str, Any]] = [] - stack = [script_tree] - while stack: - node = stack.pop() - if isinstance(node, dict): - leaf_nodes.append(node) - elif isinstance(node, list): - stack.extend(reversed(node)) - - return [ - cb for leaf in leaf_nodes if (cb := compute_control_block(leaf, script_tree)) - ] + assert isinstance(tree, dict) + return bytes([tree["leafVersion"] | 1]) + control_block # @@ -306,6 +284,26 @@ def encode(hrp, witver, witprog): # # BIP-360 Test Code # +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[str]: + """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).hex() 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", {})