-
Notifications
You must be signed in to change notification settings - Fork 79
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"))] | ||
|
@@ -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( | ||
|
@@ -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`]. | ||
|
@@ -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"); | ||
} | ||
}); | ||
|
@@ -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")] | ||
|
@@ -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 { | ||
|
@@ -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 { | ||
|
@@ -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| { | ||
|
@@ -1022,12 +1026,31 @@ 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 | ||
|
||
// Validate that the NUMS key is a valid XOnlyPublicKey | ||
XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX) | ||
.map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?; | ||
|
||
let tree = TapTree::Leaf(Arc::new(taproot_policy)); | ||
Descriptor::new_tr(NUMS_UNSPENDABLE_KEY_HEX.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![ | ||
|
@@ -1355,4 +1378,94 @@ 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")] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These You can move the backend gate to only apply to the #[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "cbf",
feature = "rpc"
))]
#[test]
fn test_psbt_is_final() {} Put the imports too under their respective gates. |
||
#[test] | ||
fn test_compile_taproot() { | ||
use super::{NUMS_UNSPENDABLE_KEY_HEX, handle_compile_subcommand}; | ||
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_HEX)); | ||
assert!(EXPECTED_AND_AB.contains(NUMS_UNSPENDABLE_KEY_HEX)); | ||
Comment on lines
+1394
to
+1395
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering how much value these assert statements add when we are in control of the expected values ? |
||
|
||
// 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); | ||
} | ||
tvpeter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#[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()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought you will use the parsed value. Something like: