From 98c02898055d3d152b81b47d7c8ffdaeb941b646 Mon Sep 17 00:00:00 2001 From: conduition Date: Sat, 27 Jun 2026 01:02:51 +0000 Subject: [PATCH 1/3] bip360: simplify computing control blocks using explicit traversal paths This refactors `compute_control_block` to improve performance, and make the function simpler to read (to me at least). The previous version walked the entire script tree searching for the first matching instance of the leaf node. The new version expects the caller to pass in an explicit `path` parameter, which tells us exactly where the leaf node lives, with left/right steps encoded as bits in an integer. We walk down the tree straight to that leaf node, and build the control block as we go. This might not be the best DX for a real-world API or library, but this is just reference code, so we can accept poor usage ergonomics if it makes the code clearer and more explicit. --- bip-0360/ref-impl/python/p2mr.py | 80 ++++++++++++++++---------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index 1a6f4484..64d4f342 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) -> Optional[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", {}) From 86e7f9d4cafd1fd622e4a636ba1252e7614f21c3 Mon Sep 17 00:00:00 2001 From: conduition Date: Sat, 27 Jun 2026 01:12:47 +0000 Subject: [PATCH 2/3] add new test case for duplicate script leaves --- .../common/tests/data/p2mr_construction.json | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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" + ] + } } ] } From 1325ccc7dbebea9f62c687702ac5088a77481f0d Mon Sep 17 00:00:00 2001 From: conduition Date: Sat, 27 Jun 2026 01:15:28 +0000 Subject: [PATCH 3/3] return type is not optional anymore --- bip-0360/ref-impl/python/p2mr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index 64d4f342..ac6bf35e 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -130,7 +130,7 @@ def compute_merkle_root(tree: ScriptTree) -> str: raise ValueError("Invalid tree node") -def compute_control_block(path: int, tree: ScriptTree) -> Optional[bytes]: +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.