Skip to content

Commit 3faae22

Browse files
committed
feat: add P2MR Merkle tree, WASM bindings, and BIP-360 spec tests
Add core P2MR tree construction module with WASM bindings and comprehensive tests against BIP-360 specification test vectors. Rust core (src/p2mr/mod.rs): - 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 WASM bindings (src/wasm/p2mr.rs): - P2mrNamespace with computeLeafHash, computeBranchHash, buildTree, buildScriptPubkey, verifyControlBlock - Uses wasm-utxo TryIntoJsValue/get_field pattern (not serde_wasm_bindgen) - Tree deserialization: leaf = {script, leafVersion?}, branch = [left, right] TypeScript (js/p2mr.ts): - Typed wrapper with ScriptTreeNode, P2mrLeafInfo, P2mrTreeInfo types Tests: - Fixture-driven tests against all 8 BIP-360 p2mr_construction vectors BTC-3241
1 parent e2330d5 commit 3faae22

9 files changed

Lines changed: 1416 additions & 3 deletions

File tree

packages/wasm-utxo/js/p2mr.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* P2MR (Pay-to-Merkle-Root) tree construction and verification (BIP-360)
3+
*
4+
* This module provides functions for building P2MR script trees,
5+
* computing leaf/branch hashes, and verifying control blocks.
6+
*/
7+
8+
import { P2mrNamespace } from "./wasm/wasm_utxo.js";
9+
10+
/** A leaf node in a P2MR script tree */
11+
export type ScriptTreeLeaf = {
12+
/** The compiled script bytes */
13+
script: Uint8Array;
14+
/** Leaf version (defaults to 0xc0 TapScript) */
15+
leafVersion?: number;
16+
};
17+
18+
/** A P2MR script tree node: either a leaf or a branch [left, right] */
19+
export type ScriptTreeNode = ScriptTreeLeaf | [ScriptTreeNode, ScriptTreeNode];
20+
21+
/** Per-leaf spending info returned by buildTree */
22+
export type P2mrLeafInfo = {
23+
leafHash: Uint8Array;
24+
controlBlock: Uint8Array;
25+
};
26+
27+
/** Result of building a P2MR tree */
28+
export type P2mrTreeInfo = {
29+
merkleRoot: Uint8Array;
30+
leaves: P2mrLeafInfo[];
31+
};
32+
33+
/**
34+
* Compute the TapLeafHash for a script.
35+
*
36+
* @param script - The script bytes
37+
* @param leafVersion - Leaf version (defaults to 0xc0 TapScript)
38+
* @returns 32-byte leaf hash
39+
*/
40+
export function computeLeafHash(script: Uint8Array, leafVersion?: number): Uint8Array {
41+
return P2mrNamespace.computeLeafHash(script, leafVersion ?? undefined);
42+
}
43+
44+
/**
45+
* Compute the TapBranchHash from two 32-byte hashes.
46+
*
47+
* @param a - First 32-byte hash
48+
* @param b - Second 32-byte hash
49+
* @returns 32-byte branch hash
50+
*/
51+
export function computeBranchHash(a: Uint8Array, b: Uint8Array): Uint8Array {
52+
return P2mrNamespace.computeBranchHash(a, b);
53+
}
54+
55+
/**
56+
* Build a P2MR tree from a script tree definition.
57+
*
58+
* @param tree - A script tree: either a leaf `{ script, leafVersion? }`
59+
* or a branch `[left, right]` where each side is another tree node.
60+
* @returns The merkle root and per-leaf spending info (leaf hash + control block)
61+
*/
62+
export function buildTree(tree: ScriptTreeNode): P2mrTreeInfo {
63+
return P2mrNamespace.buildTree(tree) as P2mrTreeInfo;
64+
}
65+
66+
/**
67+
* Build the 34-byte P2MR scriptPubKey from a 32-byte Merkle root.
68+
*
69+
* @param merkleRoot - 32-byte merkle root
70+
* @returns 34-byte scriptPubKey (OP_2 + OP_PUSHBYTES_32 + root)
71+
*/
72+
export function buildScriptPubkey(merkleRoot: Uint8Array): Uint8Array {
73+
return P2mrNamespace.buildScriptPubkey(merkleRoot);
74+
}
75+
76+
/**
77+
* Verify a P2MR control block against a leaf hash and expected merkle root.
78+
*
79+
* @param leafHash - 32-byte leaf hash
80+
* @param controlBlock - The control block bytes
81+
* @param expectedRoot - 32-byte expected merkle root
82+
* @returns True if the control block is valid
83+
*/
84+
export function verifyControlBlock(
85+
leafHash: Uint8Array,
86+
controlBlock: Uint8Array,
87+
expectedRoot: Uint8Array,
88+
): boolean {
89+
return P2mrNamespace.verifyControlBlock(leafHash, controlBlock, expectedRoot);
90+
}

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4969,9 +4969,9 @@ mod tests {
49694969
}
49704970

49714971
// If both have non_witness_utxo, compare the relevant output
4972-
if orig.non_witness_utxo.is_some() && recon.non_witness_utxo.is_some() {
4973-
let orig_tx = orig.non_witness_utxo.as_ref().unwrap();
4974-
let recon_tx = recon.non_witness_utxo.as_ref().unwrap();
4972+
if let (Some(orig_tx), Some(recon_tx)) =
4973+
(&orig.non_witness_utxo, &recon.non_witness_utxo)
4974+
{
49754975
let vout = original_tx.input[idx].previous_output.vout as usize;
49764976
assert_eq!(
49774977
orig_tx.output.get(vout),

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)]

0 commit comments

Comments
 (0)