Skip to content

Commit b5dac76

Browse files
committed
feat(compile): use random internal key for tr descriptor
1 parent fbf263e commit b5dac76

File tree

2 files changed

+59
-26
lines changed

2 files changed

+59
-26
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: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ use bdk_wallet::miniscript::miniscript;
3737
#[cfg(feature = "sqlite")]
3838
use bdk_wallet::rusqlite::Connection;
3939
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
40+
4041
#[cfg(feature = "compiler")]
4142
use bdk_wallet::{
42-
bitcoin::XOnlyPublicKey,
43+
bitcoin::{
44+
XOnlyPublicKey,
45+
key::{Parity, rand},
46+
secp256k1::{PublicKey, Scalar, SecretKey},
47+
},
4348
descriptor::{Descriptor, Legacy, Miniscript},
4449
miniscript::{Tap, descriptor::TapTree, policy::Concrete},
4550
};
51+
4652
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
4753
use serde_json::json;
4854
#[cfg(feature = "cbf")]
@@ -893,50 +899,70 @@ pub(crate) fn handle_compile_subcommand(
893899
pretty: bool,
894900
) -> Result<String, Error> {
895901
let policy = Concrete::<String>::from_str(policy.as_str())?;
896-
let legacy_policy: Miniscript<String, Legacy> = policy
897-
.compile()
898-
.map_err(|e| Error::Generic(e.to_string()))?;
899-
let segwit_policy: Miniscript<String, Segwitv0> = policy
900-
.compile()
901-
.map_err(|e| Error::Generic(e.to_string()))?;
902-
let taproot_policy: Miniscript<String, Tap> = policy
903-
.compile()
904-
.map_err(|e| Error::Generic(e.to_string()))?;
902+
903+
let legacy_policy: Miniscript<String, Legacy> = policy.compile()?;
904+
let segwit_policy: Miniscript<String, Segwitv0> = policy.compile()?;
905+
let taproot_policy: Miniscript<String, Tap> = policy.compile()?;
906+
907+
let mut r = None;
908+
let mut shorten_descriptor = None;
905909

906910
let descriptor = match script_type.as_str() {
907911
"sh" => Descriptor::new_sh(legacy_policy),
908912
"wsh" => Descriptor::new_wsh(segwit_policy),
909913
"sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
910914
"tr" => {
911-
// For tr descriptors, we use a well-known unspendable key (NUMS point).
912-
// This ensures the key path is effectively disabled and only script path can be used.
913-
// See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
915+
// For tr descriptors, we use a randomized unspendable key (H + rG).
916+
// This improves privacy by preventing observers from determining if key path spending is disabled.
917+
// See BIP-341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
918+
919+
let secp = Secp256k1::new();
920+
let r_secret = SecretKey::new(&mut rand::thread_rng());
921+
r = Some(r_secret.display_secret().to_string());
914922

915-
let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
916-
.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
923+
let nums_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)?;
924+
let nums_point = PublicKey::from_x_only_public_key(nums_key, Parity::Even);
925+
926+
let internal_key_point = nums_point.add_exp_tweak(&secp, &Scalar::from(r_secret))?;
927+
let (xonly_internal_key, _) = internal_key_point.x_only_public_key();
917928

918929
let tree = TapTree::Leaf(Arc::new(taproot_policy));
919-
Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
930+
931+
shorten_descriptor = Some(Descriptor::new_tr(
932+
shorten(xonly_internal_key, 4, 4),
933+
Some(tree.clone()),
934+
)?);
935+
936+
Descriptor::new_tr(xonly_internal_key.to_string(), Some(tree))
920937
}
921938
_ => {
922939
return Err(Error::Generic(
923940
"Invalid script type. Supported types: sh, wsh, sh-wsh, tr".to_string(),
924941
));
925942
}
926943
}?;
944+
927945
if pretty {
928-
let table = vec![vec![
929-
"Descriptor".cell().bold(true),
930-
descriptor.to_string().cell(),
931-
]]
932-
.table()
933-
.display()
934-
.map_err(|e| Error::Generic(e.to_string()))?;
946+
let descriptor = shorten_descriptor.unwrap_or(descriptor);
947+
948+
let mut rows = vec![vec!["Descriptor".cell().bold(true), descriptor.cell()]];
949+
950+
if let Some(r_value) = &r {
951+
rows.push(vec!["r".cell().bold(true), shorten(r_value, 4, 4).cell()]);
952+
}
953+
954+
let table = rows
955+
.table()
956+
.display()
957+
.map_err(|e| Error::Generic(e.to_string()))?;
958+
935959
Ok(format!("{table}"))
936960
} else {
937-
Ok(serde_json::to_string_pretty(
938-
&json!({"descriptor": descriptor.to_string()}),
939-
)?)
961+
let mut output = json!({"descriptor": descriptor});
962+
if let Some(r_value) = r {
963+
output["r"] = json!(r_value);
964+
}
965+
Ok(serde_json::to_string_pretty(&output)?)
940966
}
941967
}
942968

0 commit comments

Comments
 (0)