Skip to content

Update compile command to support creating taproot descriptors #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub enum CliSubCommand {
#[arg(env = "POLICY", required = true, index = 1)]
policy: String,
/// Sets the script type used to embed the compiled policy.
#[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh"]
#[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh", "tr"]
)]
script_type: String,
},
Expand Down
72 changes: 62 additions & 10 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use bdk_wallet::rusqlite::Connection;
#[cfg(feature = "compiler")]
use bdk_wallet::{
descriptor::{Descriptor, Legacy, Miniscript},
miniscript::policy::Concrete,
miniscript::{policy::Concrete, Tap},
};
use bdk_wallet::{KeychainKind, SignOptions, Wallet};

Expand Down Expand Up @@ -72,6 +72,12 @@ use {
bdk_wallet::chain::{BlockId, CanonicalizationParams, CheckPoint},
};

/// Well-known unspendable key used for Taproot descriptors when only script path is intended.
/// This is a NUMS (Nothing Up My Sleeve) point that ensures the key path cannot be used.
Comment on lines +75 to +76
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you provided detailed comments in the usage block, this documentation seems redundant.

#[cfg(feature = "compiler")]
const NUMS_UNSPENDABLE_KEY: &str =
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";

/// Execute an offline wallet sub-command
///
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
Expand Down Expand Up @@ -355,9 +361,9 @@ pub(crate) async fn handle_online_wallet_subcommand(
let mut once = HashSet::<KeychainKind>::new();
move |k, spk_i, _| {
if once.insert(k) {
print!("\nScanning keychain [{:?}]", k);
print!("\nScanning keychain [{k:?}]");
}
print!(" {:<3}", spk_i);
print!(" {spk_i:<3}");
stdout.flush().expect("must flush");
}
});
Expand Down Expand Up @@ -432,7 +438,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
.start_sync_with_revealed_spks()
.inspect(|item, progress| {
let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
eprintln!("[ SCANNING {:03.0}% ] {}", pc, item);
eprintln!("[ SCANNING {pc:03.0}% ] {item}");
});
match client {
#[cfg(feature = "electrum")]
Expand Down Expand Up @@ -558,7 +564,7 @@ pub(crate) async fn handle_online_wallet_subcommand(

let subscriber = tracing_subscriber::FmtSubscriber::new();
tracing::subscriber::set_global_default(subscriber)
.map_err(|e| Error::Generic(format!("SetGlobalDefault error: {}", e)))?;
.map_err(|e| Error::Generic(format!("SetGlobalDefault error: {e}")))?;

tokio::task::spawn(async move { node.run().await });
tokio::task::spawn(async move {
Expand All @@ -578,7 +584,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
let txid = tx.compute_txid();
requester
.broadcast_random(tx.clone())
.map_err(|e| Error::Generic(format!("{}", e)))?;
.map_err(|e| Error::Generic(format!("{e}")))?;
tokio::time::timeout(tokio::time::Duration::from_secs(30), async move {
while let Some(info) = info_subscriber.recv().await {
match info {
Expand Down Expand Up @@ -619,8 +625,7 @@ pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> {
let psbt_inputs = psbt.inputs.len();
if unsigned_tx_inputs != psbt_inputs {
return Err(Error::Generic(format!(
"Malformed PSBT, {} unsigned tx inputs and {} psbt inputs.",
unsigned_tx_inputs, psbt_inputs
"Malformed PSBT, {unsigned_tx_inputs} unsigned tx inputs and {psbt_inputs} psbt inputs."
)));
}
let sig_count = psbt.inputs.iter().fold(0, |count, input| {
Expand Down Expand Up @@ -714,18 +719,31 @@ pub(crate) fn handle_compile_subcommand(
policy: String,
script_type: String,
) -> Result<serde_json::Value, Error> {
use bdk_wallet::miniscript::descriptor::TapTree;
use std::sync::Arc;

let policy = Concrete::<String>::from_str(policy.as_str())?;
let legacy_policy: Miniscript<String, Legacy> = policy
.compile()
.map_err(|e| Error::Generic(e.to_string()))?;
let segwit_policy: Miniscript<String, Segwitv0> = policy
.compile()
.map_err(|e| Error::Generic(e.to_string()))?;
let taproot_policy: Miniscript<String, Tap> = policy
.compile()
.map_err(|e| Error::Generic(e.to_string()))?;

let descriptor = match script_type.as_str() {
"sh" => Descriptor::new_sh(legacy_policy),
"wsh" => Descriptor::new_wsh(segwit_policy),
"sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
"tr" => {
// For tr descriptors, we use a well-known unspendable key (NUMS point).
// This ensures the key path is effectively disabled and only script path can be used.
// See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
let tree = TapTree::Leaf(Arc::new(taproot_policy));
Descriptor::new_tr(NUMS_UNSPENDABLE_KEY.to_string(), Some(tree))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the NUMS_UNSPENDABLE_KEY represents XonlyPublicKey, so it is better to parse it into an XonlyPublicKey and not used as a string (since strings are used for key placeholders) to avoid type mismatch.

}
_ => panic!("Invalid type"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you help and change from panic to a graceful handling of the error?

}?;

Expand All @@ -747,7 +765,6 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
wallet_opts,
subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
} => {
let network = cli_opts.network;
let home_dir = prepare_home_dir(cli_opts.datadir)?;
let wallet_name = &wallet_opts.wallet;
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
Expand Down Expand Up @@ -948,7 +965,7 @@ async fn respond(
};
if let Some(value) = response {
let value = serde_json::to_string_pretty(&value).map_err(|e| e.to_string())?;
writeln!(std::io::stdout(), "{}", value).map_err(|e| e.to_string())?;
writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?;
std::io::stdout().flush().map_err(|e| e.to_string())?;
Ok(false)
} else {
Expand Down Expand Up @@ -995,4 +1012,39 @@ mod test {
let full_signed_psbt = Psbt::from_str("cHNidP8BAIkBAAAAASWJHzxzyVORV/C3lAynKHVVL7+Rw7/Jj8U9fuvD24olAAAAAAD+////AiBOAAAAAAAAIgAgLzY9yE4jzTFJnHtTjkc+rFAtJ9NB7ENFQ1xLYoKsI1cfqgKVAAAAACIAIFsbWgDeLGU8EA+RGwBDIbcv4gaGG0tbEIhDvwXXa/E7LwEAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BALLAAD/////AgD5ApUAAAAAIgAgWxtaAN4sZTwQD5EbAEMhty/iBoYbS1sQiEO/Bddr8TsAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErAPkClQAAAAAiACBbG1oA3ixlPBAPkRsAQyG3L+IGhhtLWxCIQ78F12vxOwEFR1IhA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDIQLKhV/gEZYmlsQXnsL5/Uqv5Y8O31tmWW1LQqIBkiqzCVKuIgYCyoVf4BGWJpbEF57C+f1Kr+WPDt9bZlltS0KiAZIqswkEboH3lCIGA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDBDS6ZSEBBwABCNsEAEgwRQIhAJzT6busDV9h12M/LNquZ17oOHFn7whg90kh9gjSpvshAiBEDu/1EYVD7BqJJzExPhq2CX/Vsap/ULLjfRRo99nEKQFHMEQCIGoFCvJ2zPB7PCpznh4+1jsY03kMie49KPoPDdr7/T9TAiB3jV7wzR9BH11FSbi+8U8gSX95PrBlnp1lOBgTUIUw3QFHUiED8lXZT/Sldb6I/j1ByxiKUS+RkR3imGYMzydXzAL4x4MhAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJUq4AACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==").unwrap();
assert!(is_final(&full_signed_psbt).is_ok());
}

#[cfg(feature = "compiler")]
#[test]
fn test_compile_taproot() {
use super::{handle_compile_subcommand, NUMS_UNSPENDABLE_KEY};
use bdk_wallet::bitcoin::Network;

// Expected taproot descriptors with checksums
const EXPECTED_PK_A: &str =
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,pk(A))#a2mlskt0";
const EXPECTED_AND_AB: &str = "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,and_v(v:pk(A),pk(B)))#sfplm6kv";

// Verify our test expectations use the same NUMS key
assert!(EXPECTED_PK_A.contains(NUMS_UNSPENDABLE_KEY));
assert!(EXPECTED_AND_AB.contains(NUMS_UNSPENDABLE_KEY));

// Test simple pk policy compilation to taproot
let result =
handle_compile_subcommand(Network::Testnet, "pk(A)".to_string(), "tr".to_string());
assert!(result.is_ok());
let json_result = result.unwrap();
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
assert_eq!(descriptor, EXPECTED_PK_A);

// Test more complex policy
let result = handle_compile_subcommand(
Network::Testnet,
"and(pk(A),pk(B))".to_string(),
"tr".to_string(),
);
assert!(result.is_ok());
let json_result = result.unwrap();
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
assert_eq!(descriptor, EXPECTED_AND_AB);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also add test cases for invalid/invalid policies and other edge cases?

}
6 changes: 3 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ async fn main() {
let cli_opts: CliOpts = CliOpts::parse();

let network = &cli_opts.network;
debug!("network: {:?}", network);
debug!("network: {network:?}");
if network == &Network::Bitcoin {
warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
}

match handle_command(cli_opts).await {
Ok(result) => println!("{}", result),
Ok(result) => println!("{result}"),
Err(e) => {
error!("{}", e);
error!("{e}");
std::process::exit(1);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: Box<LightClient>) ->

let subscriber = tracing_subscriber::FmtSubscriber::new();
tracing::subscriber::set_global_default(subscriber)
.map_err(|e| Error::Generic(format!("SetGlobalDefault error: {}", e)))?;
.map_err(|e| Error::Generic(format!("SetGlobalDefault error: {e}")))?;

tokio::task::spawn(async move { node.run().await });
tokio::task::spawn(async move {
Expand All @@ -354,7 +354,7 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: Box<LightClient>) ->
tracing::info!("Received update: applying to wallet");
wallet
.apply_update(update)
.map_err(|e| Error::Generic(format!("Failed to apply update: {}", e)))?;
.map_err(|e| Error::Generic(format!("Failed to apply update: {e}")))?;

tracing::info!(
"Chain tip: {}, Transactions: {}, Balance: {}",
Expand Down
6 changes: 3 additions & 3 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,19 @@ mod test {
node_datadir,
};

println!("BDK-CLI Config : {:#?}", bdk_cli);
println!("BDK-CLI Config : {bdk_cli:#?}");
let bdk_master_key = bdk_cli.key_exec(&["generate"])?;
let bdk_xprv = get_value(&bdk_master_key, "xprv")?;

let bdk_recv_desc =
bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/0", "--xprv", &bdk_xprv])?;
let bdk_recv_desc = get_value(&bdk_recv_desc, "xprv")?;
let bdk_recv_desc = format!("wpkh({})", bdk_recv_desc);
let bdk_recv_desc = format!("wpkh({bdk_recv_desc})");

let bdk_chng_desc =
bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/1", "--xprv", &bdk_xprv])?;
let bdk_chng_desc = get_value(&bdk_chng_desc, "xprv")?;
let bdk_chng_desc = format!("wpkh({})", bdk_chng_desc);
let bdk_chng_desc = format!("wpkh({bdk_chng_desc})");

bdk_cli.recv_desc = Some(bdk_recv_desc);
bdk_cli.chang_desc = Some(bdk_chng_desc);
Expand Down
Loading