|
| 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(¤t, &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