diff --git a/CHANGELOG.md b/CHANGELOG.md index 79da994d..a25e3c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. ## [Unreleased] +- Fixed `compile` rejecting policies that are valid for the requested script type + ## [4.0.0] - Added persistance to existing async payjoin integration diff --git a/src/handlers/descriptor.rs b/src/handlers/descriptor.rs index 15d2d59a..e35fcea3 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 17fdfce2..0506f926 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -203,6 +203,26 @@ mod test_compile { .stdout(predicate::str::contains("wsh(")); } + /// A policy can be valid for taproot and still exceed the limits of the + /// legacy context, whose 520-byte redeemScript cap does not apply to it. + /// Compiling for `tr` must not be blocked by the other contexts. + #[test] + fn test_compile_taproot_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})"); + + cli.cmd("compile", &[&policy, "--type", "tr"]) + .assert() + .success() + .stdout(predicate::str::contains("tr(")); + } + #[test] fn test_compile_invalid_policy() { let temp_dir = TempDir::new().unwrap();