diff --git a/src/error.rs b/src/error.rs index 76b31fd0..ddaf1c19 100644 --- a/src/error.rs +++ b/src/error.rs @@ -118,6 +118,10 @@ pub enum BDKCliError { #[error("Secp256k1 error: {0}")] Secp256k1Error(#[from] bdk_wallet::bitcoin::secp256k1::Error), + #[cfg(feature = "compiler")] + #[error("Internal key is not the NUMS point tweaked by the given r")] + UnspendableKeyMismatch, + #[cfg(feature = "electrum")] #[error("Electrum error: {0}")] Electrum(#[from] bdk_electrum::electrum_client::Error), diff --git a/src/handlers/descriptor.rs b/src/handlers/descriptor.rs index ba5005c5..46104930 100644 --- a/src/handlers/descriptor.rs +++ b/src/handlers/descriptor.rs @@ -13,27 +13,16 @@ use crate::{ }; use clap::Parser; #[cfg(feature = "compiler")] -use clap::ValueEnum; -#[cfg(feature = "compiler")] use { - bdk_wallet::{ - bitcoin::{ - XOnlyPublicKey, - key::{Parity, rand}, - secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}, - }, - miniscript::{Descriptor, descriptor::TapTree, policy::Concrete}, - }, + crate::utils::taproot::generate_unspendable_internal_key, + bdk_wallet::miniscript::{Descriptor, descriptor::TapTree, policy::Concrete}, + clap::ValueEnum, std::{str::FromStr, sync::Arc}, }; #[cfg(feature = "silent-payments")] use crate::utils::types::StatusResult; -#[cfg(feature = "compiler")] -const NUMS_UNSPENDABLE_KEY_HEX: &str = - "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; - #[derive(Parser, Debug, Clone, PartialEq)] pub struct DescriptorCommand { /// Descriptor type (script type) @@ -109,26 +98,10 @@ impl AppCommand> for CompileCommand { ScriptType::Wsh => Descriptor::new_wsh(policy.compile()?), ScriptType::ShWsh => Descriptor::new_sh_wsh(policy.compile()?), ScriptType::Tr => { - // Use a randomized unspendable internal key (H + rG) instead of a fixed NUMS - // point. This improves privacy by preventing observers from determining whether - // key-path spending is disabled. `r` is returned so the user can verify the key - // is derived from the NUMS point. See BIP-341. - let secp = Secp256k1::new(); - let r_secret = SecretKey::new(&mut rand::thread_rng()); + let (internal_key, r_secret) = generate_unspendable_internal_key()?; r = Some(r_secret.display_secret().to_string()); - - let nums_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX) - .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?; - let nums_point = PublicKey::from_x_only_public_key(nums_key, Parity::Even); - - let internal_key_point = - nums_point - .add_exp_tweak(&secp, &Scalar::from(r_secret)) - .map_err(|e| Error::Generic(format!("Failed to tweak NUMS key: {e}")))?; - let (xonly_internal_key, _) = internal_key_point.x_only_public_key(); - let tree = TapTree::Leaf(Arc::new(policy.compile()?)); - Descriptor::new_tr(xonly_internal_key.to_string(), Some(tree)) + Descriptor::new_tr(internal_key.to_string(), Some(tree)) } }?; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 15138df5..b1777ad7 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -5,4 +5,6 @@ pub use common::*; #[cfg(feature = "hwi")] pub mod hwi; pub mod runtime; +#[cfg(feature = "compiler")] +pub mod taproot; pub mod types; diff --git a/src/utils/taproot.rs b/src/utils/taproot.rs new file mode 100644 index 00000000..e6fd1617 --- /dev/null +++ b/src/utils/taproot.rs @@ -0,0 +1,127 @@ +use std::str::FromStr; + +use bdk_wallet::bitcoin::{ + XOnlyPublicKey, + key::{Parity, rand}, + secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}, +}; +use bdk_wallet::miniscript::Descriptor; + +use crate::error::BDKCliError as Error; + +/// The BIP-341 NUMS point `H`, x-only. +const NUMS_UNSPENDABLE_KEY_HEX: &str = + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; + +pub fn generate_unspendable_internal_key() -> Result<(XOnlyPublicKey, SecretKey), Error> { + let r = SecretKey::new(&mut rand::thread_rng()); + let internal_key = derive_unspendable_internal_key(&r)?; + Ok((internal_key, r)) +} + +/// Derive the unspendable internal key `H + rG` for the randomness factor `r`. +/// +/// Using the NUMS point `H` verbatim is recognisable on chain, so it is randomised. +/// `H + rG` is unspendable too, since the discrete log of `H` is unknown. +/// See BIP-341. +pub fn derive_unspendable_internal_key(r: &SecretKey) -> Result { + let secp = Secp256k1::new(); + + let nums_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX) + .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?; + let nums_point = PublicKey::from_x_only_public_key(nums_key, Parity::Even); + + let internal_key_point = nums_point + .add_exp_tweak(&secp, &Scalar::from(*r)) + .map_err(|e| Error::Generic(format!("Failed to tweak NUMS key: {e}")))?; + + Ok(internal_key_point.x_only_public_key().0) +} + +/// Check that the internal key of a `tr()` descriptor is the NUMS point tweaked by `r`. +// No caller until the CLI exposes verification. +#[allow(dead_code)] +pub fn verify_unspendable_internal_key(descriptor: &str, r: &SecretKey) -> Result<(), Error> { + if derive_unspendable_internal_key(r)? != taproot_internal_key(descriptor)? { + return Err(Error::UnspendableKeyMismatch); + } + + Ok(()) +} + +/// Extract the internal key of a `tr()` descriptor. +#[allow(dead_code)] +fn taproot_internal_key(descriptor: &str) -> Result { + let descriptor = Descriptor::::from_str(descriptor) + .map_err(|e| Error::Generic(format!("Invalid descriptor: {e}")))?; + + match descriptor { + Descriptor::Tr(tr) => XOnlyPublicKey::from_str(tr.internal_key()) + .map_err(|e| Error::Generic(format!("Invalid taproot internal key: {e}"))), + _ => Err(Error::Generic( + "Not a taproot descriptor, there is no internal key to verify".to_string(), + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// An `r` and the internal key it produced, from the output of `compile --type tr`. + const R_HEX: &str = "69c00a610663904ebb0029001bc4f56f99774a0dc9d3b46e1a4bc50629df9a4b"; + const INTERNAL_KEY_HEX: &str = + "c43c9c7a8b5a545fa49320124166499c0e1a092a991df1839800afb37693c5a8"; + const DESCRIPTOR: &str = + "tr(c43c9c7a8b5a545fa49320124166499c0e1a092a991df1839800afb37693c5a8,pk(A))#qwt4mlqc"; + + fn r() -> SecretKey { + SecretKey::from_str(R_HEX).expect("test vector should be a valid secret key") + } + + #[test] + fn derives_the_key_the_command_used_to_derive() { + let expected = + XOnlyPublicKey::from_str(INTERNAL_KEY_HEX).expect("test vector should be a key"); + + assert_eq!( + derive_unspendable_internal_key(&r()).expect("derivation should succeed"), + expected + ); + } + + #[test] + fn accepts_the_pair_compile_produced() { + verify_unspendable_internal_key(DESCRIPTOR, &r()).expect("the pair should verify"); + } + + #[test] + fn rejects_a_different_randomness_factor() { + let other = SecretKey::from_slice(&[7u8; 32]).expect("7 repeated is a valid secret key"); + + assert!(matches!( + verify_unspendable_internal_key(DESCRIPTOR, &other), + Err(Error::UnspendableKeyMismatch) + )); + } + + #[test] + fn rejects_a_plain_nums_internal_key() { + // `H` on its own is what the randomisation is meant to avoid: it is unspendable, but it + // is not `H + rG` for any `r`, so verification has to reject it. + let descriptor = format!("tr({NUMS_UNSPENDABLE_KEY_HEX},pk(A))"); + + assert!(matches!( + verify_unspendable_internal_key(&descriptor, &r()), + Err(Error::UnspendableKeyMismatch) + )); + } + + #[test] + fn rejects_a_descriptor_that_is_not_taproot() { + assert!(matches!( + verify_unspendable_internal_key("wsh(pk(A))", &r()), + Err(Error::Generic(_)) + )); + } +}