mirror of
https://github.com/bitcoin/bips.git
synced 2026-03-23 16:05:41 +00:00
BIP360: Pay to Merkle Root (P2MR) (#1670)
Review comments and assistance by: Armin Sabouri <armins88@gmail.com> D++ <82842780+dplusplus1024@users.noreply.github.com> Jameson Lopp <jameson.lopp@gmail.com> jbride <jbride2001@yahoo.com> Joey Yandle <xoloki@gmail.com> Jon Atack <jon@atack.com> Jonas Nick <jonasd.nick@gmail.com> Kyle Crews <kylecrews@Kyles-Mac-Studio.local> Mark "Murch" Erhardt <murch@murch.one> notmike-5 <notmike-5@users.noreply.github.com> Vojtěch Strnad <43024885+vostrnad@users.noreply.github.com> Co-authored-by: Ethan Heilman <ethan.r.heilman@gmail.com> Co-authored-by: Isabel Foxen Duke <110147802+Isabelfoxenduke@users.noreply.github.com>
This commit is contained in:
262
bip-0360/ref-impl/rust/tests/p2mr_construction.rs
Normal file
262
bip-0360/ref-impl/rust/tests/p2mr_construction.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use std::collections::HashSet;
|
||||
use bitcoin::{Network, ScriptBuf};
|
||||
use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash};
|
||||
use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo};
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
use hex;
|
||||
use log::debug;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use p2mr_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn};
|
||||
use p2mr_ref::error::P2MRError;
|
||||
use p2mr_ref::{create_p2mr_utxo, tagged_hash};
|
||||
|
||||
// This file contains tests that execute against the BIP360 script-path-only test vectors.
|
||||
|
||||
static TEST_VECTORS: Lazy<TestVectors> = Lazy::new(|| {
|
||||
let bip360_test_vectors = include_str!("../../common/tests/data/p2mr_construction.json");
|
||||
let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap();
|
||||
assert_eq!(test_vectors.version, 1);
|
||||
test_vectors
|
||||
});
|
||||
|
||||
static P2TR_USING_V2_WITNESS_VERSION_ERROR: &str = "p2tr_using_v2_witness_version_error";
|
||||
static P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2mr_missing_leaf_script_tree_error";
|
||||
static P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2mr_single_leaf_script_tree";
|
||||
static P2MR_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2mr_different_version_leaves";
|
||||
static P2MR_TWO_LEAF_SAME_VERSION_TEST: &str = "p2mr_two_leaf_same_version";
|
||||
static P2MR_THREE_LEAF_COMPLEX_TEST: &str = "p2mr_three_leaf_complex";
|
||||
static P2MR_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2mr_three_leaf_alternative";
|
||||
static P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2mr_simple_lightning_contract";
|
||||
|
||||
#[test]
|
||||
fn test_p2tr_using_v2_witness_version_error() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2TR_USING_V2_WITNESS_VERSION_ERROR).unwrap();
|
||||
let test_result: anyhow::Result<()> = process_test_vector_p2tr(test_vector);
|
||||
assert!(matches!(test_result.unwrap_err().downcast_ref::<P2MRError>(),
|
||||
Some(P2MRError::P2trRequiresWitnessVersion1)));
|
||||
}
|
||||
|
||||
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
|
||||
#[test]
|
||||
fn test_p2mr_missing_leaf_script_tree_error() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap();
|
||||
let test_result: anyhow::Result<()> = process_test_vector_p2mr(test_vector);
|
||||
assert!(matches!(test_result.unwrap_err().downcast_ref::<P2MRError>(),
|
||||
Some(P2MRError::MissingScriptTreeLeaf)));
|
||||
}
|
||||
|
||||
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
|
||||
#[test]
|
||||
fn test_p2mr_single_leaf_script_tree() {
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_different_version_leaves() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_DIFFERENT_VERSION_LEAVES_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_simple_lightning_contract() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_two_leaf_same_version() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_TWO_LEAF_SAME_VERSION_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_three_leaf_complex() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_COMPLEX_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_three_leaf_alternative() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_ALTERNATIVE_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
fn process_test_vector_p2tr(test_vector: &TestVector) -> anyhow::Result<()> {
|
||||
let script_pubkey_hex = test_vector.expected.script_pubkey.as_ref().unwrap();
|
||||
let script_pubkey_bytes = hex::decode(script_pubkey_hex).unwrap();
|
||||
if script_pubkey_bytes[0] != 0x51 {
|
||||
return Err(P2MRError::P2trRequiresWitnessVersion1.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> {
|
||||
|
||||
let tv_script_tree: Option<&TVScriptTree> = test_vector.given.script_tree.as_ref();
|
||||
|
||||
let mut tv_leaf_count: u8 = 0;
|
||||
let mut current_branch_id: u8 = 0;
|
||||
|
||||
// TaprootBuilder expects the addition of each leaf script with its associated depth
|
||||
// It then constructs the binary tree in DFS order, sorting siblings lexicographically & combining them via BIP341's tapbranch_hash
|
||||
// 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();
|
||||
|
||||
// 1) traverse test vector script tree and add leaves to P2MR builder
|
||||
if let Some(script_tree) = tv_script_tree {
|
||||
|
||||
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));
|
||||
|
||||
let mut modified_depth = depth + 1;
|
||||
if direction == Direction::Root {
|
||||
modified_depth = depth;
|
||||
}
|
||||
debug!("traverse_with_depth: leaf_count: {}, depth: {}, modified_depth: {}, direction: {}, tv_leaf_script: {}",
|
||||
tv_leaf_count, depth, modified_depth, direction, tv_leaf.script);
|
||||
|
||||
// NOTE: Some of the the test vectors in this project specify leaves with non-standard versions (ie: 250 / 0xfa)
|
||||
p2mr_builder = p2mr_builder.clone().add_leaf_with_ver(depth, tv_leaf_script_buf.clone(), tv_leaf_version)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Failed to add leaf: {:?}", e);
|
||||
});
|
||||
|
||||
tv_leaf_count += 1;
|
||||
} else if let TVScriptTree::Branch { left, right } = node {
|
||||
// No need to calculate branch hash.
|
||||
// TaprootBuilder does this for us.
|
||||
debug!("branch_count: {}, depth: {}, direction: {}", current_branch_id, depth, direction);
|
||||
current_branch_id += 1;
|
||||
}
|
||||
});
|
||||
}else {
|
||||
return Err(P2MRError::MissingScriptTreeLeaf.into());
|
||||
}
|
||||
|
||||
let spend_info: P2mrSpendInfo = p2mr_builder.clone()
|
||||
.finalize()
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("finalize failed: {:?}", e);
|
||||
});
|
||||
|
||||
let derived_merkle_root: TapNodeHash = spend_info.merkle_root.unwrap();
|
||||
|
||||
// 2) verify derived merkle root against test vector
|
||||
let test_vector_merkle_root = test_vector.intermediary.merkle_root.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
derived_merkle_root.to_string(),
|
||||
*test_vector_merkle_root,
|
||||
"Merkle root mismatch"
|
||||
);
|
||||
debug!("just passed merkle root validation: {}", test_vector_merkle_root);
|
||||
|
||||
let test_vector_leaf_hashes_vec: Vec<String> = test_vector.intermediary.leaf_hashes.clone();
|
||||
let test_vector_leaf_hash_set: HashSet<String> = 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<String> = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect();
|
||||
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 {
|
||||
|
||||
let version = derived_leaf.version();
|
||||
let script = derived_leaf.script();
|
||||
let merkle_branch: &TaprootMerkleBranch = derived_leaf.merkle_branch();
|
||||
|
||||
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 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
|
||||
);
|
||||
debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block);
|
||||
|
||||
}
|
||||
|
||||
let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(derived_merkle_root.to_string());
|
||||
|
||||
assert_eq!(
|
||||
p2mr_utxo_return.script_pubkey_hex,
|
||||
*test_vector.expected.script_pubkey.as_ref().unwrap(),
|
||||
"Script pubkey mismatch"
|
||||
);
|
||||
debug!("just passed script_pubkey validation. script_pubkey = {}", p2mr_utxo_return.script_pubkey_hex);
|
||||
|
||||
let bech32m_address: String = p2mr_utxo_return.bech32m_address;
|
||||
debug!("derived bech32m address for bitcoin_network: {} : {}", p2mr_utxo_return.bitcoin_network, bech32m_address);
|
||||
|
||||
if p2mr_utxo_return.bitcoin_network == Network::Bitcoin {
|
||||
assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
240
bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs
Normal file
240
bip-0360/ref-impl/rust/tests/p2mr_pqc_construction.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use std::collections::HashSet;
|
||||
use bitcoin::{Network, ScriptBuf};
|
||||
use bitcoin::taproot::{LeafVersion, TapTree, ScriptLeaves, TapLeafHash, TaprootMerkleBranch, TapNodeHash};
|
||||
use bitcoin::p2mr::{P2mrBuilder, P2mrControlBlock, P2mrSpendInfo};
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
use hex;
|
||||
use log::debug;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use p2mr_ref::data_structures::{TVScriptTree, TestVector, Direction, TestVectors, UtxoReturn};
|
||||
use p2mr_ref::error::P2MRError;
|
||||
use p2mr_ref::{create_p2mr_utxo, tagged_hash};
|
||||
|
||||
// This file contains tests that execute against the BIP360 script-path-only test vectors.
|
||||
|
||||
static TEST_VECTORS: Lazy<TestVectors> = Lazy::new(|| {
|
||||
let bip360_test_vectors = include_str!("../../common/tests/data/p2mr_pqc_construction.json");
|
||||
let test_vectors: TestVectors = serde_json::from_str(bip360_test_vectors).unwrap();
|
||||
assert_eq!(test_vectors.version, 1);
|
||||
test_vectors
|
||||
});
|
||||
|
||||
static P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST: &str = "p2mr_missing_leaf_script_tree_error";
|
||||
static P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST: &str = "p2mr_single_leaf_script_tree";
|
||||
static P2MR_DIFFERENT_VERSION_LEAVES_TEST: &str = "p2mr_different_version_leaves";
|
||||
static P2MR_TWO_LEAF_SAME_VERSION_TEST: &str = "p2mr_two_leaf_same_version";
|
||||
static P2MR_THREE_LEAF_COMPLEX_TEST: &str = "p2mr_three_leaf_complex";
|
||||
static P2MR_THREE_LEAF_ALTERNATIVE_TEST: &str = "p2mr_three_leaf_alternative";
|
||||
static P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST: &str = "p2mr_simple_lightning_contract";
|
||||
|
||||
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
|
||||
#[test]
|
||||
fn test_p2mr_pqc_missing_leaf_script_tree_error() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_MISSING_LEAF_SCRIPT_TREE_ERROR_TEST).unwrap();
|
||||
let test_result: anyhow::Result<()> = process_test_vector_p2mr(test_vector);
|
||||
assert!(matches!(test_result.unwrap_err().downcast_ref::<P2MRError>(),
|
||||
Some(P2MRError::MissingScriptTreeLeaf)));
|
||||
}
|
||||
|
||||
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
|
||||
#[test]
|
||||
fn test_p2mr_pqc_single_leaf_script_tree() {
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_SINGLE_LEAF_SCRIPT_TREE_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_pqc_different_version_leaves() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_DIFFERENT_VERSION_LEAVES_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_pqc_simple_lightning_contract() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_SIMPLE_LIGHTNING_CONTRACT_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_pqc_two_leaf_same_version() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_TWO_LEAF_SAME_VERSION_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_pqc_three_leaf_complex() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_COMPLEX_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_p2mr_pqc_three_leaf_alternative() {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let test_vectors = &*TEST_VECTORS;
|
||||
let test_vector = test_vectors.test_vector_map.get(P2MR_THREE_LEAF_ALTERNATIVE_TEST).unwrap();
|
||||
process_test_vector_p2mr(test_vector).unwrap();
|
||||
}
|
||||
|
||||
fn process_test_vector_p2mr(test_vector: &TestVector) -> anyhow::Result<()> {
|
||||
|
||||
let tv_script_tree: Option<&TVScriptTree> = test_vector.given.script_tree.as_ref();
|
||||
|
||||
let mut tv_leaf_count: u8 = 0;
|
||||
let mut current_branch_id: u8 = 0;
|
||||
|
||||
// TaprootBuilder expects the addition of each leaf script with its associated depth
|
||||
// It then constructs the binary tree in DFS order, sorting siblings lexicographically & combining them via BIP341's tapbranch_hash
|
||||
// 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();
|
||||
|
||||
// 1) traverse test vector script tree and add leaves to P2MR builder
|
||||
if let Some(script_tree) = tv_script_tree {
|
||||
|
||||
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));
|
||||
|
||||
let mut modified_depth = depth + 1;
|
||||
if direction == Direction::Root {
|
||||
modified_depth = depth;
|
||||
}
|
||||
debug!("traverse_with_depth: leaf_count: {}, depth: {}, modified_depth: {}, direction: {}, tv_leaf_script: {}",
|
||||
tv_leaf_count, depth, modified_depth, direction, tv_leaf.script);
|
||||
|
||||
// NOTE: Some of the the test vectors in this project specify leaves with non-standard versions (ie: 250 / 0xfa)
|
||||
p2mr_builder = p2mr_builder.clone().add_leaf_with_ver(depth, tv_leaf_script_buf.clone(), tv_leaf_version)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Failed to add leaf: {:?}", e);
|
||||
});
|
||||
|
||||
tv_leaf_count += 1;
|
||||
} else if let TVScriptTree::Branch { left, right } = node {
|
||||
// No need to calculate branch hash.
|
||||
// TaprootBuilder does this for us.
|
||||
debug!("branch_count: {}, depth: {}, direction: {}", current_branch_id, depth, direction);
|
||||
current_branch_id += 1;
|
||||
}
|
||||
});
|
||||
}else {
|
||||
return Err(P2MRError::MissingScriptTreeLeaf.into());
|
||||
}
|
||||
|
||||
let spend_info: P2mrSpendInfo = p2mr_builder.clone()
|
||||
.finalize()
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("finalize failed: {:?}", e);
|
||||
});
|
||||
|
||||
let derived_merkle_root: TapNodeHash = spend_info.merkle_root.unwrap();
|
||||
|
||||
// 2) verify derived merkle root against test vector
|
||||
let test_vector_merkle_root = test_vector.intermediary.merkle_root.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
derived_merkle_root.to_string(),
|
||||
*test_vector_merkle_root,
|
||||
"Merkle root mismatch"
|
||||
);
|
||||
debug!("just passed merkle root validation: {}", test_vector_merkle_root);
|
||||
|
||||
let test_vector_leaf_hashes_vec: Vec<String> = test_vector.intermediary.leaf_hashes.clone();
|
||||
let test_vector_leaf_hash_set: HashSet<String> = 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<String> = test_vector_control_blocks_vec.as_ref().unwrap().iter().cloned().collect();
|
||||
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 {
|
||||
|
||||
let version = derived_leaf.version();
|
||||
let script = derived_leaf.script();
|
||||
let merkle_branch: &TaprootMerkleBranch = derived_leaf.merkle_branch();
|
||||
|
||||
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 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
|
||||
);
|
||||
debug!("leaf_hash: {}, derived_serialized_control_block: {}", leaf_hash, derived_serialized_control_block);
|
||||
|
||||
}
|
||||
|
||||
let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(derived_merkle_root.to_string());
|
||||
|
||||
assert_eq!(
|
||||
p2mr_utxo_return.script_pubkey_hex,
|
||||
*test_vector.expected.script_pubkey.as_ref().unwrap(),
|
||||
"Script pubkey mismatch"
|
||||
);
|
||||
debug!("just passed script_pubkey validation. script_pubkey = {}", p2mr_utxo_return.script_pubkey_hex);
|
||||
|
||||
let bech32m_address: String = p2mr_utxo_return.bech32m_address;
|
||||
debug!("derived bech32m address for bitcoin_network: {} : {}", p2mr_utxo_return.bitcoin_network, bech32m_address);
|
||||
|
||||
if p2mr_utxo_return.bitcoin_network == Network::Bitcoin {
|
||||
assert_eq!(bech32m_address, *test_vector.expected.bip350_address.as_ref().unwrap(), "Bech32m address mismatch.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
102
bip-0360/ref-impl/rust/tests/p2mr_spend.rs
Normal file
102
bip-0360/ref-impl/rust/tests/p2mr_spend.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use log::info;
|
||||
use bitcoin::blockdata::witness::Witness;
|
||||
|
||||
use p2mr_ref::{ pay_to_p2wpkh_tx, serialize_script };
|
||||
|
||||
use p2mr_ref::data_structures::{SpendDetails, LeafScriptType};
|
||||
|
||||
/* The rust-bitcoin crate does not provide a single high-level API that builds the full Taproot script-path witness stack for you.
|
||||
It does expose all the necessary types and primitives to build it manually and correctly.
|
||||
*/
|
||||
|
||||
// https://learnmeabitcoin.com/technical/upgrades/taproot/#example-2-script-path-spend-simple
|
||||
#[test]
|
||||
fn test_script_path_spend_simple() {
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let script_inputs_count = hex::decode("03").unwrap();
|
||||
let script_inputs_bytes: Vec<u8> = hex::decode("08").unwrap();
|
||||
let leaf_script_bytes: Vec<u8> = hex::decode("5887").unwrap();
|
||||
let control_block_bytes: Vec<u8> =
|
||||
hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
|
||||
let test_witness_bytes: Vec<u8> = hex::decode(
|
||||
"03010802588721c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut derived_witness: Witness = Witness::new();
|
||||
derived_witness.push(script_inputs_count);
|
||||
derived_witness.push(serialize_script(&script_inputs_bytes));
|
||||
derived_witness.push(serialize_script(&leaf_script_bytes));
|
||||
derived_witness.push(serialize_script(&control_block_bytes));
|
||||
|
||||
info!("witness: {:?}", derived_witness);
|
||||
|
||||
let derived_witness_vec: Vec<u8> = derived_witness.iter().flatten().cloned().collect();
|
||||
|
||||
assert_eq!(derived_witness_vec, test_witness_bytes);
|
||||
}
|
||||
|
||||
|
||||
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
|
||||
// Spends from a p2mr UTXO to a p2wpk UTXO
|
||||
#[test]
|
||||
fn test_script_path_spend_signatures() {
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let funding_tx_id_bytes: Vec<u8> =
|
||||
hex::decode("d1c40446c65456a9b11a9dddede31ee34b8d3df83788d98f690225d2958bfe3c").unwrap();
|
||||
|
||||
// The input index of the funding tx
|
||||
let funding_tx_index: u32 = 0;
|
||||
|
||||
let funding_utxo_amount_sats: u64 = 20000;
|
||||
|
||||
// OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
|
||||
let input_leaf_script_bytes: Vec<u8> =
|
||||
hex::decode("206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0ac").unwrap();
|
||||
|
||||
// Modified from learnmeabitcoin example
|
||||
// Changed from c0 to c1 control byte to reflect p2mr specification: The parity bit of the control byte is always 1 since P2MR does not have a key-spend path.
|
||||
let input_control_block_bytes: Vec<u8> =
|
||||
hex::decode("c1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
|
||||
|
||||
let input_script_pubkey_bytes: Vec<u8> =
|
||||
hex::decode("5120f3778defe5173a9bf7169575116224f961c03c725c0e98b8da8f15df29194b80")
|
||||
.unwrap();
|
||||
let input_script_priv_key_bytes: Vec<u8> = hex::decode("9b8de5d7f20a8ebb026a82babac3aa47a008debbfde5348962b2c46520bd5189").unwrap();
|
||||
|
||||
// Convert to Vec<Vec<u8>> format expected by the function
|
||||
let input_script_priv_keys_bytes: Vec<Vec<u8>> = vec![input_script_priv_key_bytes];
|
||||
|
||||
|
||||
// https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
|
||||
let spend_output_pubkey_bytes: Vec<u8> = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
|
||||
|
||||
let spend_output_amount_sats: u64 = 15000;
|
||||
|
||||
let test_sighash_bytes: Vec<u8> = hex::decode("752453d473e511a0da2097d664d69fe5eb89d8d9d00eab924b42fc0801a980c9").unwrap();
|
||||
let test_signature_bytes: Vec<u8> = hex::decode("01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f").unwrap();
|
||||
|
||||
// Modified from learnmeabitcoin example
|
||||
// Changed from c0 to c1 control byte to reflect p2mr specification: The parity bit of the control byte is always 1 since P2MR does not have a key-spend path.
|
||||
let test_witness_bytes: Vec<u8> = hex::decode("01769105cbcbdcaaee5e58cd201ba3152477fda31410df8b91b4aee2c4864c7700615efb425e002f146a39ca0a4f2924566762d9213bd33f825fad83977fba7f01206d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0acc1924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329").unwrap();
|
||||
|
||||
let result: SpendDetails = pay_to_p2wpkh_tx(funding_tx_id_bytes,
|
||||
funding_tx_index,
|
||||
funding_utxo_amount_sats,
|
||||
input_script_pubkey_bytes,
|
||||
input_control_block_bytes,
|
||||
input_leaf_script_bytes,
|
||||
input_script_priv_keys_bytes, // Now passing Vec<Vec<u8>> format
|
||||
spend_output_pubkey_bytes,
|
||||
spend_output_amount_sats,
|
||||
LeafScriptType::SchnorrOnly // This test uses a Schnorr signature
|
||||
);
|
||||
|
||||
assert_eq!(result.sighash.as_slice(), test_sighash_bytes.as_slice(), "sighash mismatch");
|
||||
assert_eq!(result.sig_bytes, test_signature_bytes, "signature mismatch");
|
||||
assert_eq!(result.derived_witness_vec, test_witness_bytes, "derived_witness mismatch");
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user