Skip to content

Commit a821a79

Browse files
committed
Merge #208: Update compile command to support creating taproot descriptors
3bcec61 feat(compile): add tr option for compile cmd (Vadim Anufriev) Pull request description: ### Description Resolves #204. <!-- Describe the purpose of this PR, what's being adding and/or fixed --> ### Notes to the reviewers For creating the tr descriptor, I used the NUMS pubkey proposed in [BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs). There is [discussion](rust-bitcoin/rust-bitcoin#1322) about adding NUMS key to `rust-bicoin`, we can use it in the future from there. Also there is [BIP draft](bitcoin/bips#1746) for new descriptor key expression `unspendable()` for exacly this use case - we will simply use descriptor `tr(unspendable(), TREE)`. ## Changelog notice <!-- Notice the release manager should include in the release tag message changelog --> <!-- See https://keepachangelog.com/en/1.0.0/ for examples --> ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk-cli/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [ ] I've added docs for the new feature * [ ] I've updated `CHANGELOG.md` ACKs for top commit: notmandatory: utACK 3bcec61 Tree-SHA512: bd20091379fa957629921333e6a94f7a9252280b7252120b6104a11272b7ea12326062c1737cc69a11e4dd766f82b6f1d764d898931c062a57ea2f1448ed5ecd
2 parents d7d38f2 + 3bcec61 commit a821a79

File tree

4 files changed

+143
-34
lines changed

4 files changed

+143
-34
lines changed

src/commands.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub enum CliSubCommand {
9494
#[arg(env = "POLICY", required = true, index = 1)]
9595
policy: String,
9696
/// Sets the script type used to embed the compiled policy.
97-
#[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh"]
97+
#[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh", "tr"]
9898
)]
9999
script_type: String,
100100
},

src/handlers.rs

Lines changed: 137 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,20 @@ use bdk_wallet::bitcoin::{
3030
};
3131
use bdk_wallet::chain::ChainPosition;
3232
use bdk_wallet::descriptor::Segwitv0;
33+
use bdk_wallet::keys::{
34+
DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey,
35+
bip39::WordCount,
36+
};
37+
use bdk_wallet::miniscript::miniscript;
3338
#[cfg(feature = "sqlite")]
3439
use bdk_wallet::rusqlite::Connection;
3540
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
3641
#[cfg(feature = "compiler")]
3742
use bdk_wallet::{
3843
descriptor::{Descriptor, Legacy, Miniscript},
39-
miniscript::policy::Concrete,
44+
miniscript::{Tap, descriptor::TapTree, policy::Concrete},
4045
};
4146
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
42-
43-
use bdk_wallet::keys::{
44-
DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey,
45-
bip39::WordCount,
46-
};
47-
use bdk_wallet::miniscript::miniscript;
4847
use serde_json::json;
4948
use std::collections::BTreeMap;
5049
#[cfg(any(feature = "electrum", feature = "esplora"))]
@@ -53,14 +52,16 @@ use std::convert::TryFrom;
5352
#[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))]
5453
use std::io::Write;
5554
use std::str::FromStr;
55+
#[cfg(any(feature = "redb", feature = "compiler"))]
56+
use std::sync::Arc;
5657

5758
#[cfg(feature = "electrum")]
5859
use crate::utils::BlockchainClient::Electrum;
5960
#[cfg(feature = "cbf")]
6061
use bdk_kyoto::{Info, LightClient};
62+
#[cfg(feature = "compiler")]
63+
use bdk_wallet::bitcoin::XOnlyPublicKey;
6164
use bdk_wallet::bitcoin::base64::prelude::*;
62-
#[cfg(feature = "redb")]
63-
use std::sync::Arc;
6465
#[cfg(feature = "cbf")]
6566
use tokio::select;
6667
#[cfg(any(
@@ -82,6 +83,10 @@ use {
8283
bdk_wallet::chain::{BlockId, CanonicalizationParams, CheckPoint},
8384
};
8485

86+
#[cfg(feature = "compiler")]
87+
const NUMS_UNSPENDABLE_KEY_HEX: &str =
88+
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
89+
8590
/// Execute an offline wallet sub-command
8691
///
8792
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
@@ -606,9 +611,9 @@ pub(crate) async fn handle_online_wallet_subcommand(
606611
let mut once = HashSet::<KeychainKind>::new();
607612
move |k, spk_i, _| {
608613
if once.insert(k) {
609-
print!("\nScanning keychain [{:?}]", k);
614+
print!("\nScanning keychain [{k:?}]");
610615
}
611-
print!(" {:<3}", spk_i);
616+
print!(" {spk_i:<3}");
612617
stdout.flush().expect("must flush");
613618
}
614619
});
@@ -687,7 +692,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
687692
.start_sync_with_revealed_spks()
688693
.inspect(|item, progress| {
689694
let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
690-
eprintln!("[ SCANNING {:03.0}% ] {}", pc, item);
695+
eprintln!("[ SCANNING {pc:03.0}% ] {item}");
691696
});
692697
match client {
693698
#[cfg(feature = "electrum")]
@@ -813,7 +818,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
813818

814819
let subscriber = tracing_subscriber::FmtSubscriber::new();
815820
tracing::subscriber::set_global_default(subscriber)
816-
.map_err(|e| Error::Generic(format!("SetGlobalDefault error: {}", e)))?;
821+
.map_err(|e| Error::Generic(format!("SetGlobalDefault error: {e}")))?;
817822

818823
tokio::task::spawn(async move { node.run().await });
819824
tokio::task::spawn(async move {
@@ -833,7 +838,7 @@ pub(crate) async fn handle_online_wallet_subcommand(
833838
let txid = tx.compute_txid();
834839
requester
835840
.broadcast_random(tx.clone())
836-
.map_err(|e| Error::Generic(format!("{}", e)))?;
841+
.map_err(|e| Error::Generic(format!("{e}")))?;
837842
tokio::time::timeout(tokio::time::Duration::from_secs(30), async move {
838843
while let Some(info) = info_subscriber.recv().await {
839844
match info {
@@ -874,8 +879,7 @@ pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> {
874879
let psbt_inputs = psbt.inputs.len();
875880
if unsigned_tx_inputs != psbt_inputs {
876881
return Err(Error::Generic(format!(
877-
"Malformed PSBT, {} unsigned tx inputs and {} psbt inputs.",
878-
unsigned_tx_inputs, psbt_inputs
882+
"Malformed PSBT, {unsigned_tx_inputs} unsigned tx inputs and {psbt_inputs} psbt inputs."
879883
)));
880884
}
881885
let sig_count = psbt.inputs.iter().fold(0, |count, input| {
@@ -1022,12 +1026,30 @@ pub(crate) fn handle_compile_subcommand(
10221026
let segwit_policy: Miniscript<String, Segwitv0> = policy
10231027
.compile()
10241028
.map_err(|e| Error::Generic(e.to_string()))?;
1029+
let taproot_policy: Miniscript<String, Tap> = policy
1030+
.compile()
1031+
.map_err(|e| Error::Generic(e.to_string()))?;
10251032

10261033
let descriptor = match script_type.as_str() {
10271034
"sh" => Descriptor::new_sh(legacy_policy),
10281035
"wsh" => Descriptor::new_wsh(segwit_policy),
10291036
"sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
1030-
_ => panic!("Invalid type"),
1037+
"tr" => {
1038+
// For tr descriptors, we use a well-known unspendable key (NUMS point).
1039+
// This ensures the key path is effectively disabled and only script path can be used.
1040+
// See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
1041+
1042+
let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
1043+
.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
1044+
1045+
let tree = TapTree::Leaf(Arc::new(taproot_policy));
1046+
Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
1047+
}
1048+
_ => {
1049+
return Err(Error::Generic(
1050+
"Invalid script type. Supported types: sh, wsh, sh-wsh, tr".to_string(),
1051+
));
1052+
}
10311053
}?;
10321054
if pretty {
10331055
let table = vec![vec![
@@ -1331,21 +1353,20 @@ fn readline() -> Result<String, Error> {
13311353
Ok(buffer)
13321354
}
13331355

1334-
#[cfg(any(
1335-
feature = "electrum",
1336-
feature = "esplora",
1337-
feature = "cbf",
1338-
feature = "rpc"
1339-
))]
13401356
#[cfg(test)]
13411357
mod test {
1342-
use bdk_wallet::bitcoin::Psbt;
1343-
1344-
use super::is_final;
1345-
use std::str::FromStr;
1346-
1358+
#[cfg(any(
1359+
feature = "electrum",
1360+
feature = "esplora",
1361+
feature = "cbf",
1362+
feature = "rpc"
1363+
))]
13471364
#[test]
13481365
fn test_psbt_is_final() {
1366+
use super::is_final;
1367+
use bdk_wallet::bitcoin::Psbt;
1368+
use std::str::FromStr;
1369+
13491370
let unsigned_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+MeDBDS6ZSEAACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==").unwrap();
13501371
assert!(is_final(&unsigned_psbt).is_err());
13511372

@@ -1355,4 +1376,92 @@ mod test {
13551376
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();
13561377
assert!(is_final(&full_signed_psbt).is_ok());
13571378
}
1379+
1380+
#[cfg(feature = "compiler")]
1381+
#[test]
1382+
fn test_compile_taproot() {
1383+
use super::{NUMS_UNSPENDABLE_KEY_HEX, handle_compile_subcommand};
1384+
use bdk_wallet::bitcoin::Network;
1385+
1386+
// Expected taproot descriptors with checksums (using NUMS key from constant)
1387+
let expected_pk_a = format!("tr({},pk(A))#a2mlskt0", NUMS_UNSPENDABLE_KEY_HEX);
1388+
let expected_and_ab = format!(
1389+
"tr({},and_v(v:pk(A),pk(B)))#sfplm6kv",
1390+
NUMS_UNSPENDABLE_KEY_HEX
1391+
);
1392+
1393+
// Test simple pk policy compilation to taproot
1394+
let result = handle_compile_subcommand(
1395+
Network::Testnet,
1396+
"pk(A)".to_string(),
1397+
"tr".to_string(),
1398+
false,
1399+
);
1400+
assert!(result.is_ok());
1401+
let json_string = result.unwrap();
1402+
let json_result: serde_json::Value = serde_json::from_str(&json_string).unwrap();
1403+
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
1404+
assert_eq!(descriptor, expected_pk_a);
1405+
1406+
// Test more complex policy
1407+
let result = handle_compile_subcommand(
1408+
Network::Testnet,
1409+
"and(pk(A),pk(B))".to_string(),
1410+
"tr".to_string(),
1411+
false,
1412+
);
1413+
assert!(result.is_ok());
1414+
let json_string = result.unwrap();
1415+
let json_result: serde_json::Value = serde_json::from_str(&json_string).unwrap();
1416+
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
1417+
assert_eq!(descriptor, expected_and_ab);
1418+
}
1419+
1420+
#[cfg(feature = "compiler")]
1421+
#[test]
1422+
fn test_compile_invalid_cases() {
1423+
use super::handle_compile_subcommand;
1424+
use bdk_wallet::bitcoin::Network;
1425+
1426+
// Test invalid policy syntax
1427+
let result = handle_compile_subcommand(
1428+
Network::Testnet,
1429+
"invalid_policy".to_string(),
1430+
"tr".to_string(),
1431+
false,
1432+
);
1433+
assert!(result.is_err());
1434+
1435+
// Test invalid script type
1436+
let result = handle_compile_subcommand(
1437+
Network::Testnet,
1438+
"pk(A)".to_string(),
1439+
"invalid_type".to_string(),
1440+
false,
1441+
);
1442+
assert!(result.is_err());
1443+
1444+
// Test empty policy
1445+
let result =
1446+
handle_compile_subcommand(Network::Testnet, "".to_string(), "tr".to_string(), false);
1447+
assert!(result.is_err());
1448+
1449+
// Test malformed policy with unmatched parentheses
1450+
let result = handle_compile_subcommand(
1451+
Network::Testnet,
1452+
"pk(A".to_string(),
1453+
"tr".to_string(),
1454+
false,
1455+
);
1456+
assert!(result.is_err());
1457+
1458+
// Test policy with unknown function
1459+
let result = handle_compile_subcommand(
1460+
Network::Testnet,
1461+
"unknown_func(A)".to_string(),
1462+
"tr".to_string(),
1463+
false,
1464+
);
1465+
assert!(result.is_err());
1466+
}
13581467
}

src/utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ pub async fn sync_kyoto_client(wallet: &mut Wallet, client: Box<LightClient>) ->
338338

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

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

360360
tracing::info!(
361361
"Chain tip: {}, Transactions: {}, Balance: {}",

tests/integration.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,19 @@ mod test {
127127
node_datadir,
128128
};
129129

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

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

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

144144
bdk_cli.recv_desc = Some(bdk_recv_desc);
145145
bdk_cli.chang_desc = Some(bdk_chng_desc);

0 commit comments

Comments
 (0)