From 35b9ffe21b566b878b6d99564138aa21db85c594 Mon Sep 17 00:00:00 2001 From: jbride Date: Fri, 3 Apr 2026 18:38:43 -0600 Subject: [PATCH 01/14] bip360 test vectors: elements in control block array should be ordered to match tree structure of test vectors (#45) --- .../common/tests/data/p2mr_construction.json | 12 ++--- .../tests/data/p2mr_pqc_construction.json | 20 +++---- .../ref-impl/rust/tests/p2mr_construction.rs | 52 ++++++------------- .../rust/tests/p2mr_pqc_construction.rs | 52 ++++++------------- 4 files changed, 50 insertions(+), 86 deletions(-) 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..166b7a8d8c 100644 --- a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json +++ b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json @@ -123,8 +123,8 @@ "scriptPubKey": "522041646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b", "bip350Address": "bc1zg9jxlrqlu25kmkkh74r3h387uldfs72wlrz95n60c6j4n4svna4s4lhfhe", "scriptPathControlBlocks": [ - "c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9", - "c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42" + "c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42", + "c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9" ] } }, @@ -205,8 +205,8 @@ "bip350Address": "bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq", "scriptPathControlBlocks": [ "c1ffe578e9ea769027e4f5a3de40732f75a88a6353a09d767ddeb66accef85e553", - "c1ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", - "c19e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" + "c19e31407bffa15fefbf5090b149d53959ecdf3f62b1246780238c24501d5ceaf62645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817", + "c1ba982a91d4fc552163cb1c0da03676102d5b7a014304c01f0c77b2b8e888de1c2645a02e0aac1fe69d69755733a9b7621b694bb5b5cde2bbfc94066ed62b9817" ] } }, @@ -251,8 +251,8 @@ "bip350Address": "bc1z9a4jc5uhkmtgegvwpx3lq5tpv68layaf3pvz64wx7paatvejnhhsv52lcv", "scriptPathControlBlocks": [ "c13cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91", - "c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", - "c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d" + "c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d", + "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/rust/tests/p2mr_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs index 38d790d104..bf1f9bbf09 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}; @@ -136,7 +136,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 +144,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 +193,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 +206,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); } 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..6a7ec8f926 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}; @@ -114,7 +114,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 +122,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 +171,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 +184,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); } From 907db2034822fb18010e1b65ea1167c303175d33 Mon Sep 17 00:00:00 2001 From: jbride Date: Wed, 22 Apr 2026 10:35:07 -0600 Subject: [PATCH 02/14] bip360 test vectors: non-standard leaf version should be allowed during construction of P2MR output (#46) --- bip-0360/ref-impl/common/tests/data/p2mr_construction.json | 2 +- bip-0360/ref-impl/rust/tests/p2mr_construction.rs | 3 +++ bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) 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 166b7a8d8c..c6b9068384 100644 --- a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json +++ b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json @@ -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": [ { diff --git a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs index bf1f9bbf09..e9d93f7b91 100644 --- a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs +++ b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs @@ -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() { 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 6a7ec8f926..22f13ae411 100644 --- a/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs +++ b/bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs @@ -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() { From 8d28eb4517ef87d60bc4978e95b368e335db49b5 Mon Sep 17 00:00:00 2001 From: jbride Date: Thu, 30 Apr 2026 10:03:53 -0600 Subject: [PATCH 03/14] bip360 test vectors: control block now reflects use of non-standard leaf version --- .../common/tests/data/p2mr_construction.json | 2 +- bip-0360/ref-impl/rust/tests/p2mr_construction.rs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) 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 c6b9068384..fbee51b2ce 100644 --- a/bip-0360/ref-impl/common/tests/data/p2mr_construction.json +++ b/bip-0360/ref-impl/common/tests/data/p2mr_construction.json @@ -87,7 +87,7 @@ "bip350Address": "bc1zdskuzp4ts94h87ws0c7drmev3sf9dagewj8qsylyahfyqhf800hsam4d6e", "scriptPathControlBlocks": [ "c1f224a923cd0021ab202ab139cc56802ddb92dcfc172b9212261a539df79a112a", - "c18ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" + "fb8ad69ec7cf41c2a4001fd1f738bf1e505ce2277acdcaa63fe4765192497f47a7" ] } }, diff --git a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs index e9d93f7b91..d0afc21b6e 100644 --- a/bip-0360/ref-impl/rust/tests/p2mr_construction.rs +++ b/bip-0360/ref-impl/rust/tests/p2mr_construction.rs @@ -213,10 +213,15 @@ fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> { 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 derived_serialized_control_block = hex::encode(derived_control_block.serialize()); + // 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!( From 29e1a158e7314e403c0b655194b52c9faa1d4968 Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 13:14:27 -0600 Subject: [PATCH 04/14] bip-0360: added python reference example; test vector fixes This commit adds a python reference example for construction of BIP-360 outputs and control blocks. This commit also updates the test vectors. --- .../common/tests/data/p2mr_construction.json | 10 +- bip-0360/ref-impl/python/p2mr.py | 427 ++++++++++++++++++ 2 files changed, 431 insertions(+), 6 deletions(-) create mode 100644 bip-0360/ref-impl/python/p2mr.py 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 fbee51b2ce..8601b56248 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": "" }, @@ -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, @@ -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, diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py new file mode 100644 index 0000000000..1a6f448423 --- /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, Optional, 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 [int(f"{script[i:i + 2]}", 16) for i in range(0, len(script), 2)] + + +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: str, tapleaf_ver: str = "c0") -> str: + """Hash function for tree leaves""" + if not script: + raise ValueError("tapleaf_hash: script is required") + leaf = b"".join( + (bytes.fromhex(tapleaf_ver), serialize_varbytes(bytes.fromhex(script))) + ) + return tagged_hash("TapLeaf", leaf).hex() + + +def tapbranch_hash(left: str, right: str) -> bytes: + """Hash function for tree branches""" + if left < right: + return tagged_hash("TapBranch", h2b(left) + h2b(right)) + return tagged_hash("TapBranch", h2b(right) + h2b(left)) + + +def collect_leaf_hashes(tree: ScriptTree) -> List[str]: + """Recursively collect leaf hashes in order (for verification)""" + if isinstance(tree, dict): # Leaf + version = f"{tree['leafVersion']:x}" + script = tree["script"] + return [tapleaf_hash(script=script, tapleaf_ver=version)] + + elif isinstance(tree, list): # Branch: recurse on children + hashes: List[str] = [] + for sub in tree: + hashes.extend(collect_leaf_hashes(sub)) + return hashes + + else: + raise ValueError("Invalid tree node") + + +def compute_merkle_root(tree: ScriptTree) -> str: + """Recursively compute script tree merkle root""" + if isinstance(tree, dict): # Leaf + version = f"{tree['leafVersion']:x}" + script = tree["script"] + return tapleaf_hash(script=script, tapleaf_ver=version) + + elif isinstance(tree, list): # Branch + # Script trees are treated as strictly 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. It would also break here on a type mismatch. + # `tapbranch_hash` returns bytes, not the hex str this loop expects. + 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).hex() + + else: # badbadnotgood + 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 = [] + + if isinstance(tree, dict): + if tree == leaf: + version_byte = (leaf["leafVersion"] | 1) & 0xFF + return f"{version_byte:02x}" + "".join(path) + + return None + + 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 + + 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)) + ] + + +# +# 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 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 = 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"]) + 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 = 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() From 98c02898055d3d152b81b47d7c8ffdaeb941b646 Mon Sep 17 00:00:00 2001 From: conduition Date: Sat, 27 Jun 2026 01:02:51 +0000 Subject: [PATCH 05/14] 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 | 82 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index 1a6f448423..64d4f3421c 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 None + return bytes([tree["leafVersion"] | 1]) + assert isinstance(tree, list) and len(tree) == 2 - 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 - - return None + control_block = b"" + 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 -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 06/14] 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 8601b56248..1fb08f0790 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 07/14] 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 64d4f3421c..ac6bf35e17 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. From 2706a6181d727942172880933970221de8d081ea Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 19:35:32 -0600 Subject: [PATCH 08/14] bip360: Optional type removed / no longer needed --- bip-0360/ref-impl/python/p2mr.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index ac6bf35e17..3d3196e464 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -5,7 +5,7 @@ """ from enum import Enum -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Union import binascii import hashlib @@ -284,14 +284,18 @@ 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]: +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) + rchild_paths = walk_script_tree_paths( + script_tree[1], path | (1 << depth), depth + 1 + ) return lchild_paths + rchild_paths From 50a6b91352d4db5a541c8e582c1652008011e170 Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 20:00:28 -0600 Subject: [PATCH 09/14] bip360: simplify tapbranch_hash function Implements @conduitions improvement on tapbranch_hash() Co-authored-by: conduition --- bip-0360/ref-impl/python/p2mr.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index 3d3196e464..515ea0b6d5 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -87,9 +87,7 @@ def tapleaf_hash(script: str, tapleaf_ver: str = "c0") -> str: def tapbranch_hash(left: str, right: str) -> bytes: """Hash function for tree branches""" - if left < right: - return tagged_hash("TapBranch", h2b(left) + h2b(right)) - return tagged_hash("TapBranch", h2b(right) + h2b(left)) + return tagged_hash("TapBranch", b"".join(sorted((h2b(right) + h2b(left))))) def collect_leaf_hashes(tree: ScriptTree) -> List[str]: From 9bf46b5cfe6a36f16c7c15c1e0aef9c4190fd629 Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 19:49:28 -0600 Subject: [PATCH 10/14] bip360: make consistent use of h2b --- bip-0360/ref-impl/python/p2mr.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index 515ea0b6d5..3de8341288 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -79,9 +79,7 @@ def tapleaf_hash(script: str, tapleaf_ver: str = "c0") -> str: """Hash function for tree leaves""" if not script: raise ValueError("tapleaf_hash: script is required") - leaf = b"".join( - (bytes.fromhex(tapleaf_ver), serialize_varbytes(bytes.fromhex(script))) - ) + leaf = h2b(tapleaf_ver) + serialize_varbytes(h2b(script)) return tagged_hash("TapLeaf", leaf).hex() @@ -147,7 +145,7 @@ def compute_control_block(path: int, tree: ScriptTree) -> bytes: assert len(tree) == 2 sibling = tree[(path & 1) ^ 1] tree = tree[(path & 1)] - control_block = bytes.fromhex(compute_merkle_root(sibling)) + control_block + control_block = h2b(compute_merkle_root(sibling)) + control_block path >>= 1 assert isinstance(tree, dict) From ac2aa2ff7d2b8c6e992560dc4b7587ca0d211b70 Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 19:51:40 -0600 Subject: [PATCH 11/14] bip360: much cleaner, more readable s2w function --- 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 3de8341288..a8dc64f1c0 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -48,7 +48,7 @@ def h2b(h: str) -> bytes: def s2w(script: str) -> List[int]: """Convert a script/witprog hex string to a List[int] of its bytes""" - return [int(f"{script[i:i + 2]}", 16) for i in range(0, len(script), 2)] + return list(h2b(script)) def get_compact_size(n: int) -> bytes: From 8ea1eaac2d01b1632c32231d1b3bbd719710f66f Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 19:58:28 -0600 Subject: [PATCH 12/14] bip360: move collect_leaf_hashes() to the test code section --- bip-0360/ref-impl/python/p2mr.py | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index a8dc64f1c0..bd793234e7 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -88,23 +88,6 @@ def tapbranch_hash(left: str, right: str) -> bytes: return tagged_hash("TapBranch", b"".join(sorted((h2b(right) + h2b(left))))) -def collect_leaf_hashes(tree: ScriptTree) -> List[str]: - """Recursively collect leaf hashes in order (for verification)""" - if isinstance(tree, dict): # Leaf - version = f"{tree['leafVersion']:x}" - script = tree["script"] - return [tapleaf_hash(script=script, tapleaf_ver=version)] - - elif isinstance(tree, list): # Branch: recurse on children - hashes: List[str] = [] - for sub in tree: - hashes.extend(collect_leaf_hashes(sub)) - return hashes - - else: - raise ValueError("Invalid tree node") - - def compute_merkle_root(tree: ScriptTree) -> str: """Recursively compute script tree merkle root""" if isinstance(tree, dict): # Leaf @@ -280,6 +263,23 @@ def encode(hrp, witver, witprog): # # BIP-360 Test Code # +def collect_leaf_hashes(tree: ScriptTree) -> List[str]: + """Recursively collect leaf hashes in order (for verification)""" + if isinstance(tree, dict): # Leaf + version = f"{tree['leafVersion']:x}" + script = tree["script"] + return [tapleaf_hash(script=script, tapleaf_ver=version)] + + elif isinstance(tree, list): # Branch: recurse on children + hashes: List[str] = [] + 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]: From 64d068e9d101e631019e9aa95e1e0eb7cce832d9 Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 20:08:22 -0600 Subject: [PATCH 13/14] bip360: minor syntax fix on tapbranch_hash() --- 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 bd793234e7..d0b752f2e4 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -85,7 +85,7 @@ def tapleaf_hash(script: str, tapleaf_ver: str = "c0") -> str: def tapbranch_hash(left: str, right: str) -> bytes: """Hash function for tree branches""" - return tagged_hash("TapBranch", b"".join(sorted((h2b(right) + h2b(left))))) + return tagged_hash("TapBranch", b"".join(sorted((h2b(left), h2b(right))))) def compute_merkle_root(tree: ScriptTree) -> str: From 94b0f9db4cf39defd7d57fd6fa131776cd579993 Mon Sep 17 00:00:00 2001 From: notmike Date: Fri, 26 Jun 2026 23:03:50 -0600 Subject: [PATCH 14/14] bip360: consistency changes to p2mr core functions Standardizes all P2MR-specific functions to use bytes uniformly for input/output. Hex conversions are now confined to two boundaries: reading `script` field out of ScriptTree input, and comparing against hex-encoded test vector data in `run_single_test`. bech32 functions and s2w are left unchanged. --- bip-0360/ref-impl/python/p2mr.py | 48 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/bip-0360/ref-impl/python/p2mr.py b/bip-0360/ref-impl/python/p2mr.py index d0b752f2e4..f5439136cd 100644 --- a/bip-0360/ref-impl/python/p2mr.py +++ b/bip-0360/ref-impl/python/p2mr.py @@ -75,35 +75,33 @@ def serialize_varbytes(b: bytes) -> bytes: # # P2MR-specific Functions # -def tapleaf_hash(script: str, tapleaf_ver: str = "c0") -> str: +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 = h2b(tapleaf_ver) + serialize_varbytes(h2b(script)) - return tagged_hash("TapLeaf", leaf).hex() + leaf = bytes([tapleaf_ver]) + serialize_varbytes(script) + return tagged_hash("TapLeaf", leaf) -def tapbranch_hash(left: str, right: str) -> bytes: +def tapbranch_hash(left: bytes, right: bytes) -> bytes: """Hash function for tree branches""" - return tagged_hash("TapBranch", b"".join(sorted((h2b(left), h2b(right))))) + return tagged_hash("TapBranch", b"".join(sorted((left, right)))) -def compute_merkle_root(tree: ScriptTree) -> str: +def compute_merkle_root(tree: ScriptTree) -> bytes: """Recursively compute script tree merkle root""" if isinstance(tree, dict): # Leaf - version = f"{tree['leafVersion']:x}" - script = tree["script"] + version = tree["leafVersion"] + script = h2b(tree["script"]) return tapleaf_hash(script=script, tapleaf_ver=version) elif isinstance(tree, list): # Branch - # Script trees are treated as strictly binary trees; each branch node should have + # 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. It would also break here on a type mismatch. - # `tapbranch_hash` returns bytes, not the hex str this loop expects. + # 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).hex() + return tapbranch_hash(left, right) else: # badbadnotgood raise ValueError("Invalid tree node") @@ -128,7 +126,7 @@ def compute_control_block(path: int, tree: ScriptTree) -> bytes: assert len(tree) == 2 sibling = tree[(path & 1) ^ 1] tree = tree[(path & 1)] - control_block = h2b(compute_merkle_root(sibling)) + control_block + control_block = compute_merkle_root(sibling) + control_block path >>= 1 assert isinstance(tree, dict) @@ -263,15 +261,15 @@ def encode(hrp, witver, witprog): # # BIP-360 Test Code # -def collect_leaf_hashes(tree: ScriptTree) -> List[str]: +def collect_leaf_hashes(tree: ScriptTree) -> List[bytes]: """Recursively collect leaf hashes in order (for verification)""" if isinstance(tree, dict): # Leaf - version = f"{tree['leafVersion']:x}" - script = tree["script"] + 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[str] = [] + hashes: List[bytes] = [] for sub in tree: hashes.extend(collect_leaf_hashes(sub)) return hashes @@ -295,13 +293,13 @@ def walk_script_tree_paths( return lchild_paths + rchild_paths -def collect_control_blocks(script_tree: ScriptTree) -> List[str]: +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).hex() for path in leaf_node_paths] + return [compute_control_block(path, script_tree) for path in leaf_node_paths] def extract_test_data(v: Dict[str, Any]) -> Dict[str, Any]: @@ -353,7 +351,9 @@ def run_single_test(v: Dict[str, Any], test_num: int) -> bool: # General Case: Single- and Multi-Leaf script trees else: # test script leaf hashing - derived_leaf_hashes = collect_leaf_hashes(v["script_tree"]) + 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" @@ -362,7 +362,7 @@ def run_single_test(v: Dict[str, Any], test_num: int) -> bool: print("Leaf Hashes: [\n" + ",\n".join(derived_leaf_hashes) + "\n]") # test merkle root computation - derived_merkle_root = compute_merkle_root(v["script_tree"]) + 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']}" @@ -390,7 +390,9 @@ def run_single_test(v: Dict[str, Any], test_num: int) -> bool: # test control block derivation if v["script_path_control_blocks"]: - derived_control_blocks = collect_control_blocks(v["script_tree"]) + 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"