]> Untitled Git - bdk-cli/commitdiff
fix(compile): compile only for the requested script context
authorVadim Anufriev <m@vaan.io>
Fri, 21 Aug 2026 11:21:27 +0000 (15:21 +0400)
committerVadim Anufriev <m@vaan.io>
Mon, 5 Oct 2026 18:38:24 +0000 (22:38 +0400)
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.

CHANGELOG.md
src/handlers/descriptor.rs
tests/integration/init.rs

index f2bf05039034f94029502befbd5372fdbdf4dd72..b68802b31a5772bb3d500a3b65b4771208bebe38 100644 (file)
@@ -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]
 
index 15d2d59a008b52e280d01ae723bb4d2e08503069..e35fcea3a3554a654f9bdb1f3313e631cea99831 100644 (file)
@@ -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<AppContext<Init>> for CompileCommand {
         let policy: Concrete<String> = Concrete::from_str(&self.policy)
             .map_err(|e| Error::Generic(format!("Invalid policy: {e}")))?;
 
-        let legacy_policy: Miniscript<String, bdk_wallet::miniscript::Legacy> = policy
-            .compile()
-            .map_err(|e| Error::Generic(e.to_string()))?;
-        let segwit_policy: Miniscript<String, bdk_wallet::miniscript::Segwitv0> = policy
-            .compile()
-            .map_err(|e| Error::Generic(e.to_string()))?;
-        let taproot_policy: Miniscript<String, bdk_wallet::miniscript::Tap> = 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<AppContext<Init>> 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))
             }
             _ => {
index 4f57b4602bb4e7e6a3d679836682828dbdffc65e..5244d9969f643b2420a095f85838b00f3852c30d 100644 (file)
@@ -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::<Vec<_>>()
+            .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();