Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
37 changes: 5 additions & 32 deletions src/handlers/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -109,26 +98,10 @@ impl AppCommand<AppContext<Init>> 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))
}
}?;

Expand Down
2 changes: 2 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
127 changes: 127 additions & 0 deletions src/utils/taproot.rs
Original file line number Diff line number Diff line change
@@ -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<XOnlyPublicKey, Error> {
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<XOnlyPublicKey, Error> {
let descriptor = Descriptor::<String>::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(_))
));
}
}
Loading