Skip to content

Commit 7a36520

Browse files
committed
feat: add P2MR Merkle tree core module and BIP-360 spec tests
Add internal P2MR tree construction module (not exposed to JS): - Tagged hash computation (TapLeaf, TapBranch per BIP-341) - Script tree building with DFS traversal and per-leaf control blocks - Control block generation (leaf_version | 0x01 parity) and verification - Merkle proof verification against expected root Rust tests cover all 6 BIP-360 construction vectors (single leaf, two-leaf same/different versions, lightning contract, two three-leaf variants). TS tests validate mainnet bc1z address encoding against BIP-360 fixture addresses. BTC-3241
1 parent b2538e8 commit 7a36520

6 files changed

Lines changed: 1328 additions & 0 deletions

File tree

packages/wasm-utxo/bips/bip-0360/bip-0360.mediawiki

Lines changed: 430 additions & 0 deletions
Large diffs are not rendered by default.

packages/wasm-utxo/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod inscriptions;
88
pub mod inspect;
99
pub mod message;
1010
mod networks;
11+
pub mod p2mr;
1112
pub mod paygo;
1213
pub mod psbt_ops;
1314
#[cfg(test)]

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

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
//! BIP-360 Pay-to-Merkle-Root (P2MR) support.
2+
//!
3+
//! Implements Merkle tree construction and control block generation for
4+
//! SegWit v2 P2MR outputs. The tree structure is identical to Taproot
5+
//! (BIP-341) but without key-path spending or TapTweak.
6+
//!
7+
//! See `bips/bip-0360/bip-0360.mediawiki` for the full specification.
8+
9+
use miniscript::bitcoin::hashes::Hash;
10+
use miniscript::bitcoin::taproot::{LeafVersion, TapLeafHash, TapNodeHash};
11+
use miniscript::bitcoin::{Script, ScriptBuf};
12+
13+
/// Default leaf version for TapScript (BIP 342).
14+
pub const TAPSCRIPT_LEAF_VERSION: u8 = 0xc0;
15+
16+
/// Convert a raw leaf version byte to `LeafVersion`.
17+
fn leaf_version_from_u8(v: u8) -> LeafVersion {
18+
if v == TAPSCRIPT_LEAF_VERSION {
19+
LeafVersion::TapScript
20+
} else {
21+
LeafVersion::from_consensus(v).expect("valid even leaf version")
22+
}
23+
}
24+
25+
/// Compute the TapLeafHash for a script and leaf version.
26+
pub fn tap_leaf_hash(script: &[u8], leaf_version: u8) -> [u8; 32] {
27+
TapLeafHash::from_script(
28+
Script::from_bytes(script),
29+
leaf_version_from_u8(leaf_version),
30+
)
31+
.to_byte_array()
32+
}
33+
34+
/// Compute the TapBranchHash from two node hashes (lexicographically sorted).
35+
pub fn tap_branch_hash(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
36+
TapNodeHash::from_node_hashes(
37+
TapNodeHash::assume_hidden(*a),
38+
TapNodeHash::assume_hidden(*b),
39+
)
40+
.to_byte_array()
41+
}
42+
43+
/// P2MR control byte: leaf_version with parity bit forced to 1 (BIP-360 consensus rule).
44+
///
45+
/// Unlike Taproot where the parity bit encodes the output key's Y parity,
46+
/// P2MR always sets the LSB to 1 as a distinguisher.
47+
pub fn p2mr_control_byte(leaf_version: u8) -> u8 {
48+
(leaf_version & 0xfe) | 0x01
49+
}
50+
51+
/// Build the 34-byte P2MR scriptPubKey: `OP_2 (0x52) OP_PUSHBYTES_32 (0x20) <merkle_root>`
52+
pub fn build_p2mr_script_pubkey(merkle_root: &[u8; 32]) -> ScriptBuf {
53+
let mut bytes = Vec::with_capacity(34);
54+
bytes.push(0x52); // OP_2
55+
bytes.push(0x20); // OP_PUSHBYTES_32
56+
bytes.extend_from_slice(merkle_root);
57+
ScriptBuf::from(bytes)
58+
}
59+
60+
/// A node in a P2MR script tree (recursive structure matching BIP-360 test vectors).
61+
///
62+
/// Trees are specified as either a single leaf or a pair of branches:
63+
/// - Single leaf: `Leaf { script, leaf_version }`
64+
/// - Two branches: `Branch(left, right)` where each side is another `ScriptTreeNode`
65+
///
66+
/// The BIP-360 test vectors use JSON arrays for branches and objects for leaves:
67+
/// - `{ "script": "...", "leafVersion": 192 }` → leaf
68+
/// - `[node, node]` → branch
69+
#[derive(Debug, Clone)]
70+
pub enum ScriptTreeNode {
71+
Leaf { script: Vec<u8>, leaf_version: u8 },
72+
Branch(Box<ScriptTreeNode>, Box<ScriptTreeNode>),
73+
}
74+
75+
/// Per-leaf spending info produced by tree construction.
76+
#[derive(Debug, Clone)]
77+
pub struct P2mrLeafInfo {
78+
/// The TapLeafHash for this leaf.
79+
pub leaf_hash: [u8; 32],
80+
/// Serialized control block: `control_byte || merkle_path_siblings`.
81+
/// Length is `1 + 32 * depth`.
82+
pub control_block: Vec<u8>,
83+
}
84+
85+
/// Complete P2MR tree info.
86+
#[derive(Debug, Clone)]
87+
pub struct P2mrTreeInfo {
88+
/// The 32-byte Merkle root (committed in the scriptPubKey).
89+
pub merkle_root: [u8; 32],
90+
/// Per-leaf spending info, in left-to-right DFS order.
91+
pub leaves: Vec<P2mrLeafInfo>,
92+
}
93+
94+
/// Intermediate leaf data collected during tree traversal: (leaf_hash, leaf_version, merkle_path).
95+
type LeafCollector = Vec<([u8; 32], u8, Vec<[u8; 32]>)>;
96+
97+
/// Build a P2MR Merkle tree from a script tree definition.
98+
///
99+
/// Returns the Merkle root and per-leaf spending info (leaf hash + control block).
100+
/// Leaves are returned in left-to-right DFS order matching the input tree structure.
101+
pub fn build_p2mr_tree(tree: &ScriptTreeNode) -> P2mrTreeInfo {
102+
let mut leaves: LeafCollector = Vec::new();
103+
let merkle_root = compute_node(tree, &mut leaves);
104+
105+
let leaf_infos = leaves
106+
.into_iter()
107+
.map(|(leaf_hash, leaf_version, path)| {
108+
let mut control_block = vec![p2mr_control_byte(leaf_version)];
109+
for sibling in &path {
110+
control_block.extend_from_slice(sibling);
111+
}
112+
P2mrLeafInfo {
113+
leaf_hash,
114+
control_block,
115+
}
116+
})
117+
.collect();
118+
119+
P2mrTreeInfo {
120+
merkle_root,
121+
leaves: leaf_infos,
122+
}
123+
}
124+
125+
/// Recursively compute the hash of a tree node, collecting leaf info along the way.
126+
///
127+
/// Each leaf entry stores (leaf_hash, leaf_version, merkle_path_siblings).
128+
/// Leaves are output in input DFS order (left subtree before right subtree).
129+
fn compute_node(node: &ScriptTreeNode, leaves: &mut LeafCollector) -> [u8; 32] {
130+
match node {
131+
ScriptTreeNode::Leaf {
132+
script,
133+
leaf_version,
134+
} => {
135+
let hash = tap_leaf_hash(script, *leaf_version);
136+
leaves.push((hash, *leaf_version, Vec::new()));
137+
hash
138+
}
139+
ScriptTreeNode::Branch(left, right) => {
140+
let left_start = leaves.len();
141+
let left_hash = compute_node(left, leaves);
142+
let right_start = leaves.len();
143+
let right_hash = compute_node(right, leaves);
144+
145+
// Add sibling hashes to the merkle proof paths.
146+
// Left subtree leaves need right_hash as sibling, and vice versa.
147+
for leaf in leaves[left_start..right_start].iter_mut() {
148+
leaf.2.push(right_hash);
149+
}
150+
for leaf in leaves[right_start..].iter_mut() {
151+
leaf.2.push(left_hash);
152+
}
153+
154+
tap_branch_hash(&left_hash, &right_hash)
155+
}
156+
}
157+
}
158+
159+
/// Verify a P2MR control block against a leaf hash and expected merkle root.
160+
///
161+
/// Walks the merkle path in the control block, combining with `tap_branch_hash`
162+
/// at each step, and checks the result matches the expected root.
163+
pub fn verify_control_block(
164+
leaf_hash: &[u8; 32],
165+
control_block: &[u8],
166+
expected_root: &[u8; 32],
167+
) -> bool {
168+
if control_block.is_empty() {
169+
return false;
170+
}
171+
// Control block: 1 byte (control_byte) + 32*d bytes (merkle path)
172+
let path_bytes = &control_block[1..];
173+
if !path_bytes.len().is_multiple_of(32) {
174+
return false;
175+
}
176+
177+
let mut current = *leaf_hash;
178+
for chunk in path_bytes.chunks_exact(32) {
179+
let sibling: [u8; 32] = chunk.try_into().unwrap();
180+
current = tap_branch_hash(&current, &sibling);
181+
}
182+
current == *expected_root
183+
}
184+
185+
#[cfg(test)]
186+
mod tests {
187+
use super::*;
188+
189+
fn load_fixture() -> serde_json::Value {
190+
let content = std::fs::read_to_string("test/fixtures/p2mr/p2mr_construction.json")
191+
.expect("Failed to load p2mr_construction.json");
192+
serde_json::from_str(&content).expect("Failed to parse fixture")
193+
}
194+
195+
fn get_vector(fixture: &serde_json::Value, id: &str) -> serde_json::Value {
196+
fixture["test_vectors"]
197+
.as_array()
198+
.unwrap()
199+
.iter()
200+
.find(|v| v["id"].as_str() == Some(id))
201+
.unwrap_or_else(|| panic!("Vector '{}' not found", id))
202+
.clone()
203+
}
204+
205+
/// Parse a fixture scriptTree node into our ScriptTreeNode.
206+
fn parse_fixture_tree(node: &serde_json::Value) -> ScriptTreeNode {
207+
if node.is_array() {
208+
let arr = node.as_array().unwrap();
209+
assert_eq!(arr.len(), 2);
210+
ScriptTreeNode::Branch(
211+
Box::new(parse_fixture_tree(&arr[0])),
212+
Box::new(parse_fixture_tree(&arr[1])),
213+
)
214+
} else {
215+
ScriptTreeNode::Leaf {
216+
script: hex::decode(node["script"].as_str().unwrap()).unwrap(),
217+
leaf_version: node["leafVersion"].as_u64().unwrap() as u8,
218+
}
219+
}
220+
}
221+
222+
/// Run a construction test vector: build the tree and verify merkle root,
223+
/// leaf hashes, scriptPubKey, and control blocks against the fixture.
224+
fn run_construction_vector(id: &str) {
225+
let fixture = load_fixture();
226+
let vector = get_vector(&fixture, id);
227+
228+
let script_tree = &vector["given"]["scriptTree"];
229+
let tree = parse_fixture_tree(script_tree);
230+
let info = build_p2mr_tree(&tree);
231+
232+
// Verify merkle root
233+
let expected_root = vector["intermediary"]["merkleRoot"].as_str().unwrap();
234+
assert_eq!(hex::encode(info.merkle_root), expected_root, "merkle root");
235+
236+
// Verify leaf hashes
237+
let expected_leaf_hashes: Vec<&str> = vector["intermediary"]["leafHashes"]
238+
.as_array()
239+
.unwrap()
240+
.iter()
241+
.map(|v| v.as_str().unwrap())
242+
.collect();
243+
assert_eq!(info.leaves.len(), expected_leaf_hashes.len(), "leaf count");
244+
let actual_leaf_hashes: Vec<String> = info
245+
.leaves
246+
.iter()
247+
.map(|l| hex::encode(l.leaf_hash))
248+
.collect();
249+
for expected in &expected_leaf_hashes {
250+
assert!(
251+
actual_leaf_hashes.contains(&expected.to_string()),
252+
"missing leaf hash: {}",
253+
expected
254+
);
255+
}
256+
257+
// Verify scriptPubKey
258+
let expected_spk = vector["expected"]["scriptPubKey"].as_str().unwrap();
259+
let spk = build_p2mr_script_pubkey(&info.merkle_root);
260+
assert_eq!(hex::encode(spk.as_bytes()), expected_spk, "scriptPubKey");
261+
262+
// Verify bip350 address if present
263+
if let Some(expected_addr) = vector["expected"]["bip350Address"].as_str() {
264+
let addr = crate::from_output_script_with_coin(&spk, "btc")
265+
.expect("failed to encode P2MR address");
266+
assert_eq!(addr, expected_addr, "bip350Address");
267+
}
268+
269+
// Verify control blocks (order-independent, validated cryptographically)
270+
let expected_cbs: Vec<&str> = vector["expected"]["scriptPathControlBlocks"]
271+
.as_array()
272+
.unwrap()
273+
.iter()
274+
.map(|v| v.as_str().unwrap())
275+
.collect();
276+
assert_eq!(info.leaves.len(), expected_cbs.len(), "control block count");
277+
278+
// Each generated control block must verify against the merkle root
279+
for leaf in &info.leaves {
280+
assert!(
281+
verify_control_block(&leaf.leaf_hash, &leaf.control_block, &info.merkle_root),
282+
"control block for leaf {} doesn't verify",
283+
hex::encode(leaf.leaf_hash)
284+
);
285+
}
286+
// Each expected control block must verify against some leaf
287+
for cb_hex in &expected_cbs {
288+
let cb = hex::decode(cb_hex).unwrap();
289+
let verified = info
290+
.leaves
291+
.iter()
292+
.any(|l| verify_control_block(&l.leaf_hash, &cb, &info.merkle_root));
293+
assert!(
294+
verified,
295+
"expected control block doesn't verify: {}",
296+
cb_hex
297+
);
298+
}
299+
}
300+
301+
#[test]
302+
fn test_p2mr_control_byte() {
303+
assert_eq!(p2mr_control_byte(0xc0), 0xc1);
304+
assert_eq!(p2mr_control_byte(0xfa), 0xfb);
305+
assert_eq!(p2mr_control_byte(0xc1), 0xc1);
306+
}
307+
308+
#[test]
309+
fn test_single_leaf_tree() {
310+
run_construction_vector("p2mr_single_leaf_script_tree");
311+
}
312+
313+
#[test]
314+
fn test_two_leaf_same_version() {
315+
run_construction_vector("p2mr_two_leaf_same_version");
316+
}
317+
318+
#[test]
319+
fn test_different_version_leaves() {
320+
run_construction_vector("p2mr_different_version_leaves");
321+
}
322+
323+
#[test]
324+
fn test_simple_lightning_contract() {
325+
run_construction_vector("p2mr_simple_lightning_contract");
326+
}
327+
328+
#[test]
329+
fn test_three_leaf_complex() {
330+
run_construction_vector("p2mr_three_leaf_complex");
331+
}
332+
333+
#[test]
334+
fn test_three_leaf_alternative() {
335+
run_construction_vector("p2mr_three_leaf_alternative");
336+
}
337+
}

0 commit comments

Comments
 (0)