Skip to content

Commit 95389bd

Browse files
committed
fix: review fixes for P2MR implementation
- Remove find_leaf_version() redundant DFS, store leaf version during compute_node traversal instead - Enable P2MR address encoding on all Bitcoin networks (mainnet + testnets), not just BitGo Signet - Add P2MR fixtures to bitcoin.json, testnet.json, bitcoinPublicSignet.json - Validate mainnet bc1z addresses from BIP-360 fixtures directly - Add missing Rust tests for lightning contract and three-leaf alternative vectors (now all 6 construction vectors covered in Rust) BTC-3241
1 parent 90fb908 commit 95389bd

6 files changed

Lines changed: 216 additions & 102 deletions

File tree

packages/wasm-utxo/src/address/networks.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,9 @@ impl Network {
185185
let taproot = segwit && matches!(self.mainnet(), Network::Bitcoin);
186186

187187
// P2MR (BIP-360) support:
188-
// Currently only enabled on the BitGo Signet for testing.
189-
let p2mr = matches!(self, Network::BitcoinBitGoSignet);
188+
// Enabled on all Bitcoin networks (mainnet + testnets) for address encoding.
189+
// Backend activation is controlled separately.
190+
let p2mr = matches!(self.mainnet(), Network::Bitcoin);
190191

191192
OutputScriptSupport {
192193
segwit,

packages/wasm-utxo/src/p2mr/mod.rs

Lines changed: 138 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -108,23 +108,20 @@ pub struct P2mrTreeInfo {
108108
pub leaves: Vec<P2mrLeafInfo>,
109109
}
110110

111+
/// Intermediate leaf data collected during tree traversal: (leaf_hash, leaf_version, merkle_path).
112+
type LeafCollector = Vec<([u8; 32], u8, Vec<[u8; 32]>)>;
113+
111114
/// Build a P2MR Merkle tree from a script tree definition.
112115
///
113116
/// Returns the Merkle root and per-leaf spending info (leaf hash + control block).
114117
/// Leaves are returned in left-to-right DFS order matching the input tree structure.
115118
pub fn build_p2mr_tree(tree: &ScriptTreeNode) -> P2mrTreeInfo {
116-
// Compute the merkle root and collect leaf hashes with their merkle proof paths.
117-
// Leaves are output in sorted DFS order (matching tap_branch_hash lex sorting).
118-
let mut leaves: Vec<([u8; 32], Vec<[u8; 32]>)> = Vec::new();
119+
let mut leaves: LeafCollector = Vec::new();
119120
let merkle_root = compute_node(tree, &mut leaves);
120121

121122
let leaf_infos = leaves
122123
.into_iter()
123-
.map(|(leaf_hash, path)| {
124-
// Determine leaf version from the leaf hash by looking up the tree.
125-
// We need the leaf version for the control byte. Extract it from the tree.
126-
let leaf_version =
127-
find_leaf_version(tree, &leaf_hash).unwrap_or(TAPSCRIPT_LEAF_VERSION);
124+
.map(|(leaf_hash, leaf_version, path)| {
128125
let mut control_block = vec![p2mr_control_byte(leaf_version)];
129126
for sibling in &path {
130127
control_block.extend_from_slice(sibling);
@@ -144,16 +141,16 @@ pub fn build_p2mr_tree(tree: &ScriptTreeNode) -> P2mrTreeInfo {
144141

145142
/// Recursively compute the hash of a tree node, collecting leaf info along the way.
146143
///
147-
/// Leaves are output in input DFS order (left subtree before right subtree,
148-
/// preserving the tree structure as given).
149-
fn compute_node(node: &ScriptTreeNode, leaves: &mut Vec<([u8; 32], Vec<[u8; 32]>)>) -> [u8; 32] {
144+
/// Each leaf entry stores (leaf_hash, leaf_version, merkle_path_siblings).
145+
/// Leaves are output in input DFS order (left subtree before right subtree).
146+
fn compute_node(node: &ScriptTreeNode, leaves: &mut LeafCollector) -> [u8; 32] {
150147
match node {
151148
ScriptTreeNode::Leaf {
152149
script,
153150
leaf_version,
154151
} => {
155152
let hash = tap_leaf_hash(script, *leaf_version);
156-
leaves.push((hash, Vec::new()));
153+
leaves.push((hash, *leaf_version, Vec::new()));
157154
hash
158155
}
159156
ScriptTreeNode::Branch(left, right) => {
@@ -165,36 +162,17 @@ fn compute_node(node: &ScriptTreeNode, leaves: &mut Vec<([u8; 32], Vec<[u8; 32]>
165162
// Add sibling hashes to the merkle proof paths.
166163
// Left subtree leaves need right_hash as sibling, and vice versa.
167164
for leaf in leaves[left_start..right_start].iter_mut() {
168-
leaf.1.push(right_hash);
165+
leaf.2.push(right_hash);
169166
}
170167
for leaf in leaves[right_start..].iter_mut() {
171-
leaf.1.push(left_hash);
168+
leaf.2.push(left_hash);
172169
}
173170

174171
tap_branch_hash(&left_hash, &right_hash)
175172
}
176173
}
177174
}
178175

179-
/// Find the leaf version for a leaf with the given hash (DFS search).
180-
fn find_leaf_version(node: &ScriptTreeNode, target_hash: &[u8; 32]) -> Option<u8> {
181-
match node {
182-
ScriptTreeNode::Leaf {
183-
script,
184-
leaf_version,
185-
} => {
186-
if tap_leaf_hash(script, *leaf_version) == *target_hash {
187-
Some(*leaf_version)
188-
} else {
189-
None
190-
}
191-
}
192-
ScriptTreeNode::Branch(left, right) => {
193-
find_leaf_version(left, target_hash).or_else(|| find_leaf_version(right, target_hash))
194-
}
195-
}
196-
}
197-
198176
/// Verify a P2MR control block against a leaf hash and expected merkle root.
199177
///
200178
/// Walks the merkle path in the control block, combining with `tap_branch_hash`
@@ -389,6 +367,133 @@ mod tests {
389367
);
390368
}
391369

370+
/// Test against BIP-360 spec vector: p2mr_simple_lightning_contract
371+
#[test]
372+
fn test_simple_lightning_contract() {
373+
let script0 = hex::decode(
374+
"029000b275209997a497d964fc1a62885b05a51166a65a90df00492c8d7cf61d6accf54803beac",
375+
)
376+
.unwrap();
377+
let script1 = hex::decode(
378+
"a8206c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd533388204edfcf9dfe6c0b5c83d1ab3f78d1b39a46ebac6798e08e19761f5ed89ec83c10ac",
379+
)
380+
.unwrap();
381+
382+
let tree = ScriptTreeNode::Branch(
383+
Box::new(ScriptTreeNode::Leaf {
384+
script: script0,
385+
leaf_version: 192,
386+
}),
387+
Box::new(ScriptTreeNode::Leaf {
388+
script: script1,
389+
leaf_version: 192,
390+
}),
391+
);
392+
let info = build_p2mr_tree(&tree);
393+
394+
assert_eq!(
395+
hex::encode(info.merkle_root),
396+
"41646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b"
397+
);
398+
399+
// Verify leaf hashes
400+
let leaf_hashes: Vec<String> = info
401+
.leaves
402+
.iter()
403+
.map(|l| hex::encode(l.leaf_hash))
404+
.collect();
405+
assert!(leaf_hashes.contains(
406+
&"c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9".to_string()
407+
));
408+
assert!(leaf_hashes.contains(
409+
&"632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42".to_string()
410+
));
411+
412+
assert_control_blocks_match(
413+
&info,
414+
&[
415+
"c1c81451874bd9ebd4b6fd4bba1f84cdfb533c532365d22a0a702205ff658b17c9",
416+
"c1632c8632b4f29c6291416e23135cf78ecb82e525788ea5ed6483e3c6ce943b42",
417+
],
418+
);
419+
420+
// Verify scriptPubKey and address
421+
let spk = build_p2mr_script_pubkey(&info.merkle_root);
422+
assert_eq!(
423+
hex::encode(spk.as_bytes()),
424+
"522041646f8c1fe2a96ddad7f5471bc4fee7da98794ef8c45a4f4fc6a559d60c9f6b"
425+
);
426+
}
427+
428+
/// Test against BIP-360 spec vector: p2mr_three_leaf_alternative
429+
#[test]
430+
fn test_three_leaf_alternative() {
431+
let script_a =
432+
hex::decode("2071981521ad9fc9036687364118fb6ccd2035b96a423c59c5430e98310a11abe2ac")
433+
.unwrap();
434+
let script_b =
435+
hex::decode("20d5094d2dbe9b76e2c245a2b89b6006888952e2faa6a149ae318d69e520617748ac")
436+
.unwrap();
437+
let script_c =
438+
hex::decode("20c440b462ad48c7a77f94cd4532d8f2119dcebbd7c9764557e62726419b08ad4cac")
439+
.unwrap();
440+
441+
let tree = ScriptTreeNode::Branch(
442+
Box::new(ScriptTreeNode::Leaf {
443+
script: script_a,
444+
leaf_version: 192,
445+
}),
446+
Box::new(ScriptTreeNode::Branch(
447+
Box::new(ScriptTreeNode::Leaf {
448+
script: script_b,
449+
leaf_version: 192,
450+
}),
451+
Box::new(ScriptTreeNode::Leaf {
452+
script: script_c,
453+
leaf_version: 192,
454+
}),
455+
)),
456+
);
457+
let info = build_p2mr_tree(&tree);
458+
459+
assert_eq!(
460+
hex::encode(info.merkle_root),
461+
"2f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def"
462+
);
463+
464+
// Verify leaf hashes
465+
let leaf_hashes: Vec<String> = info
466+
.leaves
467+
.iter()
468+
.map(|l| hex::encode(l.leaf_hash))
469+
.collect();
470+
assert!(leaf_hashes.contains(
471+
&"f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d".to_string()
472+
));
473+
assert!(leaf_hashes.contains(
474+
&"737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711".to_string()
475+
));
476+
assert!(leaf_hashes.contains(
477+
&"d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7".to_string()
478+
));
479+
480+
assert_control_blocks_match(
481+
&info,
482+
&[
483+
"c13cd369a528b326bc9d2133cbd2ac21451acb31681a410434672c8e34fe757e91",
484+
"c1737ed1fe30bc42b8022d717b44f0d93516617af64a64753b7a06bf16b26cd711f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d",
485+
"c1d7485025fceb78b9ed667db36ed8b8dc7b1f0b307ac167fa516fe4352b9f4ef7f154e8e8e17c31d3462d7132589ed29353c6fafdb884c5a6e04ea938834f0d9d",
486+
],
487+
);
488+
489+
// Verify scriptPubKey
490+
let spk = build_p2mr_script_pubkey(&info.merkle_root);
491+
assert_eq!(
492+
hex::encode(spk.as_bytes()),
493+
"52202f6b2c5397b6d68ca18e09a3f05161668ffe93a988582d55c6f07bd5b3329def"
494+
);
495+
}
496+
392497
/// Test against BIP-360 spec vector: p2mr_three_leaf_complex
393498
/// Tree structure: [A, [B, C]]
394499
#[test]

packages/wasm-utxo/test/fixtures/address/bitcoin.json

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99
"00141e231c7f9b3415daaa53ee5a7e12e120f00ec212",
1010
"bc1qrc33clumxs2a42jnaed8uyhpyrcqassje6kugr"
1111
],
12-
[
13-
"p2sh",
14-
"a91411510d2560794b3ed7bf734bc0e030e70e4db42d87",
15-
"33GaWMLihvhqWJ9YNd7xo8diSh6otdfi5w"
16-
],
12+
["p2sh", "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87", "33GaWMLihvhqWJ9YNd7xo8diSh6otdfi5w"],
1713
[
1814
"p2shP2wsh",
1915
"a9140c4e25aa3282fa35888f5e1eedb876265328312587",
@@ -44,11 +40,7 @@
4440
"001491b8f56f155030f74259be43dff4d94a6258d84a",
4541
"bc1qjxu02mc42qc0wsjehepalaxeff393kz2mzwu70"
4642
],
47-
[
48-
"p2sh",
49-
"a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87",
50-
"3MDt56c1ME4Vi8aYP9DJdhWngxo3yDxGJb"
51-
],
43+
["p2sh", "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87", "3MDt56c1ME4Vi8aYP9DJdhWngxo3yDxGJb"],
5244
[
5345
"p2shP2wsh",
5446
"a914696cab5f237c954fc1fade8c6b234fe93e0e80f287",
@@ -79,11 +71,7 @@
7971
"00140a058aec7588fca80070436b020c352c2891b680",
8072
"bc1qpgzc4mr43r72sqrsgd4syrp49s5frd5qq7et93"
8173
],
82-
[
83-
"p2sh",
84-
"a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87",
85-
"32BkhNhA8a7byNxgz6RR3jrG8T1mCXqiC8"
86-
],
74+
["p2sh", "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87", "32BkhNhA8a7byNxgz6RR3jrG8T1mCXqiC8"],
8775
[
8876
"p2shP2wsh",
8977
"a914f7db4f654f1211a63165cfdaf1170e96d433bc1387",
@@ -114,11 +102,7 @@
114102
"00145a8451539186feb4578b4f5613df6991e3078230",
115103
"bc1qt2z9z5u3smltg4utfatp8hmfj83s0q3s62ygc4"
116104
],
117-
[
118-
"p2sh",
119-
"a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187",
120-
"3FZavdFukrNR5aJcL7dmbc18tZ9tBkVtqK"
121-
],
105+
["p2sh", "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187", "3FZavdFukrNR5aJcL7dmbc18tZ9tBkVtqK"],
122106
[
123107
"p2shP2wsh",
124108
"a91493f1dd87104175795a1e37f5245461827237a05787",
@@ -138,5 +122,20 @@
138122
"p2trMusig2",
139123
"512085078b6ce45af8c4dea63248a5fae283d8edcca75f186395b237706c4bb42a36",
140124
"bc1ps5rckm8yttuvfh4xxfy2t7hzs0vwmn98tuvx89djxacxcja59gmq8lx0zu"
125+
],
126+
[
127+
"p2mr",
128+
"5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b",
129+
"bc1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4ssr0v9k"
130+
],
131+
[
132+
"p2mr",
133+
"5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc",
134+
"bc1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxqz20g2y"
135+
],
136+
[
137+
"p2mr",
138+
"5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2",
139+
"bc1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3qzgewqq"
141140
]
142141
]

packages/wasm-utxo/test/fixtures/address/bitcoinPublicSignet.json

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99
"00141e231c7f9b3415daaa53ee5a7e12e120f00ec212",
1010
"tb1qrc33clumxs2a42jnaed8uyhpyrcqassjnud0ns"
1111
],
12-
[
13-
"p2sh",
14-
"a91411510d2560794b3ed7bf734bc0e030e70e4db42d87",
15-
"2Mtpna6GkKPDBi5n63kjqR5cyf3JybU8XmH"
16-
],
12+
["p2sh", "a91411510d2560794b3ed7bf734bc0e030e70e4db42d87", "2Mtpna6GkKPDBi5n63kjqR5cyf3JybU8XmH"],
1713
[
1814
"p2shP2wsh",
1915
"a9140c4e25aa3282fa35888f5e1eedb876265328312587",
@@ -44,11 +40,7 @@
4440
"001491b8f56f155030f74259be43dff4d94a6258d84a",
4541
"tb1qjxu02mc42qc0wsjehepalaxeff393kz23y409u"
4642
],
47-
[
48-
"p2sh",
49-
"a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87",
50-
"2NCn68qY2xgZquvD64GqBFeW3uK1Dqn2PA4"
51-
],
43+
["p2sh", "a914d640bad0fafe2eeac9fc0e0f09fb899066263ebf87", "2NCn68qY2xgZquvD64GqBFeW3uK1Dqn2PA4"],
5244
[
5345
"p2shP2wsh",
5446
"a914696cab5f237c954fc1fade8c6b234fe93e0e80f287",
@@ -79,11 +71,7 @@
7971
"00140a058aec7588fca80070436b020c352c2891b680",
8072
"tb1qpgzc4mr43r72sqrsgd4syrp49s5frd5q2czc7z"
8173
],
82-
[
83-
"p2sh",
84-
"a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87",
85-
"2Msjxm7dBk2cxBAbEfE3HfgqXLoDw5wBFpQ"
86-
],
74+
["p2sh", "a914056f5a1c07fe38d27e554b88bd857f64bda8eb6f87", "2Msjxm7dBk2cxBAbEfE3HfgqXLoDw5wBFpQ"],
8775
[
8876
"p2shP2wsh",
8977
"a914f7db4f654f1211a63165cfdaf1170e96d433bc1387",
@@ -114,11 +102,7 @@
114102
"00145a8451539186feb4578b4f5613df6991e3078230",
115103
"tb1qt2z9z5u3smltg4utfatp8hmfj83s0q3ssvlmrx"
116104
],
117-
[
118-
"p2sh",
119-
"a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187",
120-
"2N77nzNBwNJsmHMwA1FFeDYzQ6uN3yhET3E"
121-
],
105+
["p2sh", "a9149829fb41e3c2bcf6a164310a7acbf0adcc0c3ee187", "2N77nzNBwNJsmHMwA1FFeDYzQ6uN3yhET3E"],
122106
[
123107
"p2shP2wsh",
124108
"a91493f1dd87104175795a1e37f5245461827237a05787",
@@ -138,5 +122,20 @@
138122
"p2trMusig2",
139123
"512085078b6ce45af8c4dea63248a5fae283d8edcca75f186395b237706c4bb42a36",
140124
"tb1ps5rckm8yttuvfh4xxfy2t7hzs0vwmn98tuvx89djxacxcja59gmqshsqcn"
125+
],
126+
[
127+
"p2mr",
128+
"5220c525714a7f49c28aedbbba78c005931a81c234b2f6c99a73e4d06082adc8bf2b",
129+
"tb1zc5jhzjnlf8pg4mdmhfuvqpvnr2quyd9j7mye5uly6psg9twghu4s8terle"
130+
],
131+
[
132+
"p2mr",
133+
"5220ab179431c28d3b68fb798957faf5497d69c883c6fb1e1cd9f81483d87bac90cc",
134+
"tb1z4vtegvwz35ak37me39tl4a2f045u3q7xlv0pek0czjpas7avjrxq4ze8st"
135+
],
136+
[
137+
"p2mr",
138+
"5220ccbd66c6f7e8fdab47b3a486f59d28262be857f30d4773f2d5ea47f7761ce0e2",
139+
"tb1zej7kd3hhar76k3an5jr0t8fgyc47s4lnp4rh8uk4afrlwasuur3q4q0p60"
141140
]
142-
]
141+
]

0 commit comments

Comments
 (0)