mirror of
https://github.com/bitcoin/bips.git
synced 2026-03-09 15:53:54 +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:
33
bip-0360/ref-impl/rust/src/bin/slh_dsa_key_gen.rs
Normal file
33
bip-0360/ref-impl/rust/src/bin/slh_dsa_key_gen.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::env;
|
||||
use log::info;
|
||||
use rand::{rng, RngCore};
|
||||
|
||||
use bitcoinpqc::{
|
||||
generate_keypair, public_key_size, secret_key_size, 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");
|
||||
|
||||
info!("public key size / value = {}, {}", public_key_size(Algorithm::SLH_DSA_128S), hex::encode(&keypair.public_key.bytes));
|
||||
info!("private key size / value = {}, {}", secret_key_size(Algorithm::SLH_DSA_128S), hex::encode(&keypair.secret_key.bytes));
|
||||
|
||||
}
|
||||
|
||||
fn get_random_bytes(size: usize) -> Vec<u8> {
|
||||
let mut bytes = vec![0u8; size];
|
||||
rng().fill_bytes(&mut bytes);
|
||||
bytes
|
||||
}
|
||||
595
bip-0360/ref-impl/rust/src/data_structures.rs
Normal file
595
bip-0360/ref-impl/rust/src/data_structures.rs
Normal file
@@ -0,0 +1,595 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use log::debug;
|
||||
|
||||
// Add imports for the unified keypair
|
||||
use bitcoin::secp256k1::{SecretKey, XOnlyPublicKey};
|
||||
use bitcoinpqc::{KeyPair, Algorithm};
|
||||
|
||||
/// Enum representing the type of leaf script to create
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LeafScriptType {
|
||||
/// Script requires only SLH-DSA signature
|
||||
SlhDsaOnly,
|
||||
/// Script requires only Schnorr signature
|
||||
SchnorrOnly,
|
||||
/// Script requires both Schnorr and SLH-DSA signatures (in that order)
|
||||
ConcatenatedSchnorrAndSlhDsaSameLeaf,
|
||||
/// Leaves of TapTree are mixed. Some leaves are locked using Schnorr and others are locked using SLH-DSA
|
||||
Mixed,
|
||||
/// Script type is not applicable
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
impl LeafScriptType {
|
||||
/// Check if this script type uses SLH-DSA
|
||||
pub fn uses_slh_dsa(&self) -> bool {
|
||||
matches!(self, LeafScriptType::SlhDsaOnly | LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf)
|
||||
}
|
||||
|
||||
/// Check if this script type uses Schnorr
|
||||
pub fn uses_schnorr(&self) -> bool {
|
||||
matches!(self, LeafScriptType::SchnorrOnly | LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf)
|
||||
}
|
||||
|
||||
/// Check if this script type requires both signature types
|
||||
pub fn requires_both(&self) -> bool {
|
||||
matches!(self, LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf)
|
||||
}
|
||||
|
||||
/// Check if TapTree uses Schnorr for some leaves and SLH-DSA for others
|
||||
pub fn uses_mixed(&self) -> bool {
|
||||
matches!(self, LeafScriptType::Mixed)
|
||||
}
|
||||
|
||||
/// Check if this script type is not applicable
|
||||
pub fn is_not_applicable(&self) -> bool {
|
||||
matches!(self, LeafScriptType::NotApplicable)
|
||||
}
|
||||
|
||||
/// Convert to string representation for serialization
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
LeafScriptType::SlhDsaOnly => "SLH_DSA_ONLY".to_string(),
|
||||
LeafScriptType::SchnorrOnly => "SCHNORR_ONLY".to_string(),
|
||||
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => "CONCATENATED_SCHNORR_AND_SLH_DSA".to_string(),
|
||||
LeafScriptType::Mixed => "MIXED".to_string(),
|
||||
LeafScriptType::NotApplicable => "NOT_APPLICABLE".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse from string representation
|
||||
pub fn from_string(s: &str) -> Self {
|
||||
match s {
|
||||
"SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly,
|
||||
"SCHNORR_ONLY" => LeafScriptType::SchnorrOnly,
|
||||
"CONCATENATED_SCHNORR_AND_SLH_DSA" => LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf,
|
||||
"MIXED" => LeafScriptType::Mixed,
|
||||
_ => LeafScriptType::NotApplicable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TestVectors {
|
||||
pub version: u32,
|
||||
#[serde(rename = "test_vectors")]
|
||||
pub test_vectors: Vec<TestVector>,
|
||||
#[serde(skip, default = "HashMap::new")]
|
||||
pub test_vector_map: HashMap<String, TestVector>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TestVectors {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper {
|
||||
version: u32,
|
||||
#[serde(rename = "test_vectors")]
|
||||
test_vectors: Vec<TestVector>,
|
||||
}
|
||||
|
||||
let helper = Helper::deserialize(deserializer)?;
|
||||
|
||||
let mut test_vector_map = HashMap::new();
|
||||
for test in helper.test_vectors.iter() {
|
||||
test_vector_map.insert(test.id.clone(), test.clone());
|
||||
}
|
||||
|
||||
Ok(TestVectors {
|
||||
version: helper.version,
|
||||
test_vectors: helper.test_vectors,
|
||||
test_vector_map,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TestVector {
|
||||
pub id: String,
|
||||
pub objective: String,
|
||||
pub given: TestVectorGiven,
|
||||
pub intermediary: TestVectorIntermediary,
|
||||
pub expected: TestVectorExpected,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TestVectorGiven {
|
||||
|
||||
#[serde(rename = "internalPubkey")]
|
||||
pub internal_pubkey: Option<String>,
|
||||
|
||||
#[serde(rename = "scriptTree")]
|
||||
pub script_tree: Option<TVScriptTree>,
|
||||
|
||||
#[serde(rename = "scriptInputs")]
|
||||
pub script_inputs: Option<Vec<String>>,
|
||||
#[serde(rename = "scriptHex")]
|
||||
pub script_hex: Option<String>,
|
||||
#[serde(rename = "controlBlock")]
|
||||
pub control_block: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TestVectorIntermediary {
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(rename = "leafHashes")]
|
||||
pub leaf_hashes: Vec<String>,
|
||||
#[serde(rename = "merkleRoot")]
|
||||
pub merkle_root: Option<String>
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TestVectorExpected {
|
||||
#[serde(rename = "scriptPubKey")]
|
||||
pub script_pubkey: Option<String>,
|
||||
#[serde(rename = "bip350Address")]
|
||||
pub bip350_address: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(rename = "scriptPathControlBlocks")]
|
||||
pub script_path_control_blocks: Option<Vec<String>>,
|
||||
#[serde(rename = "error")]
|
||||
pub error: Option<String>,
|
||||
#[serde(rename = "address")]
|
||||
pub address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub witness: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TVScriptLeaf {
|
||||
pub id: u8,
|
||||
pub script: String,
|
||||
#[serde(rename = "leafVersion")]
|
||||
pub leaf_version: u8,
|
||||
}
|
||||
|
||||
// Taproot script trees are binary trees, so each branch should have exactly two children.
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub enum TVScriptTree {
|
||||
Leaf(TVScriptLeaf),
|
||||
Branch {
|
||||
|
||||
// Box is used because Rust needs to know the exact size of types at compile time.
|
||||
// Without it, we'd have an infinitely size recursive type.
|
||||
// The enum itself is on the stack, but the Box fields within the Branch variant store pointers to heap-allocated ScriptTree values.
|
||||
left: Box<TVScriptTree>,
|
||||
right: Box<TVScriptTree>,
|
||||
},
|
||||
}
|
||||
|
||||
// Add custom deserialize implementation
|
||||
impl<'de> Deserialize<'de> for TVScriptTree {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
Leaf(TVScriptLeaf),
|
||||
Branch(Vec<TVScriptTree>),
|
||||
}
|
||||
|
||||
match Helper::deserialize(deserializer)? {
|
||||
Helper::Leaf(leaf) => Ok(TVScriptTree::Leaf(leaf)),
|
||||
Helper::Branch(v) => {
|
||||
assert!(v.len() == 2, "Branch must have exactly two children");
|
||||
let mut iter = v.into_iter();
|
||||
Ok(TVScriptTree::Branch {
|
||||
left: Box::new(iter.next().unwrap()),
|
||||
right: Box::new(iter.next().unwrap()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add this enum before the TVScriptTree implementation
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
Left,
|
||||
Right,
|
||||
Root,
|
||||
}
|
||||
|
||||
impl TVScriptTree {
|
||||
/// Implements a "post-order" traversal as follows: left, right, branch
|
||||
pub fn traverse_with_depth<F: FnMut(&TVScriptTree, u8, Direction)>(&self, depth: u8, direction: Direction, f: &mut F) {
|
||||
match self {
|
||||
TVScriptTree::Branch { left, right } => {
|
||||
|
||||
right.traverse_with_depth(depth, Direction::Right, f); // Pass Right for right subtree
|
||||
left.traverse_with_depth(depth, Direction::Left, f); // Pass Left for left subtree
|
||||
f(self, depth, direction); // Pass the current node's direction
|
||||
}
|
||||
TVScriptTree::Leaf { .. } => {
|
||||
f(self, depth, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Traverses the tree visiting right subtree leaves first, then left subtree leaves.
|
||||
/// Depth increases by 1 at each branch level.
|
||||
/*
|
||||
root (depth 0)
|
||||
/ \
|
||||
L0 (depth 1) (subtree) (depth 1)
|
||||
/ \
|
||||
L1 (depth 2) L2 (depth 2)
|
||||
|
||||
The new traversal will visit:
|
||||
L1 at depth 2 -> L2 at depth 2 -> L0 at depth 1
|
||||
*/
|
||||
pub fn traverse_with_right_subtree_first<F: FnMut(&TVScriptTree, u8, Direction)>(&self, depth: u8, direction: Direction, f: &mut F) {
|
||||
match self {
|
||||
TVScriptTree::Branch { left, right } => {
|
||||
let next_depth = depth + 1;
|
||||
// Visit right subtree first
|
||||
right.traverse_with_right_subtree_first(next_depth, Direction::Right, f);
|
||||
// Then visit left subtree
|
||||
left.traverse_with_right_subtree_first(next_depth, Direction::Left, f);
|
||||
}
|
||||
TVScriptTree::Leaf { .. } => {
|
||||
f(self, depth, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Direction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Direction::Left => write!(f, "L"),
|
||||
Direction::Right => write!(f, "R"),
|
||||
Direction::Root => write!(f, "Root"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScriptTreeHashCache {
|
||||
pub leaf_hashes: HashMap<String, String>,
|
||||
pub branch_hashes: HashMap<u8, String>,
|
||||
}
|
||||
|
||||
impl ScriptTreeHashCache {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
leaf_hashes: HashMap::new(),
|
||||
branch_hashes: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_leaf_hash(&mut self, branch_id: u8, direction: Direction, hash: String) {
|
||||
let key = format!("{branch_id}_{direction}");
|
||||
debug!("set_leaf_hash: key: {}, hash: {}", key, hash);
|
||||
self.leaf_hashes.insert(key, hash);
|
||||
}
|
||||
|
||||
pub fn set_branch_hash(&mut self, branch_id: u8, hash: String) {
|
||||
self.branch_hashes.insert(branch_id, hash);
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_hex<S>(bytes: &Vec<u8>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
s.serialize_str(&hex::encode(bytes))
|
||||
}
|
||||
|
||||
fn deserialize_hex<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(d)?;
|
||||
hex::decode(s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpendDetails {
|
||||
pub tx_hex: String,
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
#[serde(deserialize_with = "deserialize_hex")]
|
||||
pub sighash: Vec<u8>,
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
#[serde(deserialize_with = "deserialize_hex")]
|
||||
pub sig_bytes: Vec<u8>,
|
||||
#[serde(serialize_with = "serialize_hex")]
|
||||
#[serde(deserialize_with = "deserialize_hex")]
|
||||
pub derived_witness_vec: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::process::Termination for SpendDetails {
|
||||
fn report(self) -> std::process::ExitCode {
|
||||
if let Ok(json) = serde_json::to_string_pretty(&self) {
|
||||
println!("{}", json);
|
||||
} else {
|
||||
println!("{:?}", self);
|
||||
}
|
||||
std::process::ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UtxoReturn {
|
||||
|
||||
pub script_pubkey_hex: String,
|
||||
pub bech32m_address: String,
|
||||
pub bitcoin_network: bitcoin::Network,
|
||||
}
|
||||
|
||||
impl std::process::Termination for UtxoReturn {
|
||||
fn report(self) -> std::process::ExitCode {
|
||||
if let Ok(json) = serde_json::to_string_pretty(&self) {
|
||||
println!("{}", json);
|
||||
} else {
|
||||
println!("{:?}", self);
|
||||
}
|
||||
std::process::ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaptreeReturn {
|
||||
pub leaf_script_priv_keys_hex: Vec<String>, // Changed to support multiple private keys
|
||||
pub leaf_script_hex: String,
|
||||
pub tree_root_hex: String,
|
||||
pub control_block_hex: String,
|
||||
/// The script type of the leaf being returned (needed for spending)
|
||||
pub leaf_script_type: String,
|
||||
}
|
||||
|
||||
impl std::process::Termination for TaptreeReturn {
|
||||
fn report(self) -> std::process::ExitCode {
|
||||
if let Ok(json) = serde_json::to_string_pretty(&self) {
|
||||
println!("{}", json);
|
||||
} else {
|
||||
println!("{:?}", self);
|
||||
}
|
||||
std::process::ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConstructionReturn {
|
||||
pub taptree_return: TaptreeReturn,
|
||||
pub utxo_return: UtxoReturn,
|
||||
}
|
||||
|
||||
impl std::process::Termination for ConstructionReturn {
|
||||
fn report(self) -> std::process::ExitCode {
|
||||
if let Ok(json) = serde_json::to_string_pretty(&self) {
|
||||
println!("{}", json);
|
||||
} else {
|
||||
println!("{:?}", self);
|
||||
}
|
||||
std::process::ExitCode::SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/// A unified keypair that can contain either a Schnorr keypair or an SLH-DSA keypair
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UnifiedKeypair {
|
||||
Schnorr(SecretKey, XOnlyPublicKey),
|
||||
SlhDsa(KeyPair),
|
||||
}
|
||||
|
||||
/// A container for multiple keypairs that can be used in a single leaf script
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiKeypair {
|
||||
pub schnorr_keypair: Option<UnifiedKeypair>,
|
||||
pub slh_dsa_keypair: Option<UnifiedKeypair>,
|
||||
}
|
||||
|
||||
impl MultiKeypair {
|
||||
/// Create a new MultiKeypair with only a Schnorr keypair
|
||||
pub fn new_schnorr_only(schnorr_keypair: UnifiedKeypair) -> Self {
|
||||
Self {
|
||||
schnorr_keypair: Some(schnorr_keypair),
|
||||
slh_dsa_keypair: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new MultiKeypair with only an SLH-DSA keypair
|
||||
pub fn new_slh_dsa_only(slh_dsa_keypair: UnifiedKeypair) -> Self {
|
||||
Self {
|
||||
schnorr_keypair: None,
|
||||
slh_dsa_keypair: Some(slh_dsa_keypair),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new MultiKeypair with both keypairs
|
||||
pub fn new_combined(schnorr_keypair: UnifiedKeypair, slh_dsa_keypair: UnifiedKeypair) -> Self {
|
||||
Self {
|
||||
schnorr_keypair: Some(schnorr_keypair),
|
||||
slh_dsa_keypair: Some(slh_dsa_keypair),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all secret key bytes for serialization (in order: schnorr, then slh_dsa if present)
|
||||
pub fn secret_key_bytes(&self) -> Vec<Vec<u8>> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(ref schnorr) = self.schnorr_keypair {
|
||||
result.push(schnorr.secret_key_bytes());
|
||||
}
|
||||
if let Some(ref slh_dsa) = self.slh_dsa_keypair {
|
||||
result.push(slh_dsa.secret_key_bytes());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Get all public key bytes for script construction (in order: schnorr, then slh_dsa if present)
|
||||
pub fn public_key_bytes(&self) -> Vec<Vec<u8>> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(ref schnorr) = self.schnorr_keypair {
|
||||
result.push(schnorr.public_key_bytes());
|
||||
}
|
||||
if let Some(ref slh_dsa) = self.slh_dsa_keypair {
|
||||
result.push(slh_dsa.public_key_bytes());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if this contains a Schnorr keypair
|
||||
pub fn has_schnorr(&self) -> bool {
|
||||
self.schnorr_keypair.is_some()
|
||||
}
|
||||
|
||||
/// Check if this contains an SLH-DSA keypair
|
||||
pub fn has_slh_dsa(&self) -> bool {
|
||||
self.slh_dsa_keypair.is_some()
|
||||
}
|
||||
|
||||
/// Get the Schnorr keypair if present
|
||||
pub fn schnorr_keypair(&self) -> Option<&UnifiedKeypair> {
|
||||
self.schnorr_keypair.as_ref()
|
||||
}
|
||||
|
||||
/// Get the SLH-DSA keypair if present
|
||||
pub fn slh_dsa_keypair(&self) -> Option<&UnifiedKeypair> {
|
||||
self.slh_dsa_keypair.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a single leaf in a mixed-type tree
|
||||
/// Used when different leaves in the same tree use different algorithms
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MixedLeafInfo {
|
||||
/// The leaf index in the tree
|
||||
pub leaf_index: u32,
|
||||
/// The script type for this specific leaf
|
||||
pub leaf_script_type: LeafScriptType,
|
||||
/// The keypairs for this leaf
|
||||
pub keypairs: MultiKeypair,
|
||||
/// The script for this leaf
|
||||
pub script: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MixedLeafInfo {
|
||||
/// Create a new MixedLeafInfo for a Schnorr-only leaf
|
||||
pub fn new_schnorr(leaf_index: u32, keypairs: MultiKeypair, script: Vec<u8>) -> Self {
|
||||
Self {
|
||||
leaf_index,
|
||||
leaf_script_type: LeafScriptType::SchnorrOnly,
|
||||
keypairs,
|
||||
script,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new MixedLeafInfo for an SLH-DSA-only leaf
|
||||
pub fn new_slh_dsa(leaf_index: u32, keypairs: MultiKeypair, script: Vec<u8>) -> Self {
|
||||
Self {
|
||||
leaf_index,
|
||||
leaf_script_type: LeafScriptType::SlhDsaOnly,
|
||||
keypairs,
|
||||
script,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new MixedLeafInfo for a combined Schnorr+SLH-DSA leaf
|
||||
pub fn new_combined(leaf_index: u32, keypairs: MultiKeypair, script: Vec<u8>) -> Self {
|
||||
Self {
|
||||
leaf_index,
|
||||
leaf_script_type: LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf,
|
||||
keypairs,
|
||||
script,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the secret key bytes for this leaf
|
||||
pub fn secret_key_bytes(&self) -> Vec<Vec<u8>> {
|
||||
self.keypairs.secret_key_bytes()
|
||||
}
|
||||
|
||||
/// Get the public key bytes for this leaf
|
||||
pub fn public_key_bytes(&self) -> Vec<Vec<u8>> {
|
||||
self.keypairs.public_key_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl UnifiedKeypair {
|
||||
/// Create a new Schnorr keypair
|
||||
pub fn new_schnorr(secret_key: SecretKey, public_key: XOnlyPublicKey) -> Self {
|
||||
UnifiedKeypair::Schnorr(secret_key, public_key)
|
||||
}
|
||||
|
||||
/// Create a new SLH-DSA keypair
|
||||
pub fn new_slh_dsa(keypair: KeyPair) -> Self {
|
||||
UnifiedKeypair::SlhDsa(keypair)
|
||||
}
|
||||
|
||||
/// Get the secret key bytes for serialization
|
||||
pub fn secret_key_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
UnifiedKeypair::Schnorr(secret_key, _) => secret_key.secret_bytes().to_vec(),
|
||||
UnifiedKeypair::SlhDsa(keypair) => keypair.secret_key.bytes.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the public key bytes for script construction
|
||||
pub fn public_key_bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
UnifiedKeypair::Schnorr(_, public_key) => public_key.serialize().to_vec(),
|
||||
UnifiedKeypair::SlhDsa(keypair) => keypair.public_key.bytes.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the algorithm type
|
||||
pub fn algorithm(&self) -> &'static str {
|
||||
match self {
|
||||
UnifiedKeypair::Schnorr(_, _) => "Schnorr",
|
||||
UnifiedKeypair::SlhDsa(_) => "SLH-DSA",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a Schnorr keypair
|
||||
pub fn is_schnorr(&self) -> bool {
|
||||
matches!(self, UnifiedKeypair::Schnorr(_, _))
|
||||
}
|
||||
|
||||
/// Check if this is an SLH-DSA keypair
|
||||
pub fn is_slh_dsa(&self) -> bool {
|
||||
matches!(self, UnifiedKeypair::SlhDsa(_))
|
||||
}
|
||||
|
||||
/// Get the underlying Schnorr keypair if this is a Schnorr keypair
|
||||
pub fn as_schnorr(&self) -> Option<(&SecretKey, &XOnlyPublicKey)> {
|
||||
match self {
|
||||
UnifiedKeypair::Schnorr(secret_key, public_key) => Some((secret_key, public_key)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the underlying SLH-DSA keypair if this is an SLH-DSA keypair
|
||||
pub fn as_slh_dsa(&self) -> Option<&KeyPair> {
|
||||
match self {
|
||||
UnifiedKeypair::SlhDsa(keypair) => Some(keypair),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
16
bip-0360/ref-impl/rust/src/error.rs
Normal file
16
bip-0360/ref-impl/rust/src/error.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum P2MRError {
|
||||
|
||||
#[error("P2TR requires witness version of 1")]
|
||||
P2trRequiresWitnessVersion1,
|
||||
|
||||
|
||||
#[error("P2MR requires a script tree with at least one leaf")]
|
||||
MissingScriptTreeLeaf,
|
||||
|
||||
// We can add more specific error variants here as needed
|
||||
#[error("Invalid script tree structure: {0}")]
|
||||
InvalidScriptTree(String),
|
||||
}
|
||||
721
bip-0360/ref-impl/rust/src/lib.rs
Normal file
721
bip-0360/ref-impl/rust/src/lib.rs
Normal file
@@ -0,0 +1,721 @@
|
||||
pub mod data_structures;
|
||||
pub mod error;
|
||||
|
||||
use log::{debug, info, error};
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use rand::{rng, RngCore};
|
||||
use once_cell::sync::Lazy;
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::key::{Secp256k1, Parity};
|
||||
use bitcoin::secp256k1::{Message, SecretKey, Keypair, rand::rngs::OsRng, rand::thread_rng, rand::Rng, schnorr::Signature};
|
||||
use bitcoin::{ Amount, TxOut, WPubkeyHash,
|
||||
Address, Network, OutPoint,
|
||||
blockdata::witness::Witness,
|
||||
Script, ScriptBuf, XOnlyPublicKey, PublicKey,
|
||||
sighash::{SighashCache, TapSighashType, Prevouts, TapSighash},
|
||||
taproot::{LeafVersion, NodeInfo, TapLeafHash, TapNodeHash, TapTree, ScriptLeaves, TaprootMerkleBranch, TaprootBuilder, TaprootSpendInfo, ControlBlock},
|
||||
transaction::{Transaction, Sequence}
|
||||
};
|
||||
|
||||
use bitcoin::p2mr::{P2mrScriptBuf, P2mrBuilder, P2mrSpendInfo, P2mrControlBlock, P2MR_LEAF_VERSION};
|
||||
|
||||
use bitcoinpqc::{
|
||||
generate_keypair, public_key_size, secret_key_size, Algorithm, KeyPair, sign, verify,
|
||||
};
|
||||
|
||||
use data_structures::{SpendDetails, UtxoReturn, TaptreeReturn, UnifiedKeypair, MultiKeypair, LeafScriptType, MixedLeafInfo};
|
||||
|
||||
/* 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);
|
||||
|
||||
/// Creates a Huffman tree with leaves of the specified script type.
|
||||
///
|
||||
/// For Mixed type, leaves alternate between Schnorr (even indices) and SLH-DSA (odd indices).
|
||||
/// The LEAF_TO_SPEND_FROM_TYPE env var can override the type for the leaf of interest.
|
||||
///
|
||||
/// Returns: (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type)
|
||||
fn create_huffman_tree(leaf_script_type: LeafScriptType) -> (Vec<(u32, ScriptBuf)>, MultiKeypair, ScriptBuf, LeafScriptType) {
|
||||
|
||||
let mut total_leaf_count: u32 = 1;
|
||||
if let Ok(env_value) = env::var("TOTAL_LEAF_COUNT") {
|
||||
if let Ok(parsed_value) = env_value.parse::<u32>() {
|
||||
total_leaf_count = parsed_value;
|
||||
}
|
||||
}
|
||||
|
||||
let mut leaf_to_spend_from: u32 = 0;
|
||||
if let Ok(env_value) = env::var("LEAF_TO_SPEND_FROM") {
|
||||
if let Ok(parsed_value) = env_value.parse::<u32>() {
|
||||
leaf_to_spend_from = parsed_value;
|
||||
}
|
||||
}
|
||||
|
||||
// For Mixed mode, allow overriding the type of the leaf of interest
|
||||
let leaf_to_spend_from_type: Option<LeafScriptType> = if leaf_script_type == LeafScriptType::Mixed {
|
||||
env::var("LEAF_TO_SPEND_FROM_TYPE").ok().map(|s| LeafScriptType::from_string(&s))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if total_leaf_count < 1 {
|
||||
panic!("total_leaf_count must be greater than 0");
|
||||
}
|
||||
if leaf_to_spend_from >= total_leaf_count {
|
||||
panic!("leaf_to_spend_from must be less than total_leaf_count and greater than 0");
|
||||
}
|
||||
|
||||
debug!("Creating multi-leaf taptree with total_leaf_count: {}, leaf_to_spend_from: {}", total_leaf_count, leaf_to_spend_from);
|
||||
let mut huffman_entries: Vec<(u32, ScriptBuf)> = vec![];
|
||||
let mut keypairs_of_interest: Option<MultiKeypair> = None;
|
||||
let mut script_buf_of_interest: Option<ScriptBuf> = None;
|
||||
let mut actual_leaf_type_of_interest: LeafScriptType = leaf_script_type;
|
||||
|
||||
for leaf_index in 0..total_leaf_count {
|
||||
let keypairs: MultiKeypair;
|
||||
let script_buf: ScriptBuf;
|
||||
|
||||
// Determine the effective script type for this leaf
|
||||
let effective_script_type = if leaf_script_type == LeafScriptType::Mixed {
|
||||
// For Mixed mode, check if this is the leaf of interest with an override
|
||||
if leaf_index == leaf_to_spend_from && leaf_to_spend_from_type.is_some() {
|
||||
leaf_to_spend_from_type.unwrap()
|
||||
} else {
|
||||
// Default pattern: even indices use Schnorr, odd indices use SLH-DSA
|
||||
if leaf_index % 2 == 0 {
|
||||
LeafScriptType::SchnorrOnly
|
||||
} else {
|
||||
LeafScriptType::SlhDsaOnly
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leaf_script_type
|
||||
};
|
||||
|
||||
match effective_script_type {
|
||||
LeafScriptType::SchnorrOnly => {
|
||||
let schnorr_keypair = acquire_schnorr_keypair();
|
||||
keypairs = MultiKeypair::new_schnorr_only(schnorr_keypair);
|
||||
let pubkey_bytes = keypairs.schnorr_keypair().unwrap().public_key_bytes();
|
||||
// OP_PUSHBYTES_32 <32-byte xonly pubkey> OP_CHECKSIG
|
||||
let mut script_buf_bytes = vec![0x20];
|
||||
script_buf_bytes.extend_from_slice(&pubkey_bytes);
|
||||
script_buf_bytes.push(0xac); // OP_CHECKSIG
|
||||
script_buf = ScriptBuf::from_bytes(script_buf_bytes);
|
||||
},
|
||||
LeafScriptType::SlhDsaOnly => {
|
||||
let slh_dsa_keypair = acquire_slh_dsa_keypair();
|
||||
keypairs = MultiKeypair::new_slh_dsa_only(slh_dsa_keypair);
|
||||
let pubkey_bytes = keypairs.slh_dsa_keypair().unwrap().public_key_bytes();
|
||||
// OP_PUSHBYTES_32 <32-byte pubkey> OP_SUBSTR
|
||||
let mut script_buf_bytes = vec![0x20];
|
||||
script_buf_bytes.extend_from_slice(&pubkey_bytes);
|
||||
script_buf_bytes.push(0x7f); // OP_SUBSTR
|
||||
script_buf = ScriptBuf::from_bytes(script_buf_bytes);
|
||||
},
|
||||
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
|
||||
// For combined scripts, we need both keypairs
|
||||
let schnorr_keypair = acquire_schnorr_keypair();
|
||||
let slh_dsa_keypair = acquire_slh_dsa_keypair();
|
||||
keypairs = MultiKeypair::new_combined(schnorr_keypair, slh_dsa_keypair);
|
||||
|
||||
let schnorr_pubkey = keypairs.schnorr_keypair().unwrap().public_key_bytes();
|
||||
let slh_dsa_pubkey = keypairs.slh_dsa_keypair().unwrap().public_key_bytes();
|
||||
|
||||
// Debug: Print the private key used for script construction
|
||||
info!("SLH-DSA DEBUG: Script construction using private key: {}", hex::encode(keypairs.slh_dsa_keypair().unwrap().secret_key_bytes()));
|
||||
info!("SLH-DSA DEBUG: Script construction using public key: {}", hex::encode(&slh_dsa_pubkey));
|
||||
|
||||
// Combined script: <Schnorr_PubKey> OP_CHECKSIG <SLH_DSA_PubKey> OP_SUBSTR OP_BOOLAND OP_VERIFY
|
||||
let mut script_buf_bytes = vec![0x20]; // OP_PUSHBYTES_32
|
||||
script_buf_bytes.extend_from_slice(&schnorr_pubkey);
|
||||
script_buf_bytes.push(0xac); // OP_CHECKSIG
|
||||
script_buf_bytes.push(0x20); // OP_PUSHBYTES_32
|
||||
script_buf_bytes.extend_from_slice(&slh_dsa_pubkey);
|
||||
script_buf_bytes.push(0x7f); // OP_SUBSTR
|
||||
script_buf_bytes.push(0x9a); // OP_BOOLAND
|
||||
script_buf_bytes.push(0x69); // OP_VERIFY
|
||||
script_buf = ScriptBuf::from_bytes(script_buf_bytes);
|
||||
}
|
||||
LeafScriptType::Mixed => {
|
||||
// This shouldn't happen as Mixed is resolved to a specific type above
|
||||
panic!("LeafScriptType::Mixed should have been resolved to a specific type");
|
||||
}
|
||||
LeafScriptType::NotApplicable => {
|
||||
panic!("LeafScriptType::NotApplicable is not applicable");
|
||||
}
|
||||
}
|
||||
|
||||
let random_weight = thread_rng().gen_range(0..total_leaf_count);
|
||||
|
||||
let huffman_entry = (random_weight, script_buf.clone());
|
||||
huffman_entries.push(huffman_entry);
|
||||
if leaf_index == leaf_to_spend_from {
|
||||
keypairs_of_interest = Some(keypairs);
|
||||
script_buf_of_interest = Some(script_buf.clone());
|
||||
actual_leaf_type_of_interest = effective_script_type;
|
||||
debug!("Selected leaf {}: type: {:?}, weight: {}, script: {:?}",
|
||||
leaf_index, effective_script_type, random_weight, script_buf);
|
||||
}
|
||||
}
|
||||
return (huffman_entries, keypairs_of_interest.unwrap(), script_buf_of_interest.unwrap(), actual_leaf_type_of_interest);
|
||||
}
|
||||
|
||||
/// Parses the TAP_TREE_LOCK_TYPE environment variable and returns the corresponding LeafScriptType.
|
||||
/// Defaults to LeafScriptType::SchnorrOnly if the environment variable is not set.
|
||||
/// Exits with error code 1 if an invalid value is provided.
|
||||
///
|
||||
/// Supported values:
|
||||
/// - SLH_DSA_ONLY: All leaves use SLH-DSA signatures
|
||||
/// - SCHNORR_ONLY: All leaves use Schnorr signatures
|
||||
/// - CONCATENATED_SCHNORR_AND_SLH_DSA: All leaves require both Schnorr and SLH-DSA signatures
|
||||
/// - MIXED: Different leaves use different algorithms (Schnorr or SLH-DSA) (default)
|
||||
pub fn tap_tree_lock_type() -> LeafScriptType {
|
||||
match env::var("TAP_TREE_LOCK_TYPE") {
|
||||
Ok(value) => match value.as_str() {
|
||||
"SLH_DSA_ONLY" => LeafScriptType::SlhDsaOnly,
|
||||
"SCHNORR_ONLY" => LeafScriptType::SchnorrOnly,
|
||||
"CONCATENATED_SCHNORR_AND_SLH_DSA" => LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf,
|
||||
"MIXED" => LeafScriptType::Mixed,
|
||||
_ => {
|
||||
error!("Invalid TAP_TREE_LOCK_TYPE '{}'. Must be one of: SLH_DSA_ONLY, SCHNORR_ONLY, CONCATENATED_SCHNORR_AND_SLH_DSA, MIXED", value);
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
// Default to Mixed if not set
|
||||
LeafScriptType::Mixed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_p2mr_multi_leaf_taptree() -> TaptreeReturn {
|
||||
let leaf_script_type = tap_tree_lock_type();
|
||||
|
||||
let (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) = create_huffman_tree(leaf_script_type);
|
||||
let p2mr_builder: P2mrBuilder = P2mrBuilder::with_huffman_tree(huffman_entries).unwrap();
|
||||
|
||||
|
||||
let p2mr_spend_info: P2mrSpendInfo = p2mr_builder.clone().finalize().unwrap();
|
||||
let merkle_root:TapNodeHash = p2mr_spend_info.merkle_root.unwrap();
|
||||
|
||||
|
||||
let tap_tree: TapTree = p2mr_builder.clone().into_inner().try_into_taptree().unwrap();
|
||||
let mut script_leaves: ScriptLeaves = tap_tree.script_leaves();
|
||||
let script_leaf = script_leaves
|
||||
.find(|leaf| leaf.script() == script_buf_of_interest.as_script())
|
||||
.expect("Script leaf not found");
|
||||
|
||||
let merkle_root_node_info: NodeInfo = p2mr_builder.clone().into_inner().try_into_node_info().unwrap();
|
||||
let merkle_root: TapNodeHash = merkle_root_node_info.node_hash();
|
||||
|
||||
let leaf_hash: TapLeafHash = TapLeafHash::from_script(script_leaf.script(), LeafVersion::from_consensus(P2MR_LEAF_VERSION).unwrap());
|
||||
|
||||
// Convert leaf hash to big-endian for display (like Bitcoin Core)
|
||||
let mut leaf_hash_bytes = leaf_hash.as_raw_hash().to_byte_array().to_vec();
|
||||
leaf_hash_bytes.reverse();
|
||||
|
||||
info!("leaf_hash: {}, merkle_root: {}, merkle_root: {}",
|
||||
hex::encode(leaf_hash_bytes),
|
||||
merkle_root,
|
||||
merkle_root);
|
||||
|
||||
let leaf_script = script_leaf.script();
|
||||
let merkle_branch: &TaprootMerkleBranch = script_leaf.merkle_branch();
|
||||
|
||||
info!("Leaf script: {}, merkle branch: {:?}", leaf_script, merkle_branch);
|
||||
|
||||
let control_block: P2mrControlBlock = P2mrControlBlock{
|
||||
merkle_branch: merkle_branch.clone(),
|
||||
};
|
||||
|
||||
// Not a requirement here but useful to demonstrate what Bitcoin Core does as the verifier when spending from a p2mr UTXO
|
||||
control_block.verify_script_in_merkle_root_path(leaf_script, merkle_root);
|
||||
|
||||
let control_block_hex: String = hex::encode(control_block.serialize());
|
||||
|
||||
return TaptreeReturn {
|
||||
leaf_script_priv_keys_hex: keypairs_of_interest.secret_key_bytes()
|
||||
.into_iter()
|
||||
.map(|bytes| hex::encode(bytes))
|
||||
.collect(),
|
||||
leaf_script_hex: leaf_script.to_hex_string(),
|
||||
tree_root_hex: hex::encode(merkle_root.to_byte_array()),
|
||||
control_block_hex: control_block_hex,
|
||||
leaf_script_type: actual_leaf_type.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn create_p2tr_multi_leaf_taptree(p2tr_internal_pubkey_hex: String) -> TaptreeReturn {
|
||||
|
||||
let (huffman_entries, keypairs_of_interest, script_buf_of_interest, actual_leaf_type) = create_huffman_tree(LeafScriptType::SchnorrOnly);
|
||||
|
||||
let pub_key_string = format!("02{}", p2tr_internal_pubkey_hex);
|
||||
let internal_pubkey: PublicKey = pub_key_string.parse::<PublicKey>().unwrap();
|
||||
let internal_xonly_pubkey: XOnlyPublicKey = internal_pubkey.inner.into();
|
||||
|
||||
let p2tr_builder: TaprootBuilder = TaprootBuilder::with_huffman_tree(huffman_entries).unwrap();
|
||||
let p2tr_spend_info: TaprootSpendInfo = p2tr_builder.clone().finalize(&SECP, internal_xonly_pubkey).unwrap();
|
||||
let merkle_root: TapNodeHash = p2tr_spend_info.merkle_root().unwrap();
|
||||
|
||||
// During taproot construction, the internal key is "tweaked" by adding a scalar (the tap tweak hash) to it.
|
||||
// If this tweaking operation results in a public key w/ an odd Y-coordinate, the parity bit is set to 1.
|
||||
// When spending via script path, the verifier needs to know whether the output key has an even or odd Y-coordinate to properly reconstruct & verify the internal key.
|
||||
// The internal key can be recovered from the output key using the parity bit and the merkle root.
|
||||
let output_key_parity: Parity = p2tr_spend_info.output_key_parity();
|
||||
let output_key: XOnlyPublicKey = p2tr_spend_info.output_key().into();
|
||||
|
||||
info!("keypairs_of_interest: \n\tsecret_bytes: {:?} \n\tpubkeys: {:?} \n\tmerkle_root: {}",
|
||||
keypairs_of_interest.secret_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::<Vec<_>>(), // secret_bytes returns big endian
|
||||
keypairs_of_interest.public_key_bytes().iter().map(|bytes| hex::encode(bytes)).collect::<Vec<_>>(), // serialize returns little endian
|
||||
merkle_root);
|
||||
|
||||
let tap_tree: TapTree = p2tr_builder.clone().try_into_taptree().unwrap();
|
||||
let mut script_leaves: ScriptLeaves = tap_tree.script_leaves();
|
||||
let script_leaf = script_leaves
|
||||
.find(|leaf| leaf.script() == script_buf_of_interest.as_script())
|
||||
.expect("Script leaf not found");
|
||||
let leaf_script = script_leaf.script().to_hex_string();
|
||||
let merkle_branch: &TaprootMerkleBranch = script_leaf.merkle_branch();
|
||||
debug!("Leaf script: {}, merkle branch: {:?}", leaf_script, merkle_branch);
|
||||
|
||||
let control_block: ControlBlock = ControlBlock{
|
||||
leaf_version: LeafVersion::TapScript,
|
||||
output_key_parity: output_key_parity,
|
||||
internal_key: internal_xonly_pubkey,
|
||||
merkle_branch: merkle_branch.clone(),
|
||||
};
|
||||
let control_block_hex: String = hex::encode(control_block.serialize());
|
||||
|
||||
// Not a requirement but useful to demonstrate what Bitcoin Core does as the verifier when spending from a p2tr UTXO
|
||||
let verify: bool = verify_taproot_commitment(control_block_hex.clone(), output_key, script_leaf.script());
|
||||
info!("verify_taproot_commitment: {}", verify);
|
||||
|
||||
return TaptreeReturn {
|
||||
leaf_script_priv_keys_hex: keypairs_of_interest.secret_key_bytes()
|
||||
.into_iter()
|
||||
.map(|bytes| hex::encode(bytes))
|
||||
.collect(),
|
||||
leaf_script_hex: leaf_script,
|
||||
tree_root_hex: hex::encode(merkle_root.to_byte_array()),
|
||||
control_block_hex: control_block_hex,
|
||||
leaf_script_type: actual_leaf_type.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses the BITCOIN_NETWORK environment variable and returns the corresponding Network.
|
||||
/// Defaults to Network::Regtest if the environment variable is not set or has an invalid value.
|
||||
pub fn get_bitcoin_network() -> Network {
|
||||
let mut bitcoin_network: Network = Network::Regtest;
|
||||
|
||||
// Check for BITCOIN_NETWORK environment variable and override if set
|
||||
if let Ok(network_str) = std::env::var("BITCOIN_NETWORK") {
|
||||
bitcoin_network = match network_str.to_lowercase().as_str() {
|
||||
"regtest" => Network::Regtest,
|
||||
"testnet" => Network::Testnet,
|
||||
"signet" => Network::Signet,
|
||||
_ => {
|
||||
debug!("Invalid BITCOIN_NETWORK value '{}', using default Regtest network", network_str);
|
||||
Network::Regtest
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bitcoin_network
|
||||
}
|
||||
|
||||
pub fn create_p2mr_utxo(merkle_root_hex: String) -> UtxoReturn {
|
||||
|
||||
let merkle_root_bytes= hex::decode(merkle_root_hex.clone()).unwrap();
|
||||
let merkle_root: TapNodeHash = TapNodeHash::from_byte_array(merkle_root_bytes.try_into().unwrap());
|
||||
|
||||
/* commit (in scriptPubKey) to the merkle root of all the script path leaves. ie:
|
||||
This output key is what gets committed to in the final P2MR address (ie: scriptPubKey)
|
||||
*/
|
||||
let script_buf: P2mrScriptBuf = P2mrScriptBuf::new_p2mr(merkle_root);
|
||||
let script: &Script = script_buf.as_script();
|
||||
let script_pubkey = script.to_hex_string();
|
||||
|
||||
let bitcoin_network = get_bitcoin_network();
|
||||
|
||||
// derive bech32m address and verify against test vector
|
||||
// p2mr address is comprised of network HRP + WitnessProgram (version + program)
|
||||
let bech32m_address = Address::p2mr(Some(merkle_root), bitcoin_network);
|
||||
|
||||
return UtxoReturn {
|
||||
script_pubkey_hex: script_pubkey,
|
||||
bech32m_address: bech32m_address.to_string(),
|
||||
bitcoin_network,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Given script path p2tr or p2mr UTXO details, spend to p2wpkh
|
||||
pub fn pay_to_p2wpkh_tx(
|
||||
funding_tx_id_bytes: Vec<u8>,
|
||||
funding_utxo_index: u32,
|
||||
funding_utxo_amount_sats: u64,
|
||||
funding_script_pubkey_bytes: Vec<u8>,
|
||||
control_block_bytes: Vec<u8>,
|
||||
leaf_script_bytes: Vec<u8>,
|
||||
leaf_script_priv_keys_bytes: Vec<Vec<u8>>, // Changed to support multiple private keys
|
||||
spend_output_pubkey_hash_bytes: Vec<u8>,
|
||||
spend_output_amount_sats: u64,
|
||||
leaf_script_type: LeafScriptType
|
||||
) -> SpendDetails {
|
||||
|
||||
let mut txid_little_endian = funding_tx_id_bytes.clone(); // initially in big endian format
|
||||
txid_little_endian.reverse(); // convert to little endian format
|
||||
|
||||
// vin: Create TxIn from the input utxo
|
||||
// Details of this input tx are not known at this point
|
||||
let input_tx_in = bitcoin::TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: bitcoin::Txid::from_slice(&txid_little_endian).unwrap(), // bitcoin::Txid expects the bytes in little-endian format
|
||||
vout: funding_utxo_index,
|
||||
},
|
||||
script_sig: ScriptBuf::new(), // Empty for segwit transactions - script goes in witness
|
||||
sequence: Sequence::MAX, // Default sequence, allows immediate spending (no RBF or timelock)
|
||||
witness: bitcoin::Witness::new(), // Empty for now, will be filled with signature and pubkey after signing
|
||||
};
|
||||
|
||||
let spend_wpubkey_hash = WPubkeyHash::from_byte_array(spend_output_pubkey_hash_bytes.try_into().unwrap());
|
||||
let spend_output: TxOut = TxOut {
|
||||
value: Amount::from_sat(spend_output_amount_sats),
|
||||
script_pubkey: ScriptBuf::new_p2wpkh(&spend_wpubkey_hash),
|
||||
};
|
||||
|
||||
// The spend tx to eventually be signed and broadcast
|
||||
let mut unsigned_spend_tx = Transaction {
|
||||
version: bitcoin::transaction::Version::TWO,
|
||||
lock_time: bitcoin::locktime::absolute::LockTime::ZERO,
|
||||
input: vec![input_tx_in],
|
||||
output: vec![spend_output],
|
||||
};
|
||||
|
||||
// Create the leaf hash
|
||||
let leaf_script = ScriptBuf::from_bytes(leaf_script_bytes.clone());
|
||||
let leaf_hash: TapLeafHash = TapLeafHash::from_script(&leaf_script, LeafVersion::TapScript);
|
||||
|
||||
/* prevouts parameter tells the sighash algorithm:
|
||||
1. The value of each input being spent (needed for fee calculation and sighash computation)
|
||||
2. The scriptPubKey of each input being spent (ie: type of output & how to validate the spend)
|
||||
*/
|
||||
let prevouts = vec![TxOut {
|
||||
value: Amount::from_sat(funding_utxo_amount_sats),
|
||||
script_pubkey: ScriptBuf::from_bytes(funding_script_pubkey_bytes.clone()),
|
||||
}];
|
||||
info!("prevouts: {:?}", prevouts);
|
||||
|
||||
let spending_tx_input_index = 0;
|
||||
|
||||
// Create SighashCache
|
||||
// At this point, sighash_cache does not know the values and type of input UTXO
|
||||
let mut tapscript_sighash_cache = SighashCache::new(&mut unsigned_spend_tx);
|
||||
|
||||
// Compute the sighash
|
||||
let tapscript_sighash: TapSighash = tapscript_sighash_cache.taproot_script_spend_signature_hash(
|
||||
spending_tx_input_index, // input_index
|
||||
&Prevouts::All(&prevouts),
|
||||
leaf_hash,
|
||||
TapSighashType::All
|
||||
).unwrap();
|
||||
|
||||
info!("sighash: {:?}", tapscript_sighash);
|
||||
|
||||
let spend_msg = Message::from(tapscript_sighash);
|
||||
|
||||
let mut derived_witness: Witness = Witness::new();
|
||||
let mut sig_bytes = Vec::new();
|
||||
match leaf_script_type {
|
||||
LeafScriptType::SlhDsaOnly => {
|
||||
if leaf_script_priv_keys_bytes.len() != 1 {
|
||||
panic!("SlhDsaOnly requires exactly one private key");
|
||||
}
|
||||
let secret_key: bitcoinpqc::SecretKey = bitcoinpqc::SecretKey::try_from_slice(
|
||||
Algorithm::SLH_DSA_128S, &leaf_script_priv_keys_bytes[0]).unwrap();
|
||||
let signature = sign(&secret_key, spend_msg.as_ref()).expect("Failed to sign with SLH-DSA-128S");
|
||||
debug!("SlhDsaOnly signature.bytes: {:?}", signature.bytes.len());
|
||||
let mut sig_bytes_with_sighash = signature.bytes.clone();
|
||||
sig_bytes_with_sighash.push(TapSighashType::All as u8);
|
||||
derived_witness.push(&sig_bytes_with_sighash);
|
||||
sig_bytes = signature.bytes;
|
||||
},
|
||||
LeafScriptType::SchnorrOnly => {
|
||||
if leaf_script_priv_keys_bytes.len() != 1 {
|
||||
panic!("SchnorrOnly requires exactly one private key");
|
||||
}
|
||||
// assumes bytes are in big endian format
|
||||
let secret_key = SecretKey::from_slice(&leaf_script_priv_keys_bytes[0]).unwrap();
|
||||
|
||||
// Spending a p2tr UTXO thus using Schnorr signature
|
||||
// The aux_rand parameter ensures that signing the same message with the same key produces the same signature
|
||||
// Otherwise (without providing aux_rand), the secp256k1 library internally generates a random nonce for each signature
|
||||
let signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand(
|
||||
&spend_msg,
|
||||
&secret_key.keypair(&SECP),
|
||||
&[0u8; 32] // 32 zero bytes of auxiliary random data
|
||||
);
|
||||
sig_bytes = signature.serialize().to_vec();
|
||||
let mut sig_bytes_with_sighash = sig_bytes.clone();
|
||||
sig_bytes_with_sighash.push(TapSighashType::All as u8);
|
||||
derived_witness.push(&sig_bytes_with_sighash);
|
||||
debug!("SchnorrOnly signature bytes: {:?}", sig_bytes.len());
|
||||
},
|
||||
LeafScriptType::ConcatenatedSchnorrAndSlhDsaSameLeaf => {
|
||||
if leaf_script_priv_keys_bytes.len() != 2 {
|
||||
panic!("SchnorrAndSlhDsa requires exactly two private keys (Schnorr first, then SLH-DSA)");
|
||||
}
|
||||
|
||||
// Generate Schnorr signature (first key)
|
||||
let schnorr_secret_key = SecretKey::from_slice(&leaf_script_priv_keys_bytes[0]).unwrap();
|
||||
let schnorr_signature: bitcoin::secp256k1::schnorr::Signature = SECP.sign_schnorr_with_aux_rand(
|
||||
&spend_msg,
|
||||
&schnorr_secret_key.keypair(&SECP),
|
||||
&[0u8; 32] // 32 zero bytes of auxiliary random data
|
||||
);
|
||||
// Build combined signature for return value (without sighash bytes)
|
||||
let mut combined_sig_bytes = schnorr_signature.serialize().to_vec();
|
||||
debug!("SchnorrAndSlhDsa schnorr_sig_bytes: {:?}", combined_sig_bytes.len());
|
||||
|
||||
// Generate SLH-DSA signature (second key)
|
||||
let slh_dsa_secret_key: bitcoinpqc::SecretKey = bitcoinpqc::SecretKey::try_from_slice(
|
||||
Algorithm::SLH_DSA_128S, &leaf_script_priv_keys_bytes[1]).unwrap();
|
||||
|
||||
// Debug: Print the private key being used for signature creation
|
||||
info!("SLH-DSA DEBUG: Using private key for signature creation: {}", hex::encode(&leaf_script_priv_keys_bytes[1]));
|
||||
|
||||
let slh_dsa_signature = sign(&slh_dsa_secret_key, spend_msg.as_ref()).expect("Failed to sign with SLH-DSA-128S");
|
||||
debug!("SchnorrAndSlhDsa slh_dsa_signature.bytes: {:?}", slh_dsa_signature.bytes.len());
|
||||
|
||||
// Add SLH-DSA signature to combined signature for return value
|
||||
combined_sig_bytes.extend_from_slice(&slh_dsa_signature.bytes);
|
||||
sig_bytes = combined_sig_bytes;
|
||||
|
||||
// Build witness with sighash bytes
|
||||
let mut witness_sig_bytes = schnorr_signature.serialize().to_vec();
|
||||
witness_sig_bytes.push(TapSighashType::All as u8);
|
||||
witness_sig_bytes.extend_from_slice(&slh_dsa_signature.bytes);
|
||||
witness_sig_bytes.push(TapSighashType::All as u8);
|
||||
derived_witness.push(&witness_sig_bytes);
|
||||
}
|
||||
LeafScriptType::Mixed => {
|
||||
// Mixed is not a valid type for spending - the actual leaf type should be used
|
||||
panic!("LeafScriptType::Mixed is not valid for spending. Use the actual leaf type (SchnorrOnly or SlhDsaOnly).");
|
||||
}
|
||||
LeafScriptType::NotApplicable => {
|
||||
panic!("LeafScriptType::NotApplicable is not applicable");
|
||||
}
|
||||
}
|
||||
// Note: sighash byte is now appended to signatures, not as separate witness element
|
||||
derived_witness.push(&leaf_script_bytes);
|
||||
derived_witness.push(&control_block_bytes);
|
||||
|
||||
let derived_witness_vec: Vec<u8> = derived_witness.iter().flatten().cloned().collect();
|
||||
|
||||
// Update the witness data for the tx's first input (index 0)
|
||||
*tapscript_sighash_cache.witness_mut(spending_tx_input_index).unwrap() = derived_witness;
|
||||
|
||||
// Get the signed transaction.
|
||||
let signed_tx_obj: &mut Transaction = tapscript_sighash_cache.into_transaction();
|
||||
|
||||
let tx_hex = bitcoin::consensus::encode::serialize_hex(&signed_tx_obj);
|
||||
|
||||
return SpendDetails {
|
||||
tx_hex,
|
||||
sighash: tapscript_sighash.as_byte_array().to_vec(),
|
||||
sig_bytes: sig_bytes,
|
||||
derived_witness_vec: derived_witness_vec,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
pub fn create_p2tr_utxo(merkle_root_hex: String, internal_pubkey_hex: String) -> UtxoReturn {
|
||||
|
||||
let merkle_root_bytes= hex::decode(merkle_root_hex.clone()).unwrap();
|
||||
let merkle_root: TapNodeHash = TapNodeHash::from_byte_array(merkle_root_bytes.try_into().unwrap());
|
||||
|
||||
let pub_key_string = format!("02{}", internal_pubkey_hex);
|
||||
let internal_pubkey: PublicKey = pub_key_string.parse::<PublicKey>().unwrap();
|
||||
let internal_xonly_pubkey: XOnlyPublicKey = internal_pubkey.inner.into();
|
||||
|
||||
|
||||
let script_buf: ScriptBuf = ScriptBuf::new_p2tr(&SECP, internal_xonly_pubkey, Option::Some(merkle_root));
|
||||
let script: &Script = script_buf.as_script();
|
||||
let script_pubkey = script.to_hex_string();
|
||||
|
||||
let bitcoin_network = get_bitcoin_network();
|
||||
|
||||
// 4) derive bech32m address and verify against test vector
|
||||
// p2mr address is comprised of network HRP + WitnessProgram (version + program)
|
||||
let bech32m_address = Address::p2tr(
|
||||
&SECP,
|
||||
internal_xonly_pubkey,
|
||||
Option::Some(merkle_root),
|
||||
bitcoin_network
|
||||
);
|
||||
|
||||
return UtxoReturn {
|
||||
script_pubkey_hex: script_pubkey,
|
||||
bech32m_address: bech32m_address.to_string(),
|
||||
bitcoin_network,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
// https://learnmeabitcoin.com/technical/upgrades/taproot/#examples
|
||||
pub fn tagged_hash(tag: &str, data: &[u8]) -> String {
|
||||
|
||||
// Create a hash of the tag first
|
||||
let tag_hash = sha256::Hash::hash(tag.as_bytes());
|
||||
|
||||
// Create preimage: tag_hash || tag_hash || message
|
||||
// tag_hash is prefixed twice so that the prefix is 64 bytes in total
|
||||
let mut preimage = sha256::Hash::engine();
|
||||
preimage.write_all(&tag_hash.to_byte_array()).unwrap(); // First tag hash
|
||||
preimage.write_all(&tag_hash.to_byte_array()).unwrap(); // Second tag hash
|
||||
preimage.write_all(data).unwrap(); // Message data
|
||||
let hash = sha256::Hash::from_engine(preimage).to_byte_array();
|
||||
hex::encode(hash)
|
||||
}
|
||||
|
||||
pub fn serialize_script(script: &Vec<u8>) -> Vec<u8> {
|
||||
// get length of script as number of bytes
|
||||
let length = script.len();
|
||||
|
||||
// return script with compact size prepended
|
||||
let mut result = compact_size(length as u64);
|
||||
result.extend_from_slice(&script);
|
||||
result
|
||||
}
|
||||
|
||||
/// Encodes an integer into Bitcoin's compact size format
|
||||
/// Returns a Vec<u8> containing the encoded bytes
|
||||
fn compact_size(n: u64) -> Vec<u8> {
|
||||
if n <= 252 {
|
||||
vec![n as u8]
|
||||
} else if n <= 0xffff {
|
||||
let mut result = vec![0xfd];
|
||||
result.extend_from_slice(&(n as u16).to_le_bytes());
|
||||
result
|
||||
} else if n <= 0xffffffff {
|
||||
let mut result = vec![0xfe];
|
||||
result.extend_from_slice(&(n as u32).to_le_bytes());
|
||||
result
|
||||
} else {
|
||||
let mut result = vec![0xff];
|
||||
result.extend_from_slice(&n.to_le_bytes());
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquire_schnorr_keypair() -> UnifiedKeypair {
|
||||
|
||||
/* OsRng typically draws from the OS's entropy pool (hardware random num generators, system events, etc), ie:
|
||||
* 1. $ cat /proc/sys/kernel/random/entropy_avail
|
||||
* 2. $ sudo dmesg | grep -i "random\|rng\|entropy"
|
||||
|
||||
The Linux kernel's RNG (/dev/random and /dev/urandom) typically combines multiple entropy sources: ie:
|
||||
* Hardware RNG (if available)
|
||||
* CPU RNG instructions (RDRAND/RDSEED)
|
||||
* Hardware events (disk I/O, network packets, keyboard/mouse input)
|
||||
* Timer jitter
|
||||
* Interrupt timing
|
||||
*/
|
||||
let keypair = Keypair::new(&SECP, &mut OsRng);
|
||||
|
||||
let privkey: SecretKey = keypair.secret_key();
|
||||
let pubkey: (XOnlyPublicKey, Parity) = XOnlyPublicKey::from_keypair(&keypair);
|
||||
UnifiedKeypair::new_schnorr(privkey, pubkey.0)
|
||||
}
|
||||
|
||||
pub fn verify_schnorr_signature_via_bytes(signature: &[u8], message: &[u8], pubkey_bytes: &[u8]) -> bool {
|
||||
|
||||
// schnorr is 64 bytes so remove possible trailing Sighash Type byte if present
|
||||
let mut sig_bytes = signature.to_vec();
|
||||
if sig_bytes.len() == 65 {
|
||||
sig_bytes.pop(); // Remove the last byte
|
||||
}
|
||||
let signature = bitcoin::secp256k1::schnorr::Signature::from_slice(&sig_bytes).unwrap();
|
||||
let message = Message::from_digest_slice(message).unwrap();
|
||||
let pubkey = XOnlyPublicKey::from_slice(pubkey_bytes).unwrap();
|
||||
verify_schnorr_signature(signature, message, pubkey)
|
||||
}
|
||||
|
||||
pub fn verify_slh_dsa_via_bytes(signature: &[u8], message: &[u8], pubkey_bytes: &[u8]) -> bool {
|
||||
|
||||
// Remove possible trailing Sighash Type byte if present (SLH-DSA-128S is 7856 bytes, so 7857 would indicate SIGHASH byte)
|
||||
let mut sig_bytes = signature.to_vec();
|
||||
if sig_bytes.len() == 7857 {
|
||||
sig_bytes.pop(); // Remove the last byte
|
||||
}
|
||||
|
||||
info!("verify_slh_dsa_via_bytes: signature length: {:?}, message: {:?}, pubkey_bytes: {:?}",
|
||||
sig_bytes.len(),
|
||||
hex::encode(message),
|
||||
hex::encode(pubkey_bytes));
|
||||
|
||||
let signature = bitcoinpqc::Signature::try_from_slice(Algorithm::SLH_DSA_128S, &sig_bytes).unwrap();
|
||||
let public_key: bitcoinpqc::PublicKey = bitcoinpqc::PublicKey::try_from_slice(Algorithm::SLH_DSA_128S, pubkey_bytes).unwrap();
|
||||
verify(&public_key, message, &signature).is_ok()
|
||||
}
|
||||
|
||||
pub fn verify_schnorr_signature(mut signature: Signature, message: Message, pubkey: XOnlyPublicKey) -> bool {
|
||||
|
||||
// schnorr is 64 bytes so remove possible trailing Sighash Type byte if present
|
||||
if signature.serialize().to_vec().len() == 65 {
|
||||
let mut sig_bytes = signature.serialize().to_vec();
|
||||
sig_bytes.pop(); // Remove the last byte
|
||||
signature = bitcoin::secp256k1::schnorr::Signature::from_slice(&sig_bytes).unwrap();
|
||||
}
|
||||
let is_valid: bool = SECP.verify_schnorr(&signature, &message, &pubkey).is_ok();
|
||||
if !is_valid {
|
||||
error!("verify schnorr failed:\n\tsignature: {:?}\n\tmessage: {:?}\n\tpubkey: {:?}",
|
||||
signature,
|
||||
message,
|
||||
hex::encode(pubkey.serialize()));
|
||||
}
|
||||
is_valid
|
||||
}
|
||||
|
||||
/* 1. Re-constructs merkle_root from merkle_path (found in control_block) and provided script.
|
||||
2. Determines the parity of the output key via the control byte (found in the control block).
|
||||
- the parity bit indicates whether the output key has an even or odd Y-coordinate
|
||||
3. Computes the tap tweak hash using the internal key and reconstructed merkle root.
|
||||
- tap_tweak_hash = tagged_hash("TapTweak", internal_key || merkle_root)
|
||||
4. Verifies that the provided output key can be derived from the internal key using the tweak.
|
||||
- tap_tweak_hash = tagged_hash("TapTweak", internal_key || merkle_root)
|
||||
5. This proves the script is committed to in the taptree described by the output key.
|
||||
*/
|
||||
pub fn verify_taproot_commitment(control_block_hex: String, output_key: XOnlyPublicKey, script: &Script) -> bool {
|
||||
|
||||
let control_block_bytes = hex::decode(control_block_hex).unwrap();
|
||||
let control_block: ControlBlock = ControlBlock::decode(&control_block_bytes).unwrap();
|
||||
|
||||
return control_block.verify_taproot_commitment(&SECP, output_key, script);
|
||||
}
|
||||
|
||||
fn acquire_slh_dsa_keypair() -> UnifiedKeypair {
|
||||
/*
|
||||
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);
|
||||
let keypair: KeyPair = generate_keypair(Algorithm::SLH_DSA_128S, &random_data)
|
||||
.expect("Failed to generate SLH-DSA-128S keypair");
|
||||
UnifiedKeypair::new_slh_dsa(keypair)
|
||||
}
|
||||
|
||||
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