Skip to content

Commit 75b217c

Browse files
committed
feat(compile): randomize unspendable internal key for taproot descriptor
Instead of using a fixed NUMS key as the internal key for taproot descriptors, generate a randomized unspendable key (H + rG) for each compilation. This improves privacy by preventing observers from determining whether key path spending is disabled. The randomness factor `r` is included in the output so the user can verify that the internal key is derived from the NUMS point. Also applies `shorten()` globally in pretty mode and uses `?` operator via dedicated error variants instead of `map_err`.
1 parent 37e0465 commit 75b217c

File tree

2 files changed

+53
-25
lines changed

2 files changed

+53
-25
lines changed

src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pub enum BDKCliError {
4545
#[error("Miniscript error: {0}")]
4646
MiniscriptError(#[from] bdk_wallet::miniscript::Error),
4747

48+
#[error("Miniscript compiler error: {0}")]
49+
MiniscriptCompilerError(#[from] bdk_wallet::miniscript::policy::compiler::CompilerError),
50+
4851
#[error("ParseError: {0}")]
4952
ParseError(#[from] bdk_wallet::bitcoin::address::ParseError),
5053

@@ -78,6 +81,10 @@ pub enum BDKCliError {
7881
#[error("Signer error: {0}")]
7982
SignerError(#[from] bdk_wallet::signer::SignerError),
8083

84+
#[cfg(feature = "compiler")]
85+
#[error("Secp256k1 error: {0}")]
86+
Secp256k1Error(#[from] bdk_wallet::bitcoin::secp256k1::Error),
87+
8188
#[cfg(feature = "electrum")]
8289
#[error("Electrum error: {0}")]
8390
Electrum(#[from] bdk_electrum::electrum_client::Error),

src/handlers.rs

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,18 @@ use bdk_wallet::miniscript::miniscript;
4040
#[cfg(feature = "sqlite")]
4141
use bdk_wallet::rusqlite::Connection;
4242
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
43+
4344
#[cfg(feature = "compiler")]
4445
use bdk_wallet::{
45-
bitcoin::XOnlyPublicKey,
46+
bitcoin::{
47+
XOnlyPublicKey,
48+
key::{Parity, rand},
49+
secp256k1::{PublicKey, Scalar, SecretKey},
50+
},
4651
descriptor::{Descriptor, Legacy, Miniscript},
4752
miniscript::{Tap, descriptor::TapTree, policy::Concrete},
4853
};
54+
4955
use clap::CommandFactory;
5056
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
5157
use serde_json::json;
@@ -1014,50 +1020,65 @@ pub(crate) fn handle_compile_subcommand(
10141020
pretty: bool,
10151021
) -> Result<String, Error> {
10161022
let policy = Concrete::<String>::from_str(policy.as_str())?;
1017-
let legacy_policy: Miniscript<String, Legacy> = policy
1018-
.compile()
1019-
.map_err(|e| Error::Generic(e.to_string()))?;
1020-
let segwit_policy: Miniscript<String, Segwitv0> = policy
1021-
.compile()
1022-
.map_err(|e| Error::Generic(e.to_string()))?;
1023-
let taproot_policy: Miniscript<String, Tap> = policy
1024-
.compile()
1025-
.map_err(|e| Error::Generic(e.to_string()))?;
1023+
1024+
let legacy_policy: Miniscript<String, Legacy> = policy.compile()?;
1025+
let segwit_policy: Miniscript<String, Segwitv0> = policy.compile()?;
1026+
let taproot_policy: Miniscript<String, Tap> = policy.compile()?;
1027+
1028+
let mut r = None;
10261029

10271030
let descriptor = match script_type.as_str() {
10281031
"sh" => Descriptor::new_sh(legacy_policy),
10291032
"wsh" => Descriptor::new_wsh(segwit_policy),
10301033
"sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
10311034
"tr" => {
1032-
// For tr descriptors, we use a well-known unspendable key (NUMS point).
1033-
// This ensures the key path is effectively disabled and only script path can be used.
1034-
// See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
1035+
// For tr descriptors, we use a randomized unspendable key (H + rG).
1036+
// This improves privacy by preventing observers from determining if key path spending is disabled.
1037+
// See BIP-341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
1038+
1039+
let secp = Secp256k1::new();
1040+
let r_secret = SecretKey::new(&mut rand::thread_rng());
1041+
r = Some(r_secret.display_secret().to_string());
1042+
1043+
let nums_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)?;
1044+
let nums_point = PublicKey::from_x_only_public_key(nums_key, Parity::Even);
10351045

1036-
let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
1037-
.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
1046+
let internal_key_point = nums_point.add_exp_tweak(&secp, &Scalar::from(r_secret))?;
1047+
let (xonly_internal_key, _) = internal_key_point.x_only_public_key();
10381048

10391049
let tree = TapTree::Leaf(Arc::new(taproot_policy));
1040-
Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
1050+
1051+
Descriptor::new_tr(xonly_internal_key.to_string(), Some(tree))
10411052
}
10421053
_ => {
10431054
return Err(Error::Generic(
10441055
"Invalid script type. Supported types: sh, wsh, sh-wsh, tr".to_string(),
10451056
));
10461057
}
10471058
}?;
1059+
10481060
if pretty {
1049-
let table = vec![vec![
1061+
let mut rows = vec![vec![
10501062
"Descriptor".cell().bold(true),
1051-
descriptor.to_string().cell(),
1052-
]]
1053-
.table()
1054-
.display()
1055-
.map_err(|e| Error::Generic(e.to_string()))?;
1063+
shorten(&descriptor, 32, 29).cell(),
1064+
]];
1065+
1066+
if let Some(r_value) = &r {
1067+
rows.push(vec!["r".cell().bold(true), shorten(r_value, 4, 4).cell()]);
1068+
}
1069+
1070+
let table = rows
1071+
.table()
1072+
.display()
1073+
.map_err(|e| Error::Generic(e.to_string()))?;
1074+
10561075
Ok(format!("{table}"))
10571076
} else {
1058-
Ok(serde_json::to_string_pretty(
1059-
&json!({"descriptor": descriptor.to_string()}),
1060-
)?)
1077+
let mut output = json!({"descriptor": descriptor});
1078+
if let Some(r_value) = r {
1079+
output["r"] = json!(r_value);
1080+
}
1081+
Ok(serde_json::to_string_pretty(&output)?)
10611082
}
10621083
}
10631084

0 commit comments

Comments
 (0)