mirror of
https://github.com/bitcoin/bips.git
synced 2026-03-16 15:55:37 +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:
30
bip-0360/ref-impl/rust/examples/p2mr-end-to-end.sh
Normal file
30
bip-0360/ref-impl/rust/examples/p2mr-end-to-end.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
export BITCOIN_SOURCE_DIR=$HOME/bitcoin
|
||||
export W_NAME=anduro
|
||||
export USE_PQC=false
|
||||
export TOTAL_LEAF_COUNT=5
|
||||
export LEAF_TO_SPEND_FROM=4
|
||||
|
||||
b-cli -named createwallet \
|
||||
wallet_name=$W_NAME \
|
||||
descriptors=true \
|
||||
load_on_startup=true
|
||||
|
||||
export BITCOIN_ADDRESS_INFO=$( cargo run --example p2mr_construction ) \
|
||||
&& echo $BITCOIN_ADDRESS_INFO | jq -r .
|
||||
|
||||
export QUANTUM_ROOT=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.tree_root_hex' ) \
|
||||
&& export LEAF_SCRIPT_PRIV_KEY_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_priv_key_hex' ) \
|
||||
&& export LEAF_SCRIPT_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.leaf_script_hex' ) \
|
||||
&& export CONTROL_BLOCK_HEX=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.taptree_return.control_block_hex' ) \
|
||||
&& export FUNDING_SCRIPT_PUBKEY=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.script_pubkey_hex' ) \
|
||||
&& export P2MR_ADDR=$( echo $BITCOIN_ADDRESS_INFO | jq -r '.utxo_return.bech32m_address' )
|
||||
|
||||
b-cli decodescript $LEAF_SCRIPT_HEX | jq -r '.asm'
|
||||
|
||||
export COINBASE_REWARD_TX_ID=$( b-cli -named generatetoaddress 1 $P2MR_ADDR 5 | jq -r '.[]' ) \
|
||||
&& echo $COINBASE_REWARD_TX_ID
|
||||
|
||||
export P2MR_DESC=$( b-cli getdescriptorinfo "addr($P2MR_ADDR)" | jq -r '.descriptor' ) \
|
||||
&& echo $P2MR_DESC \
|
||||
&& b-cli scantxoutset start '[{"desc": "'''$P2MR_DESC'''"}]'
|
||||
27
bip-0360/ref-impl/rust/examples/p2mr_construction.rs
Normal file
27
bip-0360/ref-impl/rust/examples/p2mr_construction.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use p2mr_ref::{create_p2mr_utxo, create_p2mr_multi_leaf_taptree, tap_tree_lock_type};
|
||||
use p2mr_ref::data_structures::{UtxoReturn, TaptreeReturn, ConstructionReturn, LeafScriptType};
|
||||
use std::env;
|
||||
use log::{info, error};
|
||||
|
||||
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
|
||||
fn main() -> ConstructionReturn {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let tap_tree_lock_type = tap_tree_lock_type();
|
||||
info!("tap_tree_lock_type: {:?}", tap_tree_lock_type);
|
||||
|
||||
let taptree_return: TaptreeReturn = create_p2mr_multi_leaf_taptree();
|
||||
let p2mr_utxo_return: UtxoReturn = create_p2mr_utxo(taptree_return.clone().tree_root_hex);
|
||||
|
||||
// Alert user about SPENDING_LEAF_TYPE requirement when using MIXED mode
|
||||
if tap_tree_lock_type == LeafScriptType::Mixed {
|
||||
info!("NOTE: TAP_TREE_LOCK_TYPE=MIXED requires setting SPENDING_LEAF_TYPE when spending (based on leaf_script_type in output above) as follows:");
|
||||
info!(" export SPENDING_LEAF_TYPE={}", taptree_return.leaf_script_type);
|
||||
}
|
||||
|
||||
return ConstructionReturn {
|
||||
taptree_return: taptree_return,
|
||||
utxo_return: p2mr_utxo_return,
|
||||
};
|
||||
}
|
||||
284
bip-0360/ref-impl/rust/examples/p2mr_spend.rs
Normal file
284
bip-0360/ref-impl/rust/examples/p2mr_spend.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use p2mr_ref::{ pay_to_p2wpkh_tx, verify_schnorr_signature_via_bytes, verify_slh_dsa_via_bytes, tap_tree_lock_type };
|
||||
|
||||
use p2mr_ref::data_structures::{SpendDetails, LeafScriptType};
|
||||
use std::env;
|
||||
use log::{info, error};
|
||||
|
||||
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
|
||||
fn main() -> SpendDetails {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
// FUNDING_TX_ID environment variable is required
|
||||
let funding_tx_id: String = env::var("FUNDING_TX_ID")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_TX_ID environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let funding_tx_id_bytes: Vec<u8> = hex::decode(funding_tx_id.clone()).unwrap();
|
||||
|
||||
// FUNDING_UTXO_AMOUNT_SATS environment variable is required
|
||||
let funding_utxo_amount_sats: u64 = env::var("FUNDING_UTXO_AMOUNT_SATS")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_UTXO_AMOUNT_SATS environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.parse::<u64>()
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_UTXO_AMOUNT_SATS must be a valid u64 integer");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// The input index of the funding tx
|
||||
// Allow override via FUNDING_UTXO_INDEX environment variable
|
||||
let funding_utxo_index: u32 = env::var("FUNDING_UTXO_INDEX")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
info!("Funding tx id: {}, utxo index: {}", funding_tx_id, funding_utxo_index);
|
||||
|
||||
// FUNDING_SCRIPT_PUBKEY environment variable is required
|
||||
let funding_script_pubkey_bytes: Vec<u8> = env::var("FUNDING_SCRIPT_PUBKEY")
|
||||
.map(|s| hex::decode(s).unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_SCRIPT_PUBKEY environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let control_block_bytes: Vec<u8> = env::var("CONTROL_BLOCK_HEX")
|
||||
.map(|s| hex::decode(s).unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
error!("CONTROL_BLOCK_HEX environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
info!("P2MR control block size: {}", control_block_bytes.len());
|
||||
|
||||
// TAP_TREE_LOCK_TYPE environment variable is required to determine key structure
|
||||
let leaf_script_type: LeafScriptType = tap_tree_lock_type();
|
||||
info!("leaf_script_type: {:?}", leaf_script_type);
|
||||
|
||||
// For Mixed trees, we need to determine the actual leaf type via SPENDING_LEAF_TYPE
|
||||
let effective_leaf_type: LeafScriptType = if leaf_script_type == LeafScriptType::Mixed {
|
||||
match env::var("SPENDING_LEAF_TYPE") {
|
||||
Ok(value) => match value.as_str() {
|
||||
"SCHNORR_ONLY" => {
|
||||
info!("SPENDING_LEAF_TYPE: SCHNORR_ONLY");
|
||||
LeafScriptType::SchnorrOnly
|
||||
},
|
||||
"SLH_DSA_ONLY" => {
|
||||
info!("SPENDING_LEAF_TYPE: SLH_DSA_ONLY");
|
||||
LeafScriptType::SlhDsaOnly
|
||||
},
|
||||
_ => {
|
||||
error!("Invalid SPENDING_LEAF_TYPE '{}'. Must be SCHNORR_ONLY or SLH_DSA_ONLY", value);
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
error!("SPENDING_LEAF_TYPE environment variable is required when TAP_TREE_LOCK_TYPE=MIXED");
|
||||
error!("Set SPENDING_LEAF_TYPE to the actual type of the leaf being spent (SCHNORR_ONLY or SLH_DSA_ONLY).");
|
||||
error!("The leaf type is returned in the 'leaf_script_type' field of the tree construction output.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leaf_script_type
|
||||
};
|
||||
|
||||
// Parse private keys based on effective script type
|
||||
let leaf_script_priv_keys_bytes: Vec<Vec<u8>> = match effective_leaf_type {
|
||||
LeafScriptType::SlhDsaOnly => {
|
||||
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SLH_DSA_ONLY");
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Parse JSON array and extract the first (and only) hex string
|
||||
let priv_keys_hex: String = serde_json::from_str::<Vec<String>>(&priv_keys_hex_array)
|
||||
.unwrap_or_else(|_| {
|
||||
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let priv_keys_bytes = hex::decode(priv_keys_hex).unwrap();
|
||||
if priv_keys_bytes.len() != 64 {
|
||||
error!("SLH-DSA private key must be 64 bytes, got {}", priv_keys_bytes.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
vec![priv_keys_bytes]
|
||||
},
|
||||
LeafScriptType::SchnorrOnly => {
|
||||
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SCHNORR_ONLY");
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Parse JSON array and extract the first (and only) hex string
|
||||
let priv_keys_hex: String = serde_json::from_str::<Vec<String>>(&priv_keys_hex_array)
|
||||
.unwrap_or_else(|_| {
|
||||
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let priv_keys_bytes = hex::decode(priv_keys_hex).unwrap();
|
||||
if priv_keys_bytes.len() != 32 {
|
||||
error!("Schnorr private key must be 32 bytes, got {}", priv_keys_bytes.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
vec![priv_keys_bytes]
|
||||
},
|
||||
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
|
||||
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required for SCHNORR_AND_SLH_DSA");
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Parse JSON array and extract the hex strings
|
||||
let priv_keys_hex_vec: Vec<String> = serde_json::from_str(&priv_keys_hex_array)
|
||||
.unwrap_or_else(|_| {
|
||||
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if priv_keys_hex_vec.len() != 2 {
|
||||
error!("For SCHNORR_AND_SLH_DSA, LEAF_SCRIPT_PRIV_KEYS_HEX must contain exactly 2 hex strings, got {}", priv_keys_hex_vec.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let schnorr_priv_key_hex = &priv_keys_hex_vec[0];
|
||||
let slh_dsa_priv_key_hex = &priv_keys_hex_vec[1];
|
||||
|
||||
let schnorr_priv_key_bytes = hex::decode(schnorr_priv_key_hex).unwrap();
|
||||
let slh_dsa_priv_key_bytes = hex::decode(slh_dsa_priv_key_hex).unwrap();
|
||||
|
||||
if schnorr_priv_key_bytes.len() != 32 {
|
||||
error!("Schnorr private key must be 32 bytes, got {}", schnorr_priv_key_bytes.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
if slh_dsa_priv_key_bytes.len() != 64 {
|
||||
error!("SLH-DSA private key must be 64 bytes, got {}", slh_dsa_priv_key_bytes.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
vec![schnorr_priv_key_bytes, slh_dsa_priv_key_bytes]
|
||||
},
|
||||
LeafScriptType::Mixed => {
|
||||
// This case should never be reached because Mixed is resolved to effective_leaf_type above
|
||||
unreachable!("Mixed should have been resolved to effective_leaf_type");
|
||||
},
|
||||
LeafScriptType::NotApplicable => {
|
||||
panic!("LeafScriptType::NotApplicable is not applicable");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
|
||||
let leaf_script_bytes: Vec<u8> = env::var("LEAF_SCRIPT_HEX")
|
||||
.map(|s| hex::decode(s).unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
error!("LEAF_SCRIPT_HEX environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
|
||||
let spend_output_pubkey_hash_bytes: Vec<u8> = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
|
||||
|
||||
// OUTPUT_AMOUNT_SATS env var is optional. Default is FUNDING_UTXO_AMOUNT_SATS - 5000 sats
|
||||
let spend_output_amount_sats: u64 = env::var("OUTPUT_AMOUNT_SATS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(funding_utxo_amount_sats.saturating_sub(5000));
|
||||
|
||||
|
||||
let result: SpendDetails = pay_to_p2wpkh_tx(
|
||||
funding_tx_id_bytes,
|
||||
funding_utxo_index,
|
||||
funding_utxo_amount_sats,
|
||||
funding_script_pubkey_bytes,
|
||||
control_block_bytes,
|
||||
leaf_script_bytes.clone(),
|
||||
leaf_script_priv_keys_bytes, // Now passing Vec<Vec<u8>> instead of Vec<u8>
|
||||
spend_output_pubkey_hash_bytes,
|
||||
spend_output_amount_sats,
|
||||
effective_leaf_type // Use effective type (resolved from SPENDING_LEAF_TYPE if Mixed)
|
||||
);
|
||||
|
||||
// Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes
|
||||
let tapleaf_pubkey_bytes: Vec<u8> = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec();
|
||||
|
||||
match effective_leaf_type {
|
||||
LeafScriptType::SlhDsaOnly => {
|
||||
let is_valid: bool = verify_slh_dsa_via_bytes(&result.sig_bytes, &result.sighash, &tapleaf_pubkey_bytes);
|
||||
info!("is_valid: {}", is_valid);
|
||||
},
|
||||
LeafScriptType::SchnorrOnly => {
|
||||
let is_valid: bool = verify_schnorr_signature_via_bytes(
|
||||
&result.sig_bytes,
|
||||
&result.sighash,
|
||||
&tapleaf_pubkey_bytes);
|
||||
info!("is_valid: {}", is_valid);
|
||||
},
|
||||
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
|
||||
// For combined scripts, we need to separate the signatures
|
||||
// The sig_bytes contains: [schnorr_sig (64 bytes), slh_dsa_sig (7856 bytes)] (raw signatures without sighash)
|
||||
let schnorr_sig_len = 64; // Schnorr signature is 64 bytes
|
||||
let slh_dsa_sig_len = 7856; // SLH-DSA signature is 7856 bytes
|
||||
|
||||
let expected_min_len = schnorr_sig_len + slh_dsa_sig_len;
|
||||
|
||||
if result.sig_bytes.len() < expected_min_len {
|
||||
error!("Combined signature length is too short: expected at least {}, got {}",
|
||||
expected_min_len, result.sig_bytes.len());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extract Schnorr signature (first 64 bytes)
|
||||
let schnorr_sig = &result.sig_bytes[..schnorr_sig_len];
|
||||
// Extract SLH-DSA signature (next 7856 bytes)
|
||||
let slh_dsa_sig = &result.sig_bytes[schnorr_sig_len..schnorr_sig_len + slh_dsa_sig_len];
|
||||
|
||||
// For SCHNORR_AND_SLH_DSA scripts, we need to extract the individual public keys
|
||||
// The script structure is: OP_PUSHBYTES_32 <schnorr_pubkey(32)> OP_CHECKSIG OP_PUSHBYTES_32 <slh_dsa_pubkey(32)> OP_SUBSTR OP_BOOLAND OP_VERIFY
|
||||
// So we need to extract the Schnorr pubkey (first 32 bytes after OP_PUSHBYTES_32)
|
||||
let schnorr_pubkey_bytes = &leaf_script_bytes[1..33]; // Skip OP_PUSHBYTES_32 (0x20), get next 32 bytes
|
||||
let slh_dsa_pubkey_bytes = &leaf_script_bytes[35..67]; // Skip OP_CHECKSIG (0xac), OP_PUSHBYTES_32 (0x20), get next 32 bytes
|
||||
|
||||
// Verify Schnorr signature
|
||||
let schnorr_is_valid: bool = verify_schnorr_signature_via_bytes(
|
||||
schnorr_sig,
|
||||
&result.sighash,
|
||||
schnorr_pubkey_bytes);
|
||||
info!("Schnorr signature is_valid: {}", schnorr_is_valid);
|
||||
|
||||
// Verify SLH-DSA signature
|
||||
let slh_dsa_is_valid: bool = verify_slh_dsa_via_bytes(
|
||||
slh_dsa_sig,
|
||||
&result.sighash,
|
||||
slh_dsa_pubkey_bytes);
|
||||
info!("SLH-DSA signature is_valid: {}", slh_dsa_is_valid);
|
||||
|
||||
let both_valid = schnorr_is_valid && slh_dsa_is_valid;
|
||||
info!("Both signatures valid: {}", both_valid);
|
||||
}
|
||||
LeafScriptType::Mixed => {
|
||||
// This case should never be reached because Mixed is resolved to effective_leaf_type above
|
||||
unreachable!("Mixed should have been resolved to effective_leaf_type");
|
||||
}
|
||||
LeafScriptType::NotApplicable => {
|
||||
panic!("LeafScriptType::NotApplicable is not applicable");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
17
bip-0360/ref-impl/rust/examples/p2tr_construction.rs
Normal file
17
bip-0360/ref-impl/rust/examples/p2tr_construction.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use p2mr_ref::{create_p2tr_utxo, create_p2tr_multi_leaf_taptree};
|
||||
use p2mr_ref::data_structures::{UtxoReturn, TaptreeReturn, ConstructionReturn};
|
||||
|
||||
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
|
||||
fn main() -> ConstructionReturn {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
let internal_pubkey_hex = "924c163b385af7093440184af6fd6244936d1288cbb41cc3812286d3f83a3329".to_string();
|
||||
|
||||
let taptree_return: TaptreeReturn = create_p2tr_multi_leaf_taptree(internal_pubkey_hex.clone());
|
||||
let utxo_return: UtxoReturn = create_p2tr_utxo(taptree_return.clone().tree_root_hex, internal_pubkey_hex);
|
||||
return ConstructionReturn {
|
||||
taptree_return: taptree_return,
|
||||
utxo_return: utxo_return,
|
||||
};
|
||||
}
|
||||
129
bip-0360/ref-impl/rust/examples/p2tr_spend.rs
Normal file
129
bip-0360/ref-impl/rust/examples/p2tr_spend.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use p2mr_ref::{ pay_to_p2wpkh_tx , verify_schnorr_signature_via_bytes};
|
||||
|
||||
use p2mr_ref::data_structures::{SpendDetails, LeafScriptType};
|
||||
use std::env;
|
||||
use log::{info, error};
|
||||
|
||||
// Inspired by: https://learnmeabitcoin.com/technical/upgrades/taproot/#example-3-script-path-spend-signature
|
||||
fn main() -> SpendDetails {
|
||||
|
||||
let _ = env_logger::try_init(); // Use try_init to avoid reinitialization error
|
||||
|
||||
// FUNDING_TX_ID environment variable is required
|
||||
let funding_tx_id: String = env::var("FUNDING_TX_ID")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_TX_ID environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let funding_tx_id_bytes: Vec<u8> = hex::decode(funding_tx_id.clone()).unwrap();
|
||||
|
||||
// FUNDING_UTXO_AMOUNT_SATS environment variable is required
|
||||
let funding_utxo_amount_sats: u64 = env::var("FUNDING_UTXO_AMOUNT_SATS")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_UTXO_AMOUNT_SATS environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.parse::<u64>()
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_UTXO_AMOUNT_SATS must be a valid u64 integer");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// The input index of the funding tx
|
||||
// Allow override via FUNDING_UTXO_INDEX environment variable
|
||||
let funding_utxo_index: u32 = env::var("FUNDING_UTXO_INDEX")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
info!("Funding tx id: {}, utxo index: {}", funding_tx_id, funding_utxo_index);
|
||||
|
||||
// FUNDING_SCRIPT_PUBKEY environment variable is required
|
||||
let funding_script_pubkey_bytes: Vec<u8> = env::var("FUNDING_SCRIPT_PUBKEY")
|
||||
.map(|s| hex::decode(s).unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
error!("FUNDING_SCRIPT_PUBKEY environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let control_block_bytes: Vec<u8> = env::var("CONTROL_BLOCK_HEX")
|
||||
.map(|s| hex::decode(s).unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
error!("CONTROL_BLOCK_HEX environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
info!("P2TR control block size: {}", control_block_bytes.len());
|
||||
|
||||
// P2TR only supports Schnorr signatures, so we only need one private key
|
||||
let leaf_script_priv_key_bytes: Vec<u8> = {
|
||||
let priv_keys_hex_array = env::var("LEAF_SCRIPT_PRIV_KEYS_HEX")
|
||||
.unwrap_or_else(|_| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Parse JSON array and extract the first (and only) hex string
|
||||
let priv_keys_hex: String = serde_json::from_str::<Vec<String>>(&priv_keys_hex_array)
|
||||
.unwrap_or_else(|_| {
|
||||
error!("Failed to parse LEAF_SCRIPT_PRIV_KEYS_HEX as JSON array");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| {
|
||||
error!("LEAF_SCRIPT_PRIV_KEYS_HEX array is empty");
|
||||
std::process::exit(1);
|
||||
});
|
||||
hex::decode(priv_keys_hex).unwrap()
|
||||
};
|
||||
|
||||
// Validate that the private key is 32 bytes (Schnorr key size)
|
||||
if leaf_script_priv_key_bytes.len() != 32 {
|
||||
error!("P2TR private key must be 32 bytes (Schnorr), got {}", leaf_script_priv_key_bytes.len());
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Convert to Vec<Vec<u8>> format expected by the function
|
||||
let leaf_script_priv_keys_bytes: Vec<Vec<u8>> = vec![leaf_script_priv_key_bytes];
|
||||
|
||||
// ie: OP_PUSHBYTES_32 6d4ddc0e47d2e8f82cbe2fc2d0d749e7bd3338112cecdc76d8f831ae6620dbe0 OP_CHECKSIG
|
||||
let leaf_script_bytes: Vec<u8> = env::var("LEAF_SCRIPT_HEX")
|
||||
.map(|s| hex::decode(s).unwrap())
|
||||
.unwrap_or_else(|_| {
|
||||
error!("LEAF_SCRIPT_HEX environment variable is required but not set");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// https://learnmeabitcoin.com/explorer/tx/797505b104b5fb840931c115ea35d445eb1f64c9279bf23aa5bb4c3d779da0c2#outputs
|
||||
let spend_output_pubkey_hash_bytes: Vec<u8> = hex::decode("0de745dc58d8e62e6f47bde30cd5804a82016f9e").unwrap();
|
||||
|
||||
// OUTPUT_AMOUNT_SATS env var is optional. Default is FUNDING_UTXO_AMOUNT_SATS - 5000 sats
|
||||
let spend_output_amount_sats: u64 = env::var("OUTPUT_AMOUNT_SATS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(funding_utxo_amount_sats.saturating_sub(5000));
|
||||
|
||||
|
||||
let result: SpendDetails = pay_to_p2wpkh_tx(
|
||||
funding_tx_id_bytes,
|
||||
funding_utxo_index,
|
||||
funding_utxo_amount_sats,
|
||||
funding_script_pubkey_bytes,
|
||||
control_block_bytes,
|
||||
leaf_script_bytes.clone(),
|
||||
leaf_script_priv_keys_bytes, // Now passing Vec<Vec<u8>> format
|
||||
spend_output_pubkey_hash_bytes.clone(),
|
||||
spend_output_amount_sats,
|
||||
LeafScriptType::SchnorrOnly
|
||||
);
|
||||
|
||||
// Remove first and last byte from leaf_script_bytes to get tapleaf_pubkey_bytes
|
||||
let tapleaf_pubkey_bytes: Vec<u8> = leaf_script_bytes[1..leaf_script_bytes.len()-1].to_vec();
|
||||
|
||||
let is_valid: bool = verify_schnorr_signature_via_bytes(
|
||||
&result.sig_bytes,
|
||||
&result.sighash,
|
||||
&tapleaf_pubkey_bytes);
|
||||
info!("is_valid: {}", is_valid);
|
||||
|
||||
return result;
|
||||
}
|
||||
69
bip-0360/ref-impl/rust/examples/schnorr_example.rs
Normal file
69
bip-0360/ref-impl/rust/examples/schnorr_example.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::env;
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use bitcoin::key::{Secp256k1};
|
||||
use bitcoin::hashes::{sha256::Hash, Hash as HashTrait};
|
||||
use bitcoin::secp256k1::{Message};
|
||||
|
||||
use p2mr_ref::{ acquire_schnorr_keypair, verify_schnorr_signature };
|
||||
|
||||
/* Secp256k1 implements the Signing trait when it's initialized in signing mode.
|
||||
It's important to note that Secp256k1 has different capabilities depending on how it's constructed:
|
||||
* Secp256k1::new() creates a context capable of both signing and verification
|
||||
* Secp256k1::signing_only() creates a context that can only sign
|
||||
* Secp256k1::verification_only() creates a context that can only verify
|
||||
*/
|
||||
static SECP: Lazy<Secp256k1<bitcoin::secp256k1::All>> = Lazy::new(Secp256k1::new);
|
||||
|
||||
fn main() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
// acquire a schnorr keypair (leveraging OS provided random number generator)
|
||||
let keypair = acquire_schnorr_keypair();
|
||||
let (secret_key, public_key) = keypair.as_schnorr().unwrap();
|
||||
let message_bytes = b"hello";
|
||||
|
||||
// secp256k1 operates on a 256-bit (32-byte) field, so inputs must be exactly this size
|
||||
// subsequently, Schnorr signatures on secp256k1 require exactly a 32-byte input (the curve's scalar field size)
|
||||
let message_hash: Hash = Hash::hash(message_bytes);
|
||||
|
||||
let message: Message = Message::from_digest_slice(&message_hash.to_byte_array()).unwrap();
|
||||
|
||||
|
||||
/* The secp256k1 library internally generates a random scalar value (aka: nonce or k-value) for each signature
|
||||
* Every signature is unique - even if you sign the same message with the same private key multiple times
|
||||
* The randomness is handled automatically by the secp256k1 implementation
|
||||
* You get different signatures each time for the same inputs
|
||||
* The nonce is only needed during signing, not during verification
|
||||
|
||||
Schnorr signatures require randomness for security reasons:
|
||||
* Prevents private key recovery - If the same nonce is used twice, an attacker could potentially derive your private key
|
||||
* Ensures signature uniqueness - Each signature should be cryptographically distinct
|
||||
* Protects against replay attacks - Different signatures for the same data
|
||||
*/
|
||||
let signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr(&message, &secret_key.keypair(&SECP));
|
||||
info!("Signature created successfully, size: {}", signature.serialize().len());
|
||||
|
||||
//let pubkey = public_key;
|
||||
|
||||
|
||||
/*
|
||||
* The nonce provides security during signing (prevents private key recovery)
|
||||
* The nonce is mathematically eliminated during verification
|
||||
* The verifier only needs public information (signature, message, public key)
|
||||
*/
|
||||
let schnorr_valid = verify_schnorr_signature(signature, message, *public_key);
|
||||
info!("schnorr_valid: {}", schnorr_valid);
|
||||
|
||||
|
||||
let aux_rand = [0u8; 32]; // 32 zero bytes; fine for testing
|
||||
let signature_aux_rand: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand(
|
||||
&message,
|
||||
&secret_key.keypair(&SECP),
|
||||
&aux_rand
|
||||
);
|
||||
info!("aux_rand signature created successfully, size: {}", signature_aux_rand.serialize().len());
|
||||
|
||||
let schnorr_valid_aux_rand = verify_schnorr_signature(signature_aux_rand, message, *public_key);
|
||||
info!("schnorr_valid_aux_rand: {}", schnorr_valid_aux_rand);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use std::env;
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use bitcoin::hashes::{sha256::Hash, Hash as HashTrait};
|
||||
use rand::{rng, RngCore};
|
||||
|
||||
use bitcoinpqc::{
|
||||
generate_keypair, public_key_size, secret_key_size, sign, signature_size, verify, Algorithm, KeyPair,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
/*
|
||||
In SPHINCS+ (underlying algorithm of SLH-DSA), the random data is used to:
|
||||
* Initialize hash function parameters within the key generation
|
||||
* Seed the Merkle tree construction that forms the public key
|
||||
* Generate the secret key components that enable signing
|
||||
*/
|
||||
let random_data = get_random_bytes(128);
|
||||
println!("Generated random data of size {}", random_data.len());
|
||||
|
||||
let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data)
|
||||
.expect("Failed to generate SLH-DSA-128S keypair");
|
||||
|
||||
let message_bytes = b"SLH-DSA-128S Test Message";
|
||||
|
||||
println!("Message to sign: {message_bytes:?}");
|
||||
|
||||
/* No need to hash the message
|
||||
1. Variable Input Size: SPHINCS+ can handle messages of arbitrary length directly
|
||||
2. Internal Hashing: The SPHINCS+ algorithm internally handles message processing and hashing as part of its design
|
||||
3. Hash-Based Design: SPHINCS+ is built on hash functions and Merkle trees, so it's designed to work with variable-length inputs
|
||||
4. No Curve Constraints: Unlike elliptic curve schemes, SPHINCS+ doesn't have fixed field size requirements
|
||||
|
||||
SLH-DSA doesn't use nonces like Schnorr does.
|
||||
With SLH-DSA, randomness is built into the key generation process only ( and not the signing process; ie: SECP256K1)
|
||||
Thus, no need for aux_rand data fed to the signature function.
|
||||
The signing algorithm is deterministic and doesn't require random input during signing.
|
||||
*/
|
||||
|
||||
let signature = sign(&keypair.secret_key, message_bytes).expect("Failed to sign with SLH-DSA-128S");
|
||||
|
||||
println!(
|
||||
"Signature created successfully, size: {}",
|
||||
signature.bytes.len()
|
||||
);
|
||||
println!(
|
||||
"Signature prefix: {:02x?}",
|
||||
&signature.bytes[..8.min(signature.bytes.len())]
|
||||
);
|
||||
|
||||
// Verify the signature
|
||||
println!("Verifying signature...");
|
||||
let result = verify(&keypair.public_key, message_bytes, &signature);
|
||||
println!("Verification result: {result:?}");
|
||||
|
||||
assert!(result.is_ok(), "SLH-DSA-128S signature verification failed");
|
||||
|
||||
// Try to verify with a modified message - should fail
|
||||
let modified_message = b"SLH-DSA-128S Modified Message";
|
||||
println!("Modified message: {modified_message:?}");
|
||||
|
||||
let result = verify(&keypair.public_key, modified_message, &signature);
|
||||
println!("Verification with modified message result: {result:?}");
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"SLH-DSA-128S verification should fail with modified message"
|
||||
);
|
||||
}
|
||||
|
||||
fn get_random_bytes(size: usize) -> Vec<u8> {
|
||||
let mut bytes = vec![0u8; size];
|
||||
rng().fill_bytes(&mut bytes);
|
||||
bytes
|
||||
}
|
||||
Reference in New Issue
Block a user