Skip to content

Commit bb6e241

Browse files
committed
feat(compile): add tr option for compile cmd
1 parent 7a71b14 commit bb6e241

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

src/commands.rs

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

src/handlers.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use bdk_wallet::rusqlite::Connection;
3030
#[cfg(feature = "compiler")]
3131
use bdk_wallet::{
3232
descriptor::{Descriptor, Legacy, Miniscript},
33-
miniscript::policy::Concrete,
33+
miniscript::{policy::Concrete, Tap},
3434
};
3535
use bdk_wallet::{KeychainKind, SignOptions, Wallet};
3636

@@ -72,6 +72,12 @@ use {
7272
bdk_wallet::chain::{BlockId, CanonicalizationParams, CheckPoint},
7373
};
7474

75+
/// Well-known unspendable key used for Taproot descriptors when only script path is intended.
76+
/// This is a NUMS (Nothing Up My Sleeve) point that ensures the key path cannot be used.
77+
#[cfg(feature = "compiler")]
78+
const NUMS_UNSPENDABLE_KEY: &str =
79+
"50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
80+
7581
/// Execute an offline wallet sub-command
7682
///
7783
/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
@@ -714,18 +720,31 @@ pub(crate) fn handle_compile_subcommand(
714720
policy: String,
715721
script_type: String,
716722
) -> Result<serde_json::Value, Error> {
723+
use bdk_wallet::miniscript::descriptor::TapTree;
724+
use std::sync::Arc;
725+
717726
let policy = Concrete::<String>::from_str(policy.as_str())?;
718727
let legacy_policy: Miniscript<String, Legacy> = policy
719728
.compile()
720729
.map_err(|e| Error::Generic(e.to_string()))?;
721730
let segwit_policy: Miniscript<String, Segwitv0> = policy
722731
.compile()
723732
.map_err(|e| Error::Generic(e.to_string()))?;
733+
let taproot_policy: Miniscript<String, Tap> = policy
734+
.compile()
735+
.map_err(|e| Error::Generic(e.to_string()))?;
724736

725737
let descriptor = match script_type.as_str() {
726738
"sh" => Descriptor::new_sh(legacy_policy),
727739
"wsh" => Descriptor::new_wsh(segwit_policy),
728740
"sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
741+
"tr" => {
742+
// For tr descriptors, we use a well-known unspendable key (NUMS point).
743+
// This ensures the key path is effectively disabled and only script path can be used.
744+
// See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
745+
let tree = TapTree::Leaf(Arc::new(taproot_policy));
746+
Descriptor::new_tr(NUMS_UNSPENDABLE_KEY.to_string(), Some(tree))
747+
}
729748
_ => panic!("Invalid type"),
730749
}?;
731750

@@ -995,4 +1014,39 @@ mod test {
9951014
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();
9961015
assert!(is_final(&full_signed_psbt).is_ok());
9971016
}
1017+
1018+
#[cfg(feature = "compiler")]
1019+
#[test]
1020+
fn test_compile_taproot() {
1021+
use super::{handle_compile_subcommand, NUMS_UNSPENDABLE_KEY};
1022+
use bdk_wallet::bitcoin::Network;
1023+
1024+
// Expected taproot descriptors with checksums
1025+
const EXPECTED_PK_A: &str =
1026+
"tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,pk(A))#a2mlskt0";
1027+
const EXPECTED_AND_AB: &str = "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,and_v(v:pk(A),pk(B)))#sfplm6kv";
1028+
1029+
// Verify our test expectations use the same NUMS key
1030+
assert!(EXPECTED_PK_A.contains(NUMS_UNSPENDABLE_KEY));
1031+
assert!(EXPECTED_AND_AB.contains(NUMS_UNSPENDABLE_KEY));
1032+
1033+
// Test simple pk policy compilation to taproot
1034+
let result =
1035+
handle_compile_subcommand(Network::Testnet, "pk(A)".to_string(), "tr".to_string());
1036+
assert!(result.is_ok());
1037+
let json_result = result.unwrap();
1038+
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
1039+
assert_eq!(descriptor, EXPECTED_PK_A);
1040+
1041+
// Test more complex policy
1042+
let result = handle_compile_subcommand(
1043+
Network::Testnet,
1044+
"and(pk(A),pk(B))".to_string(),
1045+
"tr".to_string(),
1046+
);
1047+
assert!(result.is_ok());
1048+
let json_result = result.unwrap();
1049+
let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
1050+
assert_eq!(descriptor, EXPECTED_AND_AB);
1051+
}
9981052
}

0 commit comments

Comments
 (0)