Skip to content
Merged
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 @@ -94,7 +94,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
165 changes: 137 additions & 28 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,20 @@ use bdk_wallet::bitcoin::{
};
use bdk_wallet::chain::ChainPosition;
use bdk_wallet::descriptor::Segwitv0;
use bdk_wallet::keys::{
DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey,
bip39::WordCount,
};
use bdk_wallet::miniscript::miniscript;
#[cfg(feature = "sqlite")]
use bdk_wallet::rusqlite::Connection;
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
#[cfg(feature = "compiler")]
use bdk_wallet::{
descriptor::{Descriptor, Legacy, Miniscript},
miniscript::policy::Concrete,
miniscript::{Tap, descriptor::TapTree, policy::Concrete},
};
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};

use bdk_wallet::keys::{
DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey,
bip39::WordCount,
};
use bdk_wallet::miniscript::miniscript;
use serde_json::json;
use std::collections::BTreeMap;
#[cfg(any(feature = "electrum", feature = "esplora"))]
Expand All @@ -53,14 +52,16 @@ use std::convert::TryFrom;
#[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))]
use std::io::Write;
use std::str::FromStr;
#[cfg(any(feature = "redb", feature = "compiler"))]
use std::sync::Arc;

#[cfg(feature = "electrum")]
use crate::utils::BlockchainClient::Electrum;
#[cfg(feature = "cbf")]
use bdk_kyoto::{Info, LightClient};
#[cfg(feature = "compiler")]
use bdk_wallet::bitcoin::XOnlyPublicKey;
use bdk_wallet::bitcoin::base64::prelude::*;
#[cfg(feature = "redb")]
use std::sync::Arc;
#[cfg(feature = "cbf")]
use tokio::select;
#[cfg(any(
Expand All @@ -82,6 +83,10 @@ use {
bdk_wallet::chain::{BlockId, CanonicalizationParams, CheckPoint},
};

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

/// Execute an offline wallet sub-command
///
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
Expand Down Expand Up @@ -606,9 +611,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 @@ -687,7 +692,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 @@ -813,7 +818,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 @@ -833,7 +838,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 @@ -874,8 +879,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 @@ -1022,12 +1026,30 @@ pub(crate) fn handle_compile_subcommand(
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),
_ => panic!("Invalid type"),
"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 xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
Copy link
Member

Choose a reason for hiding this comment

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

I believe technically the NUMS_UNSPENDABLE_KEY_HEX is not supposed to be used as is (privacy concerns), though for now it's fine. Per BIP-0431, should add an issue to do the following:

"In order to avoid leaking the information that key path spending is not possible it is recommended to pick a fresh integer r in the range 0...n-1 uniformly at random and use H + rG as internal key. It is possible to prove that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created."

.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;

let tree = TapTree::Leaf(Arc::new(taproot_policy));
Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
}
_ => {
return Err(Error::Generic(
"Invalid script type. Supported types: sh, wsh, sh-wsh, tr".to_string(),
));
}
}?;
if pretty {
let table = vec![vec![
Expand Down Expand Up @@ -1331,21 +1353,20 @@ fn readline() -> Result<String, Error> {
Ok(buffer)
}

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
#[cfg(test)]
mod test {
use bdk_wallet::bitcoin::Psbt;

use super::is_final;
use std::str::FromStr;

#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
#[test]
fn test_psbt_is_final() {
use super::is_final;
use bdk_wallet::bitcoin::Psbt;
use std::str::FromStr;

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();
assert!(is_final(&unsigned_psbt).is_err());

Expand All @@ -1355,4 +1376,92 @@ 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::{NUMS_UNSPENDABLE_KEY_HEX, handle_compile_subcommand};
use bdk_wallet::bitcoin::Network;

// Expected taproot descriptors with checksums (using NUMS key from constant)
let expected_pk_a = format!("tr({},pk(A))#a2mlskt0", NUMS_UNSPENDABLE_KEY_HEX);
let expected_and_ab = format!(
"tr({},and_v(v:pk(A),pk(B)))#sfplm6kv",
NUMS_UNSPENDABLE_KEY_HEX
);

// Test simple pk policy compilation to taproot
let result = handle_compile_subcommand(
Network::Testnet,
"pk(A)".to_string(),
"tr".to_string(),
false,
);
assert!(result.is_ok());
let json_string = result.unwrap();
let json_result: serde_json::Value = serde_json::from_str(&json_string).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(),
false,
);
assert!(result.is_ok());
let json_string = result.unwrap();
let json_result: serde_json::Value = serde_json::from_str(&json_string).unwrap();
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
assert_eq!(descriptor, expected_and_ab);
}

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

// Test invalid policy syntax
let result = handle_compile_subcommand(
Network::Testnet,
"invalid_policy".to_string(),
"tr".to_string(),
false,
);
assert!(result.is_err());

// Test invalid script type
let result = handle_compile_subcommand(
Network::Testnet,
"pk(A)".to_string(),
"invalid_type".to_string(),
false,
);
assert!(result.is_err());

// Test empty policy
let result =
handle_compile_subcommand(Network::Testnet, "".to_string(), "tr".to_string(), false);
assert!(result.is_err());

// Test malformed policy with unmatched parentheses
let result = handle_compile_subcommand(
Network::Testnet,
"pk(A".to_string(),
"tr".to_string(),
false,
);
assert!(result.is_err());

// Test policy with unknown function
let result = handle_compile_subcommand(
Network::Testnet,
"unknown_func(A)".to_string(),
"tr".to_string(),
false,
);
assert!(result.is_err());
}
}
4 changes: 2 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,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 @@ -355,7 +355,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