From: Vadim Anufriev Date: Fri, 21 Aug 2026 11:21:27 +0000 (+0400) Subject: fix(compile): compile only for the requested script context X-Git-Url: http://internal-gitweb-vhost/-client-interface/src/example_cli/bitcoin/struct.Transaction.html?a=commitdiff_plain;h=eadbdc7db8b80728d8dfd2dafadf6f4f901afb16;p=bdk-cli fix(compile): compile only for the requested script context Compiling for all three contexts let the narrowest one reject a policy that is valid for the requested type: a 9-of-16 multisig, fine as taproot multi_a, failed even for --type tr because legacy hit the 520-byte consensus limit on script elements. The same policy also panicked for --type wsh, which the test now covers. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index f2bf050..b68802b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. - Fixed routing electrum and esplora traffic through configured socks5 proxy - Routed compact filter (cbf) traffic through the configured SOCKS5 proxy - Rejected `--proxy` on the `rpc` backend, and unsupported proxy options (`--proxy_auth`, `--timeout`) on the `rpc` and `cbf` backends, instead of silently ignoring them +- Fixed `compile` rejecting policies that are valid for the requested script type ## [4.0.0] diff --git a/src/handlers/descriptor.rs b/src/handlers/descriptor.rs index 15d2d59..e35fcea 100644 --- a/src/handlers/descriptor.rs +++ b/src/handlers/descriptor.rs @@ -20,7 +20,7 @@ use { key::{Parity, rand}, secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}, }, - miniscript::{Descriptor, Miniscript, descriptor::TapTree, policy::Concrete}, + miniscript::{Descriptor, descriptor::TapTree, policy::Concrete}, }, std::{str::FromStr, sync::Arc}, }; @@ -83,22 +83,14 @@ impl AppCommand> for CompileCommand { let policy: Concrete = Concrete::from_str(&self.policy) .map_err(|e| Error::Generic(format!("Invalid policy: {e}")))?; - let legacy_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; - let segwit_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; - let taproot_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; - let mut r = None; + // Compile per branch, not once up front: the contexts have different script + // limits, and the narrowest one would reject policies valid for the requested type. let descriptor = match self.script_type.as_str() { - "sh" => Descriptor::new_sh(legacy_policy), - "wsh" => Descriptor::new_wsh(segwit_policy), - "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy), + "sh" => Descriptor::new_sh(policy.compile()?), + "wsh" => Descriptor::new_wsh(policy.compile()?), + "sh-wsh" => Descriptor::new_sh_wsh(policy.compile()?), "tr" => { // Use a randomized unspendable internal key (H + rG) instead of a fixed NUMS // point. This improves privacy by preventing observers from determining whether @@ -118,7 +110,7 @@ impl AppCommand> for CompileCommand { .map_err(|e| Error::Generic(format!("Failed to tweak NUMS key: {e}")))?; let (xonly_internal_key, _) = internal_key_point.x_only_public_key(); - let tree = TapTree::Leaf(Arc::new(taproot_policy)); + let tree = TapTree::Leaf(Arc::new(policy.compile()?)); Descriptor::new_tr(xonly_internal_key.to_string(), Some(tree)) } _ => { diff --git a/tests/integration/init.rs b/tests/integration/init.rs index 4f57b46..5244d99 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -197,6 +197,32 @@ mod test_compile { .stdout(predicate::str::contains("wsh(")); } + /// A policy can be valid for tr or wsh type and still exceed the limits of the + /// legacy context, whose 520-byte redeemScript cap does not apply to it. + #[test] + fn test_compile_policy_beyond_legacy_limits() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + let keys = (1..=20) + .map(|i| format!("pk(K{i:02})")) + .collect::>() + .join(","); + let policy = format!("thresh(2,{keys})"); + + // compile tr + cli.cmd("compile", &[&policy, "--type", "tr"]) + .assert() + .success() + .stdout(predicate::str::contains("tr(")); + + // compile wsh + cli.cmd("compile", &[&policy, "--type", "wsh"]) + .assert() + .success() + .stdout(predicate::str::contains("wsh(")); + } + #[test] fn test_compile_invalid_policy() { let temp_dir = TempDir::new().unwrap();