From 1a80c39a83b61a1c3b11e1da4172e36598824f58 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sun, 23 Aug 2026 18:10:48 +0000 Subject: [PATCH 01/10] export primitives::positive_f64::NormalizedIterator It feels a little weird to export this at the root but there isn't anywhere else in the public API where it really fits, so just dump it there. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9d07100ad..1d61f3873 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,7 +141,7 @@ pub use crate::miniscript::{hash256, Miniscript}; pub use crate::policy::semantic::MathSyntaxError; use crate::prelude::*; pub use crate::primitives::absolute_locktime::{AbsLockTime, AbsLockTimeError}; -pub use crate::primitives::positive_f64::PositiveF64; +pub use crate::primitives::positive_f64::{NormalizedIterator, PositiveF64}; pub use crate::primitives::relative_locktime::{RelLockTime, RelLockTimeError}; pub use crate::primitives::threshold::{Threshold, ThresholdError}; pub use crate::validation::{Error as ValidationError, ValidationParams}; From 369d90ad635462af397e11740b2bdc5590c978bd Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 19 Jun 2026 21:39:45 +0000 Subject: [PATCH 02/10] greatly boost regression_compiler.rs fuzztest If we want to regression test the Concrete parser, that's fine but belongs in a separate fuzztest (which I may add in a separate PR). But for testing the compiler, we definitely want to be synthesizing policies directly rather than parsing and rejecting. --- fuzz/fuzz_targets/regression_compiler.rs | 123 ++++++++++++++++------- 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/fuzz/fuzz_targets/regression_compiler.rs b/fuzz/fuzz_targets/regression_compiler.rs index 8703e7f3f..ac1cb3fe8 100644 --- a/fuzz/fuzz_targets/regression_compiler.rs +++ b/fuzz/fuzz_targets/regression_compiler.rs @@ -1,52 +1,98 @@ -use descriptor_fuzz::FuzzPk; +use std::sync::Arc; + use honggfuzz::fuzz; -use miniscript::{policy, ParseError, ParseNumError}; +use miniscript::{policy, AbsLockTime, RelLockTime, Threshold}; use old_miniscript::policy as old_policy; -type Policy = policy::Concrete; -type OldPolicy = old_policy::Concrete; +type Policy = policy::Concrete; +type OldPolicy = old_policy::Concrete; fn do_test(data: &[u8]) { - let data_str = String::from_utf8_lossy(data); - match (data_str.parse::(), data_str.parse::()) { + let mut stack = vec![]; + for sl in data.chunks_exact(2) { + let byte = sl[0]; + let extra = sl[1]; + let ext32 = u32::from(extra); + match byte & 15 { + 0 => stack.push(Policy::Unsatisfiable), + 1 => stack.push(Policy::Trivial), + 2 => stack.push(Policy::Key(format!("key_{:02x}", extra))), + 3 => stack.push(Policy::After(AbsLockTime::from_consensus(1 + ext32).unwrap())), + 4 => stack.push(Policy::Older(RelLockTime::from_consensus(1 + ext32).unwrap())), + 5 => stack.push(Policy::Sha256(format!("hash_{:02x}", extra))), + 6 => { + let (r, l) = match (stack.pop(), stack.pop()) { + (Some(r), Some(l)) => (r, l), + _ => return, + }; + stack.push(Policy::And(vec![l.into(), r.into()])); + } + 7 => { + let (r, l) = match (stack.pop(), stack.pop()) { + (Some(r), Some(l)) => (r, l), + _ => return, + }; + + let l_weight = match ext32.try_into() { + Ok(l_weight) => l_weight, + _ => return, + }; + let r_weight = match (ext32 >> 4).try_into() { + Ok(r_weight) => r_weight, + _ => return, + }; + + stack.push(Policy::Or(vec![(l_weight, l.into()), (r_weight, r.into())])); + } + 8 => { + let n = 1 + (extra >> 4); + let k = 1 + (extra % n); + let inner = match (0..n) + .map(|_| stack.pop().map(Arc::new)) + .collect::>>() + { + Some(inner) => inner, + None => return, + }; + let thresh = Threshold::new(k.into(), inner).unwrap(); + stack.push(Policy::Thresh(thresh)) + } + _ => return, + } + if stack.len() > 128 { + return; + }; + } + let new = match stack.pop() { + Some(new) => new, + None => return, + }; + let new_str = new.to_string(); + let old = match new_str.parse::() { + Ok(old) => old, + Err(e) => panic!("new policy {} fails with {}", new_str, e), + }; + + assert_eq!(old.to_string(), new_str, "(left is old, right is new)",); + + let comp = new.compile::(); + let old_comp = old.compile::(); + + match (comp, old_comp) { (Err(_), Err(_)) => {} - (Ok(x), Err(e)) => panic!("new logic parses {} as {:?}, old fails with {}", data_str, x, e), - // These is anew parse error - ( - Err(miniscript::Error::Parse(ParseError::Num(ParseNumError::IllegalZero { .. }))), - Ok(_), - ) => {} + (Ok(x), Err(e)) => { + panic!("new logic compiles {} as {:?}, old fails with {}", new, x, e) + } (Err(e), Ok(x)) => { - panic!("old logic parses {} as {:?}, new fails with {:?}", data_str, x, e) + panic!("old logic compiles {} as {:?}, new fails with {}", new, x, e) } (Ok(new), Ok(old)) => { assert_eq!( old.to_string(), new.to_string(), - "input {} (left is old, right is new)", - data_str + "compiling the policy {} (left is old, right is new)", + new_str ); - - let comp = new.compile::(); - let old_comp = old.compile::(); - - match (comp, old_comp) { - (Err(_), Err(_)) => {} - (Ok(x), Err(e)) => { - panic!("new logic compiles {} as {:?}, old fails with {}", data_str, x, e) - } - (Err(e), Ok(x)) => { - panic!("old logic compiles {} as {:?}, new fails with {}", data_str, x, e) - } - (Ok(new), Ok(old)) => { - assert_eq!( - old.to_string(), - new.to_string(), - "input {} (left is old, right is new)", - data_str - ); - } - } } } } @@ -62,5 +108,8 @@ fn main() { #[cfg(test)] mod tests { #[test] - fn duplicate_crash() { crate::do_test(b"or(0@pk(09),0@TRIVIAL)") } + fn duplicate_crash() { + let v = miniscript::hex::decode_to_vec("0000000000000014000000280100000000010000000000000400000010011100000000000000000000000200000000000115000000000000002d3530303933910100000f0000000000000004010035000072000011007228354077727336667472697669620806727233374903").unwrap(); + crate::do_test(&v); + } } From 46a7f6ed100883d8a431f1f9144d5cebf60e973d Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 15 Jun 2026 13:39:32 +0000 Subject: [PATCH 03/10] compiler: one more small cleanup Gets rid of a couple 'as f64' casts. --- src/policy/compiler.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/policy/compiler.rs b/src/policy/compiler.rs index 530be3d83..6821a4f2f 100644 --- a/src/policy/compiler.rs +++ b/src/policy/compiler.rs @@ -451,14 +451,14 @@ impl CompilerExtData { } } - fn threshold(k: usize, n: usize, mut sub_ck: S) -> Self + fn threshold(thresh: &crate::Threshold, mut sub_ck: S) -> Self where S: FnMut(usize) -> Self, { - let k_over_n = k as f64 / n as f64; + let k_over_n = f64::from(PositiveF64::k_over_n(thresh)); let mut sat_cost = 0.0; let mut dissat_cost = 0.0; - for i in 0..n { + for i in 0..thresh.n() { let sub = sub_ck(i); sat_cost += sub.sat_cost; dissat_cost += sub.dissat_cost.unwrap(); @@ -996,7 +996,6 @@ where best_compilations_or(&mut ret, policy_cache, policy, subs, sat_prob, dissat_prob)?; } Concrete::Thresh(ref thresh) => { - let k = thresh.k(); let n = thresh.n(); let k_over_n = PositiveF64::k_over_n(thresh); @@ -1058,7 +1057,7 @@ where if let Ok(ms) = Miniscript::from_ast(ast) { let ast_ext = AstElemExt { ms: Arc::new(ms), - comp_ext_data: CompilerExtData::threshold(k, n, |i| sub_ext_data[i]), + comp_ext_data: CompilerExtData::threshold(thresh, |i| sub_ext_data[i]), }; insert_wrap!(ast_ext); } From 06dbcf25b516342c30619ffef817c42cbe8c1fc4 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Mon, 15 Jun 2026 20:49:35 +0000 Subject: [PATCH 04/10] rename compiler.rs to compiler/mod.rs --- src/policy/{compiler.rs => compiler/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/policy/{compiler.rs => compiler/mod.rs} (100%) diff --git a/src/policy/compiler.rs b/src/policy/compiler/mod.rs similarity index 100% rename from src/policy/compiler.rs rename to src/policy/compiler/mod.rs From b53b5db8cae551a491cd982ad57036f66ff98645 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 19 Jun 2026 22:39:42 +0000 Subject: [PATCH 05/10] compiler: add a couple of regression tests These policies exercise paths that earlier iterations of this PR introduced bugs to. (The bugs were found by fuzzing.) --- src/policy/compiler/mod.rs | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/policy/compiler/mod.rs b/src/policy/compiler/mod.rs index 6821a4f2f..6e2e25da1 100644 --- a/src/policy/compiler/mod.rs +++ b/src/policy/compiler/mod.rs @@ -1273,6 +1273,49 @@ mod tests { ); } + #[test] + fn compile_output_regression_1() { + let policy = "or(73@and(and(and(or(114@pk(key_28),7@sha256(hash_40)),after(55)),pk(key_69)),pk(key_08)),4@pk(key_33))" + .parse::().unwrap(); + let compilation: AstElemExt<_, Legacy> = + best_t(&mut BTreeMap::new(), &policy, PositiveF64::ONE, None).unwrap(); + + assert_eq!(compilation.ms.to_string(), "andor(pk(key_08),and_v(v:pk(key_69),and_v(or_c(pk(key_28),v:sha256(hash_40)),after(55))),pkh(key_33))"); + assert_eq!(compilation.cost_1d(PositiveF64::ONE, None), 388.09477299559944); + assert_eq!(policy.lift().unwrap().sorted(), compilation.ms.lift().unwrap().sorted()); + } + + #[test] + fn compile_output_regression_2() { + // This policy demonstrates the need to attempt `vc` casts even if the `c` cast is suboptimal. + let policy = "and(or(86@and(TRIVIAL,pk(key_38)),5@pk(key_df)),after(223))" + .parse::() + .unwrap(); + let compilation: AstElemExt<_, Legacy> = + best_t(&mut BTreeMap::new(), &policy, PositiveF64::ONE, None).unwrap(); + + assert_eq!( + compilation.ms.to_string(), + "and_v(vc:or_i(pk_h(key_df),and_v(v:1,pk_k(key_38))),after(223))" + ); + assert_eq!(compilation.cost_1d(PositiveF64::ONE, None), 143.9230769230769); + assert_eq!(policy.lift().unwrap().sorted(), compilation.ms.lift().unwrap().sorted()); + } + + #[test] + fn compile_output_regression_3() { + // This policy demonstrates that you need to try an extra compilation with dissat_prob = None, + // then l/u/d/j-wrap that, when inserting the cast closure, in insert_best_wrapped. + let policy = "thresh(2,and(after(147),or(114@and(TRIVIAL,pk(key_02)),7@pk(key_c4))),pk(key_37),pk(key_f2))" + .parse::().unwrap(); + let compilation: AstElemExt<_, Legacy> = + best_t(&mut BTreeMap::new(), &policy, PositiveF64::ONE, None).unwrap(); + + assert_eq!(compilation.ms.to_string(), "thresh(2,nl:and_v(vc:or_i(pk_h(key_c4),and_v(v:1,pk_k(key_02))),after(147)),s:pk(key_37),s:pk(key_f2))"); + assert_eq!(compilation.cost_1d(PositiveF64::ONE, None), 299.0165289256198); + assert_eq!(policy.lift().unwrap().sorted(), compilation.ms.lift().unwrap().sorted()); + } + #[test] fn compile_q() { let policy = SPolicy::from_str("or(1@and(pk(A),pk(B)),127@pk(C))").expect("parsing"); From ac3c7b63b259d22410e57ae603ed3b70f79903b4 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 Jun 2026 12:00:36 +0000 Subject: [PATCH 06/10] compiler: move CompilerExtData and AstExtData into own module Better encapsulation, and I want to clean up these types. Code move only. --- src/policy/compiler/ext_data.rs | 428 ++++++++++++++++++++++++++++++++ src/policy/compiler/mod.rs | 411 +----------------------------- 2 files changed, 431 insertions(+), 408 deletions(-) create mode 100644 src/policy/compiler/ext_data.rs diff --git a/src/policy/compiler/ext_data.rs b/src/policy/compiler/ext_data.rs new file mode 100644 index 000000000..6e1dcf672 --- /dev/null +++ b/src/policy/compiler/ext_data.rs @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! # Extra Node Data for the Policy Compiler + +use sync::Arc; + +use crate::miniscript::context::SigType; +use crate::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG}; +use crate::miniscript::types; +use crate::prelude::*; +use crate::{Miniscript, MiniscriptKey, PositiveF64, ScriptContext, Terminal}; + +/// Miniscript AST fragment with additional data needed by the compiler +#[derive(Clone, Debug)] +pub struct AstElemExt { + /// The actual Miniscript fragment with type information + pub ms: Arc>, + /// Its "type" in terms of compiler data + pub comp_ext_data: CompilerExtData, +} + +impl AstElemExt { + /// Compute a 1-dimensional cost, given a probability of satisfaction + /// and a probability of dissatisfaction; if `dissat_prob` is `None` + /// then it is assumed that dissatisfaction never occurs + pub fn cost_1d(&self, sat_prob: PositiveF64, dissat_prob: Option) -> f64 { + self.ms.ext.pk_cost as f64 + + self.comp_ext_data.sat_cost * f64::from(sat_prob) + + match (dissat_prob, self.comp_ext_data.dissat_cost) { + (Some(prob), Some(cost)) => f64::from(prob) * cost, + (Some(_), None) => f64::INFINITY, + (None, Some(_)) => 0.0, + (None, None) => 0.0, + } + } +} + +impl AstElemExt { + pub fn unsatisfiable() -> Self { + Self { ms: Arc::new(Miniscript::FALSE), comp_ext_data: CompilerExtData::FALSE } + } + + pub fn trivial() -> Self { + Self { ms: Arc::new(Miniscript::TRUE), comp_ext_data: CompilerExtData::TRUE } + } + + pub fn pk_h(key: Pk) -> Self { + Self { + ms: Arc::new(Miniscript::pk_h(key)), + comp_ext_data: CompilerExtData::pk_h::(), + } + } + + pub fn pk_k(key: Pk) -> Self { + Self { + ms: Arc::new(Miniscript::pk_k(key)), + comp_ext_data: CompilerExtData::pk_k::(), + } + } + + pub fn after(t: crate::AbsLockTime) -> Self { + Self { ms: Arc::new(Miniscript::after(t)), comp_ext_data: CompilerExtData::time() } + } + + pub fn older(t: crate::RelLockTime) -> Self { + Self { ms: Arc::new(Miniscript::older(t)), comp_ext_data: CompilerExtData::time() } + } + + pub fn sha256(h: Pk::Sha256) -> Self { + Self { ms: Arc::new(Miniscript::sha256(h)), comp_ext_data: CompilerExtData::hash() } + } + + pub fn hash256(h: Pk::Hash256) -> Self { + Self { ms: Arc::new(Miniscript::hash256(h)), comp_ext_data: CompilerExtData::hash() } + } + + pub fn ripemd160(h: Pk::Ripemd160) -> Self { + Self { ms: Arc::new(Miniscript::ripemd160(h)), comp_ext_data: CompilerExtData::hash() } + } + + pub fn hash160(h: Pk::Hash160) -> Self { + Self { ms: Arc::new(Miniscript::hash160(h)), comp_ext_data: CompilerExtData::hash() } + } + + pub fn multi(thresh: crate::Threshold) -> Self { + let k = thresh.k(); + Self { + ms: Arc::new(Miniscript::multi(thresh)), + comp_ext_data: CompilerExtData::multi(k), + } + } + + pub fn multi_a(thresh: crate::Threshold) -> Self { + let k = thresh.k(); + let n = thresh.n(); + Self { + ms: Arc::new(Miniscript::multi_a(thresh)), + comp_ext_data: CompilerExtData::multi_a(k, n), + } + } + + /// Helper functions to compose two Miniscript fragments, where we assume + /// by construction that all validation parameters are upheld. + fn compose_typeck_only( + term: Terminal, + ) -> Result>, types::Error> { + let ty = types::Type::type_check(&term)?; + let ext = types::ExtData::type_check(&term); + Ok(Arc::new(Miniscript::from_components_unchecked(term, ty, ext))) + } + + pub fn and_b(left: &Self, right: &Self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::AndB( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + ))?, + comp_ext_data: CompilerExtData::and_b(left.comp_ext_data, right.comp_ext_data), + }) + } + + pub fn and_v(left: &Self, right: &Self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::AndV( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + ))?, + comp_ext_data: CompilerExtData::and_v(left.comp_ext_data, right.comp_ext_data), + }) + } + + /// and_n(a,b) == andor(a,b,0) is a conjunction of a and b + pub fn and_n(left: &Self, right: &Self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::AndOr( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + Arc::new(Miniscript::FALSE), + ))?, + comp_ext_data: CompilerExtData::and_n(left.comp_ext_data, right.comp_ext_data), + }) + } + + pub fn and_or( + a: &Self, + b: &Self, + c: &Self, + l_weight: PositiveF64, + r_weight: PositiveF64, + ) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::AndOr( + Arc::clone(&a.ms), + Arc::clone(&b.ms), + Arc::clone(&c.ms), + ))?, + comp_ext_data: CompilerExtData::and_or( + a.comp_ext_data, + b.comp_ext_data, + c.comp_ext_data, + l_weight, + r_weight, + ), + }) + } + + pub fn or_b( + left: &Self, + right: &Self, + l_weight: PositiveF64, + r_weight: PositiveF64, + ) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::OrB( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + ))?, + comp_ext_data: CompilerExtData::or_b( + left.comp_ext_data, + right.comp_ext_data, + l_weight, + r_weight, + ), + }) + } + + pub fn or_d( + left: &Self, + right: &Self, + l_weight: PositiveF64, + r_weight: PositiveF64, + ) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::OrD( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + ))?, + comp_ext_data: CompilerExtData::or_d( + left.comp_ext_data, + right.comp_ext_data, + l_weight, + r_weight, + ), + }) + } + + pub fn or_c( + left: &Self, + right: &Self, + l_weight: PositiveF64, + r_weight: PositiveF64, + ) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::OrC( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + ))?, + comp_ext_data: CompilerExtData::or_c( + left.comp_ext_data, + right.comp_ext_data, + l_weight, + r_weight, + ), + }) + } + + pub fn or_i( + left: &Self, + right: &Self, + l_weight: PositiveF64, + r_weight: PositiveF64, + ) -> Result { + Ok(Self { + ms: Self::compose_typeck_only(Terminal::OrI( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + ))?, + comp_ext_data: CompilerExtData::or_i( + left.comp_ext_data, + right.comp_ext_data, + l_weight, + r_weight, + ), + }) + } +} + +#[derive(Copy, Clone, Debug)] +pub struct CompilerExtData { + /// The number of bytes needed to satisfy the fragment in segwit format + /// (total length of all witness pushes, plus their own length prefixes) + sat_cost: f64, + /// The number of bytes needed to dissatisfy the fragment in segwit format + /// (total length of all witness pushes, plus their own length prefixes) + /// for fragments that can be dissatisfied without failing the script. + dissat_cost: Option, +} + +impl CompilerExtData { + const TRUE: Self = Self { sat_cost: 0.0, dissat_cost: None }; + + const FALSE: Self = Self { sat_cost: f64::MAX, dissat_cost: Some(0.0) }; + + pub fn pk_k() -> Self { + Self { + sat_cost: match Ctx::sig_type() { + SigType::Ecdsa => 73.0, + SigType::Schnorr => 1.0 /* */ + 64.0 /* sig */ + 1.0, /* */ + }, + dissat_cost: Some(1.0), + } + } + + pub fn pk_h() -> Self { + Self { + sat_cost: match Ctx::sig_type() { + SigType::Ecdsa => 73.0 + 34.0, + SigType::Schnorr => 66.0 + 33.0, + }, + dissat_cost: Some( + 1.0 + match Ctx::sig_type() { + SigType::Ecdsa => 34.0, + SigType::Schnorr => 33.0, + }, + ), + } + } + + fn multi(k: usize) -> Self { + Self { sat_cost: 1.0 + 73.0 * k as f64, dissat_cost: Some(1.0 * (k + 1) as f64) } + } + + fn multi_a(k: usize, n: usize) -> Self { + Self { + sat_cost: 66.0 * k as f64 + (n - k) as f64, + dissat_cost: Some(n as f64), /* ... := 0x00 ... 0x00 (n times) */ + } + } + + fn hash() -> Self { Self { sat_cost: 33.0, dissat_cost: Some(33.0) } } + + fn time() -> Self { Self { sat_cost: 0.0, dissat_cost: None } } + + pub fn cast_alt(self) -> Self { + Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } + } + + pub fn cast_swap(self) -> Self { + Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } + } + + pub fn cast_check(self) -> Self { + Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } + } + + pub fn cast_dupif(self) -> Self { + Self { sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0) } + } + + pub fn cast_verify(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: None } } + + pub fn cast_nonzero(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: Some(1.0) } } + + pub fn cast_zeronotequal(self) -> Self { + Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } + } + + pub fn cast_true(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: None } } + + pub fn cast_unlikely(self) -> Self { + Self { sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0) } + } + + pub fn cast_likely(self) -> Self { + Self { sat_cost: 1.0 + self.sat_cost, dissat_cost: Some(2.0) } + } + + pub fn and_b(left: Self, right: Self) -> Self { + Self { + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: match (left.dissat_cost, right.dissat_cost) { + (Some(l), Some(r)) => Some(l + r), + _ => None, + }, + } + } + + pub fn and_v(left: Self, right: Self) -> Self { + Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: None } + } + + fn or_b(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { + Self { + sat_cost: f64::from(lprob) * (l.sat_cost + r.dissat_cost.unwrap()) + + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), + dissat_cost: Some(l.dissat_cost.unwrap() + r.dissat_cost.unwrap()), + } + } + + fn or_d(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { + Self { + sat_cost: f64::from(lprob) * l.sat_cost + + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), + dissat_cost: r.dissat_cost.map(|rd| l.dissat_cost.unwrap() + rd), + } + } + + fn or_c(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { + Self { + sat_cost: f64::from(lprob) * l.sat_cost + + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), + dissat_cost: None, + } + } + + #[allow(clippy::manual_map)] // Complex if/let is better as is. + fn or_i(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { + Self { + sat_cost: f64::from(lprob) * (2.0 + l.sat_cost) + f64::from(rprob) * (1.0 + r.sat_cost), + dissat_cost: if let (Some(ldis), Some(rdis)) = (l.dissat_cost, r.dissat_cost) { + if (2.0 + ldis) > (1.0 + rdis) { + Some(1.0 + rdis) + } else { + Some(2.0 + ldis) + } + } else if let Some(ldis) = l.dissat_cost { + Some(2.0 + ldis) + } else if let Some(rdis) = r.dissat_cost { + Some(1.0 + rdis) + } else { + None + }, + } + } + + pub fn and_or(a: Self, b: Self, c: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { + let adis = a + .dissat_cost + .expect("BUG: and_or first arg(a) must be dissatisfiable"); + Self { + sat_cost: f64::from(lprob) * (a.sat_cost + b.sat_cost) + + f64::from(rprob) * (adis + c.sat_cost), + dissat_cost: c.dissat_cost.map(|cdis| adis + cdis), + } + } + + pub fn and_n(left: Self, right: Self) -> Self { + Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: left.dissat_cost } + } + + pub fn threshold(thresh: &crate::Threshold, mut sub_ck: S) -> Self + where + S: FnMut(usize) -> Self, + { + let k_over_n = f64::from(PositiveF64::k_over_n(thresh)); + let mut sat_cost = 0.0; + let mut dissat_cost = 0.0; + for i in 0..thresh.n() { + let sub = sub_ck(i); + sat_cost += sub.sat_cost; + dissat_cost += sub.dissat_cost.unwrap(); + } + Self { + sat_cost: sat_cost * k_over_n + dissat_cost * (1.0 - k_over_n), + dissat_cost: Some(dissat_cost), + } + } +} diff --git a/src/policy/compiler/mod.rs b/src/policy/compiler/mod.rs index 6e2e25da1..9eec98d9d 100644 --- a/src/policy/compiler/mod.rs +++ b/src/policy/compiler/mod.rs @@ -5,6 +5,8 @@ //! Optimizing compiler from concrete policies to Miniscript //! +mod ext_data; + use core::num::NonZeroU32; use core::{f64, fmt, mem}; #[cfg(feature = "std")] @@ -12,8 +14,8 @@ use std::error; use sync::Arc; +use self::ext_data::{AstElemExt, CompilerExtData}; use crate::miniscript::context::SigType; -use crate::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG}; use crate::miniscript::types::{self, ErrorKind, Type}; use crate::miniscript::ScriptContext; use crate::policy::Concrete; @@ -298,413 +300,6 @@ impl CompilationKey { } } -#[derive(Copy, Clone, Debug)] -struct CompilerExtData { - /// The number of bytes needed to satisfy the fragment in segwit format - /// (total length of all witness pushes, plus their own length prefixes) - sat_cost: f64, - /// The number of bytes needed to dissatisfy the fragment in segwit format - /// (total length of all witness pushes, plus their own length prefixes) - /// for fragments that can be dissatisfied without failing the script. - dissat_cost: Option, -} - -impl CompilerExtData { - const TRUE: Self = Self { sat_cost: 0.0, dissat_cost: None }; - - const FALSE: Self = Self { sat_cost: f64::MAX, dissat_cost: Some(0.0) }; - - fn pk_k() -> Self { - Self { - sat_cost: match Ctx::sig_type() { - SigType::Ecdsa => 73.0, - SigType::Schnorr => 1.0 /* */ + 64.0 /* sig */ + 1.0, /* */ - }, - dissat_cost: Some(1.0), - } - } - - fn pk_h() -> Self { - Self { - sat_cost: match Ctx::sig_type() { - SigType::Ecdsa => 73.0 + 34.0, - SigType::Schnorr => 66.0 + 33.0, - }, - dissat_cost: Some( - 1.0 + match Ctx::sig_type() { - SigType::Ecdsa => 34.0, - SigType::Schnorr => 33.0, - }, - ), - } - } - - fn multi(k: usize) -> Self { - Self { sat_cost: 1.0 + 73.0 * k as f64, dissat_cost: Some(1.0 * (k + 1) as f64) } - } - - fn multi_a(k: usize, n: usize) -> Self { - Self { - sat_cost: 66.0 * k as f64 + (n - k) as f64, - dissat_cost: Some(n as f64), /* ... := 0x00 ... 0x00 (n times) */ - } - } - - fn hash() -> Self { Self { sat_cost: 33.0, dissat_cost: Some(33.0) } } - - fn time() -> Self { Self { sat_cost: 0.0, dissat_cost: None } } - - fn cast_alt(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } } - - fn cast_swap(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } } - - fn cast_check(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } } - - fn cast_dupif(self) -> Self { Self { sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0) } } - - fn cast_verify(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: None } } - - fn cast_nonzero(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: Some(1.0) } } - - fn cast_zeronotequal(self) -> Self { - Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } - } - - fn cast_true(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: None } } - - fn cast_unlikely(self) -> Self { - Self { sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0) } - } - - fn cast_likely(self) -> Self { Self { sat_cost: 1.0 + self.sat_cost, dissat_cost: Some(2.0) } } - - fn and_b(left: Self, right: Self) -> Self { - Self { - sat_cost: left.sat_cost + right.sat_cost, - dissat_cost: match (left.dissat_cost, right.dissat_cost) { - (Some(l), Some(r)) => Some(l + r), - _ => None, - }, - } - } - - fn and_v(left: Self, right: Self) -> Self { - Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: None } - } - - fn and_n(left: Self, right: Self) -> Self { - Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: left.dissat_cost } - } - - fn or_b(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * (l.sat_cost + r.dissat_cost.unwrap()) - + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), - dissat_cost: Some(l.dissat_cost.unwrap() + r.dissat_cost.unwrap()), - } - } - - fn or_d(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * l.sat_cost - + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), - dissat_cost: r.dissat_cost.map(|rd| l.dissat_cost.unwrap() + rd), - } - } - - fn or_c(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * l.sat_cost - + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), - dissat_cost: None, - } - } - - #[allow(clippy::manual_map)] // Complex if/let is better as is. - fn or_i(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * (2.0 + l.sat_cost) + f64::from(rprob) * (1.0 + r.sat_cost), - dissat_cost: if let (Some(ldis), Some(rdis)) = (l.dissat_cost, r.dissat_cost) { - if (2.0 + ldis) > (1.0 + rdis) { - Some(1.0 + rdis) - } else { - Some(2.0 + ldis) - } - } else if let Some(ldis) = l.dissat_cost { - Some(2.0 + ldis) - } else if let Some(rdis) = r.dissat_cost { - Some(1.0 + rdis) - } else { - None - }, - } - } - - fn and_or(a: Self, b: Self, c: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - let adis = a - .dissat_cost - .expect("BUG: and_or first arg(a) must be dissatisfiable"); - Self { - sat_cost: f64::from(lprob) * (a.sat_cost + b.sat_cost) - + f64::from(rprob) * (adis + c.sat_cost), - dissat_cost: c.dissat_cost.map(|cdis| adis + cdis), - } - } - - fn threshold(thresh: &crate::Threshold, mut sub_ck: S) -> Self - where - S: FnMut(usize) -> Self, - { - let k_over_n = f64::from(PositiveF64::k_over_n(thresh)); - let mut sat_cost = 0.0; - let mut dissat_cost = 0.0; - for i in 0..thresh.n() { - let sub = sub_ck(i); - sat_cost += sub.sat_cost; - dissat_cost += sub.dissat_cost.unwrap(); - } - Self { - sat_cost: sat_cost * k_over_n + dissat_cost * (1.0 - k_over_n), - dissat_cost: Some(dissat_cost), - } - } -} - -/// Miniscript AST fragment with additional data needed by the compiler -#[derive(Clone, Debug)] -struct AstElemExt { - /// The actual Miniscript fragment with type information - ms: Arc>, - /// Its "type" in terms of compiler data - comp_ext_data: CompilerExtData, -} - -impl AstElemExt { - /// Compute a 1-dimensional cost, given a probability of satisfaction - /// and a probability of dissatisfaction; if `dissat_prob` is `None` - /// then it is assumed that dissatisfaction never occurs - fn cost_1d(&self, sat_prob: PositiveF64, dissat_prob: Option) -> f64 { - self.ms.ext.pk_cost as f64 - + self.comp_ext_data.sat_cost * f64::from(sat_prob) - + match (dissat_prob, self.comp_ext_data.dissat_cost) { - (Some(prob), Some(cost)) => f64::from(prob) * cost, - (Some(_), None) => f64::INFINITY, - (None, Some(_)) => 0.0, - (None, None) => 0.0, - } - } -} - -impl AstElemExt { - fn unsatisfiable() -> Self { - Self { ms: Arc::new(Miniscript::FALSE), comp_ext_data: CompilerExtData::FALSE } - } - - fn trivial() -> Self { - Self { ms: Arc::new(Miniscript::TRUE), comp_ext_data: CompilerExtData::TRUE } - } - - fn pk_h(key: Pk) -> Self { - Self { - ms: Arc::new(Miniscript::pk_h(key)), - comp_ext_data: CompilerExtData::pk_h::(), - } - } - - fn pk_k(key: Pk) -> Self { - Self { - ms: Arc::new(Miniscript::pk_k(key)), - comp_ext_data: CompilerExtData::pk_k::(), - } - } - - fn after(t: crate::AbsLockTime) -> Self { - Self { ms: Arc::new(Miniscript::after(t)), comp_ext_data: CompilerExtData::time() } - } - - fn older(t: crate::RelLockTime) -> Self { - Self { ms: Arc::new(Miniscript::older(t)), comp_ext_data: CompilerExtData::time() } - } - - fn sha256(h: Pk::Sha256) -> Self { - Self { ms: Arc::new(Miniscript::sha256(h)), comp_ext_data: CompilerExtData::hash() } - } - - fn hash256(h: Pk::Hash256) -> Self { - Self { ms: Arc::new(Miniscript::hash256(h)), comp_ext_data: CompilerExtData::hash() } - } - - fn ripemd160(h: Pk::Ripemd160) -> Self { - Self { ms: Arc::new(Miniscript::ripemd160(h)), comp_ext_data: CompilerExtData::hash() } - } - - fn hash160(h: Pk::Hash160) -> Self { - Self { ms: Arc::new(Miniscript::hash160(h)), comp_ext_data: CompilerExtData::hash() } - } - - fn multi(thresh: crate::Threshold) -> Self { - let k = thresh.k(); - Self { - ms: Arc::new(Miniscript::multi(thresh)), - comp_ext_data: CompilerExtData::multi(k), - } - } - - fn multi_a(thresh: crate::Threshold) -> Self { - let k = thresh.k(); - let n = thresh.n(); - Self { - ms: Arc::new(Miniscript::multi_a(thresh)), - comp_ext_data: CompilerExtData::multi_a(k, n), - } - } - - /// Helper functions to compose two Miniscript fragments, where we assume - /// by construction that all validation parameters are upheld. - fn compose_typeck_only( - term: Terminal, - ) -> Result>, types::Error> { - let ty = types::Type::type_check(&term)?; - let ext = types::ExtData::type_check(&term); - Ok(Arc::new(Miniscript::from_components_unchecked(term, ty, ext))) - } - - fn and_b(left: &Self, right: &Self) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndB( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, - comp_ext_data: CompilerExtData::and_b(left.comp_ext_data, right.comp_ext_data), - }) - } - - fn and_v(left: &Self, right: &Self) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndV( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, - comp_ext_data: CompilerExtData::and_v(left.comp_ext_data, right.comp_ext_data), - }) - } - - /// and_n(a,b) == andor(a,b,0) is a conjunction of a and b - fn and_n(left: &Self, right: &Self) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndOr( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - Arc::new(Miniscript::FALSE), - ))?, - comp_ext_data: CompilerExtData::and_n(left.comp_ext_data, right.comp_ext_data), - }) - } - - fn and_or( - a: &Self, - b: &Self, - c: &Self, - l_weight: PositiveF64, - r_weight: PositiveF64, - ) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndOr( - Arc::clone(&a.ms), - Arc::clone(&b.ms), - Arc::clone(&c.ms), - ))?, - comp_ext_data: CompilerExtData::and_or( - a.comp_ext_data, - b.comp_ext_data, - c.comp_ext_data, - l_weight, - r_weight, - ), - }) - } - - fn or_b( - left: &Self, - right: &Self, - l_weight: PositiveF64, - r_weight: PositiveF64, - ) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrB( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, - comp_ext_data: CompilerExtData::or_b( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), - }) - } - - fn or_d( - left: &Self, - right: &Self, - l_weight: PositiveF64, - r_weight: PositiveF64, - ) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrD( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, - comp_ext_data: CompilerExtData::or_d( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), - }) - } - - fn or_c( - left: &Self, - right: &Self, - l_weight: PositiveF64, - r_weight: PositiveF64, - ) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrC( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, - comp_ext_data: CompilerExtData::or_c( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), - }) - } - - fn or_i( - left: &Self, - right: &Self, - l_weight: PositiveF64, - r_weight: PositiveF64, - ) -> Result { - Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrI( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, - comp_ext_data: CompilerExtData::or_i( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), - }) - } -} - /// Different types of casts possible for each node. #[allow(clippy::type_complexity)] #[derive(Copy, Clone)] From 0a9ef0fbe97324c93c0fedd5f86c89145fd3667d Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 Jun 2026 13:00:34 +0000 Subject: [PATCH 07/10] compiler: don't use Types::type_check The Type::type_check function takes a terminal, matches on it, calls the appropriate method on Type, and wraps it in a nice error. It turns out that in the compiler, this is obscenely slow. I'm not sure if it's because the big match undermines inlining, or if the error-wrapping (which the compiler doesn't even use; it only uses typeck errors as a binary "ok or not?" signal), or what. But empirically, replacing the call to Type::type_check with direct calls to the methods on Type, results in a massive compiler speedup. At the cost of a bit more code repetition. IMO definitely worth it. You shouldn't trust these benchmarks too much because my system was busy, but the difference is quite extreme: Before: test benchmarks::compiler_benches::compile_basic ... bench: 6,285,527.70 ns/iter (+/- 4,895,149.56) test benchmarks::compiler_benches::compile_large ... bench: 14,602,446.20 ns/iter (+/- 9,250,348.77) test benchmarks::compiler_benches::compile_large_tap ... bench: 982,121,582.90 ns/iter (+/- 1,209,655,507.47) test benchmarks::compiler_benches::compile_xlarge ... bench: 582,951,636.20 ns/iter (+/- 58,752,609.07) After: test benchmarks::compiler_benches::compile_basic ... bench: 1,520,658.20 ns/iter (+/- 13,249.16) test benchmarks::compiler_benches::compile_large ... bench: 4,128,737.40 ns/iter (+/- 55,408.00) test benchmarks::compiler_benches::compile_large_tap ... bench: 716,569,601.10 ns/iter (+/- 130,612,914.15) test benchmarks::compiler_benches::compile_xlarge ... bench: 148,633,378.80 ns/iter (+/- 16,348,954.09) On `compile_large_tap` it's "only" a 35% speedup but on the others, and on the `segwit_limits` unit tests, it's a 3-4x speedup. Wild. --- src/policy/compiler/ext_data.rs | 93 ++++++++++++++++----------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/src/policy/compiler/ext_data.rs b/src/policy/compiler/ext_data.rs index 6e1dcf672..2f2299154 100644 --- a/src/policy/compiler/ext_data.rs +++ b/src/policy/compiler/ext_data.rs @@ -101,42 +101,42 @@ impl AstElemExt { /// Helper functions to compose two Miniscript fragments, where we assume /// by construction that all validation parameters are upheld. - fn compose_typeck_only( - term: Terminal, - ) -> Result>, types::Error> { - let ty = types::Type::type_check(&term)?; + fn compose_typeck_only(term: Terminal, ty: types::Type) -> Arc> { let ext = types::ExtData::type_check(&term); - Ok(Arc::new(Miniscript::from_components_unchecked(term, ty, ext))) + Arc::new(Miniscript::from_components_unchecked(term, ty, ext)) } - pub fn and_b(left: &Self, right: &Self) -> Result { + pub fn and_b(left: &Self, right: &Self) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndB( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::AndB(Arc::clone(&left.ms), Arc::clone(&right.ms)), + types::Type::and_b(left.ms.ty, right.ms.ty)?, + ), comp_ext_data: CompilerExtData::and_b(left.comp_ext_data, right.comp_ext_data), }) } - pub fn and_v(left: &Self, right: &Self) -> Result { + pub fn and_v(left: &Self, right: &Self) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndV( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::AndV(Arc::clone(&left.ms), Arc::clone(&right.ms)), + types::Type::and_v(left.ms.ty, right.ms.ty)?, + ), comp_ext_data: CompilerExtData::and_v(left.comp_ext_data, right.comp_ext_data), }) } /// and_n(a,b) == andor(a,b,0) is a conjunction of a and b - pub fn and_n(left: &Self, right: &Self) -> Result { + pub fn and_n(left: &Self, right: &Self) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndOr( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - Arc::new(Miniscript::FALSE), - ))?, + ms: Self::compose_typeck_only( + Terminal::AndOr( + Arc::clone(&left.ms), + Arc::clone(&right.ms), + Arc::new(Miniscript::FALSE), + ), + types::Type::and_or(left.ms.ty, right.ms.ty, types::Type::FALSE)?, + ), comp_ext_data: CompilerExtData::and_n(left.comp_ext_data, right.comp_ext_data), }) } @@ -147,13 +147,12 @@ impl AstElemExt { c: &Self, l_weight: PositiveF64, r_weight: PositiveF64, - ) -> Result { + ) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::AndOr( - Arc::clone(&a.ms), - Arc::clone(&b.ms), - Arc::clone(&c.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::AndOr(Arc::clone(&a.ms), Arc::clone(&b.ms), Arc::clone(&c.ms)), + types::Type::and_or(a.ms.ty, b.ms.ty, c.ms.ty)?, + ), comp_ext_data: CompilerExtData::and_or( a.comp_ext_data, b.comp_ext_data, @@ -169,12 +168,12 @@ impl AstElemExt { right: &Self, l_weight: PositiveF64, r_weight: PositiveF64, - ) -> Result { + ) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrB( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::OrB(Arc::clone(&left.ms), Arc::clone(&right.ms)), + types::Type::or_b(left.ms.ty, right.ms.ty)?, + ), comp_ext_data: CompilerExtData::or_b( left.comp_ext_data, right.comp_ext_data, @@ -189,12 +188,12 @@ impl AstElemExt { right: &Self, l_weight: PositiveF64, r_weight: PositiveF64, - ) -> Result { + ) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrD( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::OrD(Arc::clone(&left.ms), Arc::clone(&right.ms)), + types::Type::or_d(left.ms.ty, right.ms.ty)?, + ), comp_ext_data: CompilerExtData::or_d( left.comp_ext_data, right.comp_ext_data, @@ -209,12 +208,12 @@ impl AstElemExt { right: &Self, l_weight: PositiveF64, r_weight: PositiveF64, - ) -> Result { + ) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrC( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::OrC(Arc::clone(&left.ms), Arc::clone(&right.ms)), + types::Type::or_c(left.ms.ty, right.ms.ty)?, + ), comp_ext_data: CompilerExtData::or_c( left.comp_ext_data, right.comp_ext_data, @@ -229,12 +228,12 @@ impl AstElemExt { right: &Self, l_weight: PositiveF64, r_weight: PositiveF64, - ) -> Result { + ) -> Result { Ok(Self { - ms: Self::compose_typeck_only(Terminal::OrI( - Arc::clone(&left.ms), - Arc::clone(&right.ms), - ))?, + ms: Self::compose_typeck_only( + Terminal::OrI(Arc::clone(&left.ms), Arc::clone(&right.ms)), + types::Type::or_i(left.ms.ty, right.ms.ty)?, + ), comp_ext_data: CompilerExtData::or_i( left.comp_ext_data, right.comp_ext_data, From 3b40c85df4067bf754da38695a20229b72ef0897 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 Jun 2026 12:16:00 +0000 Subject: [PATCH 08/10] compiler: replace Cast struct with function pointers This is a weird structure. I'm not sure what I was thinking here. It's 4 function pointers in a struct, with a `cast()` method on them that calls all the functions in the right order to transform an AstElemExt. Why not just make it an AstElemExt method directly? I did this to simplify the code, and it also comes with another fairly big speedup (though this one is small enough that it's plausibly just noise from my busy machine). The weird order is copied from the old logic. I don't think there's any rhyme or reason to it, but if we change the order then some compilations can change (e.g. swapping the l and n wrappers, which functionally commute). Before (copied from the "after" from my previous commit) test benchmarks::compiler_benches::compile_basic ... bench: 1,520,658.20 ns/iter (+/- 13,249.16) test benchmarks::compiler_benches::compile_large ... bench: 4,128,737.40 ns/iter (+/- 55,408.00) test benchmarks::compiler_benches::compile_large_tap ... bench: 716,569,601.10 ns/iter (+/- 130,612,914.15) test benchmarks::compiler_benches::compile_xlarge ... bench: 148,633,378.80 ns/iter (+/- 16,348,954.09) After: test benchmarks::compiler_benches::compile_basic ... bench: 1,199,013.60 ns/iter (+/- 28,096.21) test benchmarks::compiler_benches::compile_large ... bench: 3,350,305.90 ns/iter (+/- 463,149.87) test benchmarks::compiler_benches::compile_large_tap ... bench: 703,996,003.60 ns/iter (+/- 835,362,175.58) test benchmarks::compiler_benches::compile_xlarge ... bench: 140,988,430.90 ns/iter (+/- 24,692,231.52) seems like 30% or so on the small ones, 5% or so on the big ones. Nice. --- src/policy/compiler/ext_data.rs | 152 +++++++++++++++++++++++++------- src/policy/compiler/mod.rs | 107 ++++------------------ 2 files changed, 136 insertions(+), 123 deletions(-) diff --git a/src/policy/compiler/ext_data.rs b/src/policy/compiler/ext_data.rs index 2f2299154..3861e0fee 100644 --- a/src/policy/compiler/ext_data.rs +++ b/src/policy/compiler/ext_data.rs @@ -242,6 +242,124 @@ impl AstElemExt { ), }) } + + pub fn cast_alt(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::Alt(Arc::clone(&self.ms)), + types::Type::cast_alt(self.ms.ty)?, + ), + comp_ext_data: self.comp_ext_data, + }) + } + + pub fn cast_swap(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::Swap(Arc::clone(&self.ms)), + types::Type::cast_swap(self.ms.ty)?, + ), + comp_ext_data: self.comp_ext_data, + }) + } + + pub fn cast_check(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::Check(Arc::clone(&self.ms)), + types::Type::cast_check(self.ms.ty)?, + ), + comp_ext_data: self.comp_ext_data, + }) + } + + pub fn cast_dupif(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::DupIf(Arc::clone(&self.ms)), + types::Type::cast_dupif(self.ms.ty)?, + ), + comp_ext_data: CompilerExtData { + sat_cost: 2.0 + self.comp_ext_data.sat_cost, + dissat_cost: Some(1.0), + }, + }) + } + + pub fn cast_verify(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::Verify(Arc::clone(&self.ms)), + types::Type::cast_verify(self.ms.ty)?, + ), + comp_ext_data: CompilerExtData { + sat_cost: self.comp_ext_data.sat_cost, + dissat_cost: None, + }, + }) + } + + pub fn cast_nonzero(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::NonZero(Arc::clone(&self.ms)), + types::Type::cast_nonzero(self.ms.ty)?, + ), + comp_ext_data: CompilerExtData { + sat_cost: self.comp_ext_data.sat_cost, + dissat_cost: Some(1.0), + }, + }) + } + + pub fn cast_zeronotequal(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::ZeroNotEqual(Arc::clone(&self.ms)), + types::Type::cast_zeronotequal(self.ms.ty)?, + ), + comp_ext_data: self.comp_ext_data, + }) + } + + pub fn cast_true(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::AndV(Arc::clone(&self.ms), Arc::new(Miniscript::TRUE)), + types::Type::cast_true(self.ms.ty)?, + ), + comp_ext_data: CompilerExtData { + sat_cost: self.comp_ext_data.sat_cost, + dissat_cost: None, + }, + }) + } + + pub fn cast_likely(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::OrI(Arc::new(Miniscript::FALSE), Arc::clone(&self.ms)), + types::Type::cast_likely(self.ms.ty)?, + ), + comp_ext_data: CompilerExtData { + sat_cost: 1.0 + self.comp_ext_data.sat_cost, + dissat_cost: Some(2.0), + }, + }) + } + + pub fn cast_unlikely(&self) -> Result { + Ok(Self { + ms: Self::compose_typeck_only( + Terminal::OrI(Arc::clone(&self.ms), Arc::new(Miniscript::FALSE)), + types::Type::cast_unlikely(self.ms.ty)?, + ), + comp_ext_data: CompilerExtData { + sat_cost: 2.0 + self.comp_ext_data.sat_cost, + dissat_cost: Some(1.0), + }, + }) + } } #[derive(Copy, Clone, Debug)] @@ -300,40 +418,6 @@ impl CompilerExtData { fn time() -> Self { Self { sat_cost: 0.0, dissat_cost: None } } - pub fn cast_alt(self) -> Self { - Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } - } - - pub fn cast_swap(self) -> Self { - Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } - } - - pub fn cast_check(self) -> Self { - Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } - } - - pub fn cast_dupif(self) -> Self { - Self { sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0) } - } - - pub fn cast_verify(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: None } } - - pub fn cast_nonzero(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: Some(1.0) } } - - pub fn cast_zeronotequal(self) -> Self { - Self { sat_cost: self.sat_cost, dissat_cost: self.dissat_cost } - } - - pub fn cast_true(self) -> Self { Self { sat_cost: self.sat_cost, dissat_cost: None } } - - pub fn cast_unlikely(self) -> Self { - Self { sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0) } - } - - pub fn cast_likely(self) -> Self { - Self { sat_cost: 1.0 + self.sat_cost, dissat_cost: Some(2.0) } - } - pub fn and_b(left: Self, right: Self) -> Self { Self { sat_cost: left.sat_cost + right.sat_cost, diff --git a/src/policy/compiler/mod.rs b/src/policy/compiler/mod.rs index 9eec98d9d..4295f06ec 100644 --- a/src/policy/compiler/mod.rs +++ b/src/policy/compiler/mod.rs @@ -16,7 +16,7 @@ use sync::Arc; use self::ext_data::{AstElemExt, CompilerExtData}; use crate::miniscript::context::SigType; -use crate::miniscript::types::{self, ErrorKind, Type}; +use crate::miniscript::types::{self, Type}; use crate::miniscript::ScriptContext; use crate::policy::Concrete; use crate::prelude::*; @@ -300,91 +300,20 @@ impl CompilationKey { } } -/// Different types of casts possible for each node. -#[allow(clippy::type_complexity)] -#[derive(Copy, Clone)] -struct Cast { - node: fn(Arc>) -> Terminal, - ast_type: fn(types::Type) -> Result, - ext_data: fn(types::ExtData) -> types::ExtData, - comp_ext_data: fn(CompilerExtData) -> CompilerExtData, -} - -impl Cast { - fn cast(&self, ast: &AstElemExt) -> Result, ErrorKind> { - Ok(AstElemExt { - ms: Arc::new(Miniscript::from_components_unchecked( - (self.node)(Arc::clone(&ast.ms)), - (self.ast_type)(ast.ms.ty)?, - (self.ext_data)(ast.ms.ext), - )), - comp_ext_data: (self.comp_ext_data)(ast.comp_ext_data), - }) - } -} - -fn all_casts() -> [Cast; 10] { +#[allow(clippy::type_complexity)] // clippy really doesn't like AstElemExt +fn all_casts( +) -> [fn(&AstElemExt) -> Result, types::ErrorKind>; 10] { [ - Cast { - ext_data: types::ExtData::cast_check, - node: Terminal::Check, - ast_type: types::Type::cast_check, - comp_ext_data: CompilerExtData::cast_check, - }, - Cast { - ext_data: types::ExtData::cast_dupif, - node: Terminal::DupIf, - ast_type: types::Type::cast_dupif, - comp_ext_data: CompilerExtData::cast_dupif, - }, - Cast { - ext_data: types::ExtData::cast_likely, - node: |ms| Terminal::OrI(Arc::new(Miniscript::FALSE), ms), - ast_type: types::Type::cast_likely, - comp_ext_data: CompilerExtData::cast_likely, - }, - Cast { - ext_data: types::ExtData::cast_unlikely, - node: |ms| Terminal::OrI(ms, Arc::new(Miniscript::FALSE)), - ast_type: types::Type::cast_unlikely, - comp_ext_data: CompilerExtData::cast_unlikely, - }, - Cast { - ext_data: types::ExtData::cast_verify, - node: Terminal::Verify, - ast_type: types::Type::cast_verify, - comp_ext_data: CompilerExtData::cast_verify, - }, - Cast { - ext_data: types::ExtData::cast_nonzero, - node: Terminal::NonZero, - ast_type: types::Type::cast_nonzero, - comp_ext_data: CompilerExtData::cast_nonzero, - }, - Cast { - ext_data: types::ExtData::cast_true, - node: |ms| Terminal::AndV(ms, Arc::new(Miniscript::TRUE)), - ast_type: types::Type::cast_true, - comp_ext_data: CompilerExtData::cast_true, - }, - Cast { - ext_data: types::ExtData::cast_swap, - node: Terminal::Swap, - ast_type: types::Type::cast_swap, - comp_ext_data: CompilerExtData::cast_swap, - }, - Cast { - node: Terminal::Alt, - ast_type: types::Type::cast_alt, - ext_data: types::ExtData::cast_alt, - comp_ext_data: CompilerExtData::cast_alt, - }, - Cast { - ext_data: types::ExtData::cast_zeronotequal, - node: Terminal::ZeroNotEqual, - ast_type: types::Type::cast_zeronotequal, - comp_ext_data: CompilerExtData::cast_zeronotequal, - }, + AstElemExt::cast_check, + AstElemExt::cast_dupif, + AstElemExt::cast_likely, + AstElemExt::cast_unlikely, + AstElemExt::cast_verify, + AstElemExt::cast_nonzero, + AstElemExt::cast_true, + AstElemExt::cast_swap, + AstElemExt::cast_alt, + AstElemExt::cast_zeronotequal, ] } @@ -458,12 +387,12 @@ fn insert_elem_closure( cast_stack.push_back(astelem_ext); } - let casts: [Cast; 10] = all_casts::(); + let casts = all_casts::(); while !cast_stack.is_empty() { let current = cast_stack.pop_front().unwrap(); for c in &casts { - if let Ok(new_ext) = c.cast(¤t) { + if let Ok(new_ext) = c(¤t) { if insert_elem(map, new_ext.clone(), sat_prob, dissat_prob) { cast_stack.push_back(new_ext); } @@ -492,11 +421,11 @@ fn insert_best_wrapped( insert_elem_closure(map, data, sat_prob, dissat_prob); if dissat_prob.is_some() { - let casts: [Cast; 10] = all_casts::(); + let casts = all_casts::(); for c in &casts { for x in best_compilations(policy_cache, policy, sat_prob, None)?.values() { - if let Ok(new_ext) = c.cast(x) { + if let Ok(new_ext) = c(x) { insert_elem_closure(map, new_ext, sat_prob, dissat_prob); } } From e1c98a65dde0c12f385dc097e18ea127120212ba Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 Jun 2026 13:53:01 +0000 Subject: [PATCH 09/10] compiler: move CompExtData::threshold function to AstElemData::threshold This simplifies and cleans up the code, but with a pretty severe performance hit for the "small" benchmarks, in exchange for a improvement on the larger ones. Will keep an eye on this. Before: test benchmarks::compiler_benches::compile_basic ... bench: 1,199,013.60 ns/iter (+/- 28,096.21) test benchmarks::compiler_benches::compile_large ... bench: 3,350,305.90 ns/iter (+/- 463,149.87) test benchmarks::compiler_benches::compile_large_tap ... bench: 703,996,003.60 ns/iter (+/- 835,362,175.58) test benchmarks::compiler_benches::compile_xlarge ... bench: 140,988,430.90 ns/iter (+/- 24,692,231.52) After: test benchmarks::compiler_benches::compile_basic ... bench: 1,570,666.35 ns/iter (+/- 1,066,467.89) test benchmarks::compiler_benches::compile_large ... bench: 6,010,996.75 ns/iter (+/- 3,744,165.23) test benchmarks::compiler_benches::compile_large_tap ... bench: 694,140,897.60 ns/iter (+/- 656,723,194.35) test benchmarks::compiler_benches::compile_xlarge ... bench: 135,546,606.00 ns/iter (+/- 11,686,439.12) --- src/policy/compiler/ext_data.rs | 35 ++++++++++++++++----------------- src/policy/compiler/mod.rs | 34 ++++++++++---------------------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/policy/compiler/ext_data.rs b/src/policy/compiler/ext_data.rs index 3861e0fee..632ed7c26 100644 --- a/src/policy/compiler/ext_data.rs +++ b/src/policy/compiler/ext_data.rs @@ -243,6 +243,23 @@ impl AstElemExt { }) } + pub fn threshold(ms: Miniscript, k_over_n: f64, subs: &[Self]) -> Self { + let mut sat_cost = 0.0; + let mut dissat_cost = 0.0; + for sub in subs { + sat_cost += sub.comp_ext_data.sat_cost; + dissat_cost += sub.comp_ext_data.dissat_cost.unwrap(); + } + + Self { + ms: Arc::new(ms), + comp_ext_data: CompilerExtData { + sat_cost: sat_cost * k_over_n + dissat_cost * (1.0 - k_over_n), + dissat_cost: Some(dissat_cost), + }, + } + } + pub fn cast_alt(&self) -> Result { Ok(Self { ms: Self::compose_typeck_only( @@ -490,22 +507,4 @@ impl CompilerExtData { pub fn and_n(left: Self, right: Self) -> Self { Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: left.dissat_cost } } - - pub fn threshold(thresh: &crate::Threshold, mut sub_ck: S) -> Self - where - S: FnMut(usize) -> Self, - { - let k_over_n = f64::from(PositiveF64::k_over_n(thresh)); - let mut sat_cost = 0.0; - let mut dissat_cost = 0.0; - for i in 0..thresh.n() { - let sub = sub_ck(i); - sat_cost += sub.sat_cost; - dissat_cost += sub.dissat_cost.unwrap(); - } - Self { - sat_cost: sat_cost * k_over_n + dissat_cost * (1.0 - k_over_n), - dissat_cost: Some(dissat_cost), - } - } } diff --git a/src/policy/compiler/mod.rs b/src/policy/compiler/mod.rs index 4295f06ec..c5056d418 100644 --- a/src/policy/compiler/mod.rs +++ b/src/policy/compiler/mod.rs @@ -14,7 +14,7 @@ use std::error; use sync::Arc; -use self::ext_data::{AstElemExt, CompilerExtData}; +use self::ext_data::AstElemExt; use crate::miniscript::context::SigType; use crate::miniscript::types::{self, Type}; use crate::miniscript::ScriptContext; @@ -523,8 +523,6 @@ where let n = thresh.n(); let k_over_n = PositiveF64::k_over_n(thresh); - let mut sub_ext_data = Vec::with_capacity(n); - let mut best_es = Vec::with_capacity(n); let mut best_ws = Vec::with_capacity(n); @@ -547,8 +545,8 @@ where let bw = best(types::Base::W, policy_cache, ast.as_ref(), sp, dp)?; let diff = be.cost_1d(sp, dp) - bw.cost_1d(sp, dp); - best_es.push((be.comp_ext_data, be)); - best_ws.push((bw.comp_ext_data, bw)); + best_es.push(be); + best_ws.push(bw); if diff < min_value.1 { min_value.0 = i; @@ -558,31 +556,19 @@ where // Construct the threshold, swapping the index of the best (i.e. most // advantageous to be a E vs a W) entry into the first slot so that - // it can be an E. + // it can be an E. Do this in the `best_ws` vector to minimize the + // number of swaps that need to be done. + mem::swap(&mut best_ws[min_value.0], &mut best_es[min_value.0]); + best_ws.swap(0, min_value.0); let mut idx = 0; let ast = Terminal::Thresh(thresh.map_ref(|_| { - let ret = if idx == 0 { - // swap 0 with min_value... - sub_ext_data.push(best_es[min_value.0].0); - Arc::clone(&best_es[min_value.0].1.ms) - } else if idx == min_value.0 { - // swap min_value with 0... - sub_ext_data.push(best_ws[0].0); - Arc::clone(&best_ws[0].1.ms) - } else { - // ...and leave everything else unchanged - sub_ext_data.push(best_ws[idx].0); - Arc::clone(&best_ws[idx].1.ms) - }; idx += 1; - ret + Arc::clone(&best_ws[idx - 1].ms) })); if let Ok(ms) = Miniscript::from_ast(ast) { - let ast_ext = AstElemExt { - ms: Arc::new(ms), - comp_ext_data: CompilerExtData::threshold(thresh, |i| sub_ext_data[i]), - }; + let ast_ext = + AstElemExt::threshold(ms, f64::from(PositiveF64::k_over_n(thresh)), &best_ws); insert_wrap!(ast_ext); } From 482b0a13abc83c540b7ea1809b309cf8990ff600 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 17 Jun 2026 12:12:29 +0000 Subject: [PATCH 10/10] compiler: inline the CompilerExtData struct There is no longer any reason to keep this as a separate data structure. --- src/policy/compiler/ext_data.rs | 307 ++++++++++---------------------- 1 file changed, 92 insertions(+), 215 deletions(-) diff --git a/src/policy/compiler/ext_data.rs b/src/policy/compiler/ext_data.rs index 632ed7c26..545b99bac 100644 --- a/src/policy/compiler/ext_data.rs +++ b/src/policy/compiler/ext_data.rs @@ -15,8 +15,13 @@ use crate::{Miniscript, MiniscriptKey, PositiveF64, ScriptContext, Terminal}; pub struct AstElemExt { /// The actual Miniscript fragment with type information pub ms: Arc>, - /// Its "type" in terms of compiler data - pub comp_ext_data: CompilerExtData, + /// The number of bytes needed to satisfy the fragment in segwit format + /// (total length of all witness pushes, plus their own length prefixes) + sat_cost: f64, + /// The number of bytes needed to dissatisfy the fragment in segwit format + /// (total length of all witness pushes, plus their own length prefixes) + /// for fragments that can be dissatisfied without failing the script. + dissat_cost: Option, } impl AstElemExt { @@ -25,8 +30,8 @@ impl AstElemExt { /// then it is assumed that dissatisfaction never occurs pub fn cost_1d(&self, sat_prob: PositiveF64, dissat_prob: Option) -> f64 { self.ms.ext.pk_cost as f64 - + self.comp_ext_data.sat_cost * f64::from(sat_prob) - + match (dissat_prob, self.comp_ext_data.dissat_cost) { + + self.sat_cost * f64::from(sat_prob) + + match (dissat_prob, self.dissat_cost) { (Some(prob), Some(cost)) => f64::from(prob) * cost, (Some(_), None) => f64::INFINITY, (None, Some(_)) => 0.0, @@ -37,56 +42,70 @@ impl AstElemExt { impl AstElemExt { pub fn unsatisfiable() -> Self { - Self { ms: Arc::new(Miniscript::FALSE), comp_ext_data: CompilerExtData::FALSE } + Self { ms: Arc::new(Miniscript::FALSE), sat_cost: f64::MAX, dissat_cost: Some(0.0) } } pub fn trivial() -> Self { - Self { ms: Arc::new(Miniscript::TRUE), comp_ext_data: CompilerExtData::TRUE } + Self { ms: Arc::new(Miniscript::TRUE), sat_cost: 0.0, dissat_cost: None } } pub fn pk_h(key: Pk) -> Self { Self { ms: Arc::new(Miniscript::pk_h(key)), - comp_ext_data: CompilerExtData::pk_h::(), + sat_cost: match Ctx::sig_type() { + SigType::Ecdsa => 73.0 + 34.0, + SigType::Schnorr => 66.0 + 33.0, + }, + dissat_cost: Some( + 1.0 + match Ctx::sig_type() { + SigType::Ecdsa => 34.0, + SigType::Schnorr => 33.0, + }, + ), } } pub fn pk_k(key: Pk) -> Self { Self { ms: Arc::new(Miniscript::pk_k(key)), - comp_ext_data: CompilerExtData::pk_k::(), + sat_cost: match Ctx::sig_type() { + SigType::Ecdsa => 73.0, + SigType::Schnorr => 1.0 /* */ + 64.0 /* sig */ + 1.0, /* */ + }, + dissat_cost: Some(1.0), } } pub fn after(t: crate::AbsLockTime) -> Self { - Self { ms: Arc::new(Miniscript::after(t)), comp_ext_data: CompilerExtData::time() } + Self { ms: Arc::new(Miniscript::after(t)), sat_cost: 0.0, dissat_cost: None } } pub fn older(t: crate::RelLockTime) -> Self { - Self { ms: Arc::new(Miniscript::older(t)), comp_ext_data: CompilerExtData::time() } + Self { ms: Arc::new(Miniscript::older(t)), sat_cost: 0.0, dissat_cost: None } } pub fn sha256(h: Pk::Sha256) -> Self { - Self { ms: Arc::new(Miniscript::sha256(h)), comp_ext_data: CompilerExtData::hash() } + Self { ms: Arc::new(Miniscript::sha256(h)), sat_cost: 33.0, dissat_cost: Some(33.0) } } pub fn hash256(h: Pk::Hash256) -> Self { - Self { ms: Arc::new(Miniscript::hash256(h)), comp_ext_data: CompilerExtData::hash() } + Self { ms: Arc::new(Miniscript::hash256(h)), sat_cost: 33.0, dissat_cost: Some(33.0) } } pub fn ripemd160(h: Pk::Ripemd160) -> Self { - Self { ms: Arc::new(Miniscript::ripemd160(h)), comp_ext_data: CompilerExtData::hash() } + Self { ms: Arc::new(Miniscript::ripemd160(h)), sat_cost: 33.0, dissat_cost: Some(33.0) } } pub fn hash160(h: Pk::Hash160) -> Self { - Self { ms: Arc::new(Miniscript::hash160(h)), comp_ext_data: CompilerExtData::hash() } + Self { ms: Arc::new(Miniscript::hash160(h)), sat_cost: 33.0, dissat_cost: Some(33.0) } } pub fn multi(thresh: crate::Threshold) -> Self { let k = thresh.k(); Self { ms: Arc::new(Miniscript::multi(thresh)), - comp_ext_data: CompilerExtData::multi(k), + sat_cost: 1.0 + 73.0 * k as f64, + dissat_cost: Some(1.0 * (k + 1) as f64), } } @@ -95,7 +114,8 @@ impl AstElemExt { let n = thresh.n(); Self { ms: Arc::new(Miniscript::multi_a(thresh)), - comp_ext_data: CompilerExtData::multi_a(k, n), + sat_cost: 66.0 * k as f64 + (n - k) as f64, + dissat_cost: Some(n as f64), /* ... := 0x00 ... 0x00 (n times) */ } } @@ -112,7 +132,8 @@ impl AstElemExt { Terminal::AndB(Arc::clone(&left.ms), Arc::clone(&right.ms)), types::Type::and_b(left.ms.ty, right.ms.ty)?, ), - comp_ext_data: CompilerExtData::and_b(left.comp_ext_data, right.comp_ext_data), + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: left.dissat_cost.zip(right.dissat_cost).map(|(l, r)| l + r), }) } @@ -122,7 +143,8 @@ impl AstElemExt { Terminal::AndV(Arc::clone(&left.ms), Arc::clone(&right.ms)), types::Type::and_v(left.ms.ty, right.ms.ty)?, ), - comp_ext_data: CompilerExtData::and_v(left.comp_ext_data, right.comp_ext_data), + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: None, }) } @@ -137,7 +159,8 @@ impl AstElemExt { ), types::Type::and_or(left.ms.ty, right.ms.ty, types::Type::FALSE)?, ), - comp_ext_data: CompilerExtData::and_n(left.comp_ext_data, right.comp_ext_data), + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: left.dissat_cost, }) } @@ -148,18 +171,20 @@ impl AstElemExt { l_weight: PositiveF64, r_weight: PositiveF64, ) -> Result { + // Do typecheck first, or the 'expect' on the next line might fire. + let ty = types::Type::and_or(a.ms.ty, b.ms.ty, c.ms.ty)?; + let adis = a + .dissat_cost + .expect("BUG: and_or first arg(a) must be dissatisfiable"); + Ok(Self { ms: Self::compose_typeck_only( Terminal::AndOr(Arc::clone(&a.ms), Arc::clone(&b.ms), Arc::clone(&c.ms)), - types::Type::and_or(a.ms.ty, b.ms.ty, c.ms.ty)?, - ), - comp_ext_data: CompilerExtData::and_or( - a.comp_ext_data, - b.comp_ext_data, - c.comp_ext_data, - l_weight, - r_weight, + ty, ), + sat_cost: f64::from(l_weight) * (a.sat_cost + b.sat_cost) + + f64::from(r_weight) * (adis + c.sat_cost), + dissat_cost: c.dissat_cost.map(|cdis| adis + cdis), }) } @@ -174,12 +199,9 @@ impl AstElemExt { Terminal::OrB(Arc::clone(&left.ms), Arc::clone(&right.ms)), types::Type::or_b(left.ms.ty, right.ms.ty)?, ), - comp_ext_data: CompilerExtData::or_b( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), + sat_cost: f64::from(l_weight) * (left.sat_cost + right.dissat_cost.unwrap()) + + f64::from(r_weight) * (right.sat_cost + left.dissat_cost.unwrap()), + dissat_cost: Some(left.dissat_cost.unwrap() + right.dissat_cost.unwrap()), }) } @@ -194,12 +216,9 @@ impl AstElemExt { Terminal::OrD(Arc::clone(&left.ms), Arc::clone(&right.ms)), types::Type::or_d(left.ms.ty, right.ms.ty)?, ), - comp_ext_data: CompilerExtData::or_d( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), + sat_cost: f64::from(l_weight) * left.sat_cost + + f64::from(r_weight) * (right.sat_cost + left.dissat_cost.unwrap()), + dissat_cost: right.dissat_cost.map(|rd| left.dissat_cost.unwrap() + rd), }) } @@ -214,15 +233,13 @@ impl AstElemExt { Terminal::OrC(Arc::clone(&left.ms), Arc::clone(&right.ms)), types::Type::or_c(left.ms.ty, right.ms.ty)?, ), - comp_ext_data: CompilerExtData::or_c( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), + sat_cost: f64::from(l_weight) * left.sat_cost + + f64::from(r_weight) * (right.sat_cost + left.dissat_cost.unwrap()), + dissat_cost: None, }) } + #[allow(clippy::manual_map)] // Complex if/let is better as is. pub fn or_i( left: &Self, right: &Self, @@ -234,12 +251,16 @@ impl AstElemExt { Terminal::OrI(Arc::clone(&left.ms), Arc::clone(&right.ms)), types::Type::or_i(left.ms.ty, right.ms.ty)?, ), - comp_ext_data: CompilerExtData::or_i( - left.comp_ext_data, - right.comp_ext_data, - l_weight, - r_weight, - ), + sat_cost: f64::from(l_weight) * (2.0 + left.sat_cost) + + f64::from(r_weight) * (1.0 + right.sat_cost), + dissat_cost: { + let ldis = left.dissat_cost.map(|ldis| 2.0 + ldis); + let rdis = right.dissat_cost.map(|rdis| 1.0 + rdis); + match (ldis, rdis) { + (Some(ldis), Some(rdis)) => Some(ldis.min(rdis)), + (opt_l, opt_r) => opt_l.or(opt_r), + } + }, }) } @@ -247,16 +268,14 @@ impl AstElemExt { let mut sat_cost = 0.0; let mut dissat_cost = 0.0; for sub in subs { - sat_cost += sub.comp_ext_data.sat_cost; - dissat_cost += sub.comp_ext_data.dissat_cost.unwrap(); + sat_cost += sub.sat_cost; + dissat_cost += sub.dissat_cost.unwrap(); } Self { ms: Arc::new(ms), - comp_ext_data: CompilerExtData { - sat_cost: sat_cost * k_over_n + dissat_cost * (1.0 - k_over_n), - dissat_cost: Some(dissat_cost), - }, + sat_cost: sat_cost * k_over_n + dissat_cost * (1.0 - k_over_n), + dissat_cost: Some(dissat_cost), } } @@ -266,7 +285,7 @@ impl AstElemExt { Terminal::Alt(Arc::clone(&self.ms)), types::Type::cast_alt(self.ms.ty)?, ), - comp_ext_data: self.comp_ext_data, + ..*self }) } @@ -276,7 +295,7 @@ impl AstElemExt { Terminal::Swap(Arc::clone(&self.ms)), types::Type::cast_swap(self.ms.ty)?, ), - comp_ext_data: self.comp_ext_data, + ..*self }) } @@ -286,7 +305,7 @@ impl AstElemExt { Terminal::Check(Arc::clone(&self.ms)), types::Type::cast_check(self.ms.ty)?, ), - comp_ext_data: self.comp_ext_data, + ..*self }) } @@ -296,10 +315,8 @@ impl AstElemExt { Terminal::DupIf(Arc::clone(&self.ms)), types::Type::cast_dupif(self.ms.ty)?, ), - comp_ext_data: CompilerExtData { - sat_cost: 2.0 + self.comp_ext_data.sat_cost, - dissat_cost: Some(1.0), - }, + sat_cost: 2.0 + self.sat_cost, + dissat_cost: Some(1.0), }) } @@ -309,10 +326,8 @@ impl AstElemExt { Terminal::Verify(Arc::clone(&self.ms)), types::Type::cast_verify(self.ms.ty)?, ), - comp_ext_data: CompilerExtData { - sat_cost: self.comp_ext_data.sat_cost, - dissat_cost: None, - }, + dissat_cost: None, + ..*self }) } @@ -322,10 +337,8 @@ impl AstElemExt { Terminal::NonZero(Arc::clone(&self.ms)), types::Type::cast_nonzero(self.ms.ty)?, ), - comp_ext_data: CompilerExtData { - sat_cost: self.comp_ext_data.sat_cost, - dissat_cost: Some(1.0), - }, + dissat_cost: Some(1.0), + ..*self }) } @@ -335,7 +348,7 @@ impl AstElemExt { Terminal::ZeroNotEqual(Arc::clone(&self.ms)), types::Type::cast_zeronotequal(self.ms.ty)?, ), - comp_ext_data: self.comp_ext_data, + ..*self }) } @@ -345,10 +358,8 @@ impl AstElemExt { Terminal::AndV(Arc::clone(&self.ms), Arc::new(Miniscript::TRUE)), types::Type::cast_true(self.ms.ty)?, ), - comp_ext_data: CompilerExtData { - sat_cost: self.comp_ext_data.sat_cost, - dissat_cost: None, - }, + dissat_cost: None, + ..*self }) } @@ -358,10 +369,8 @@ impl AstElemExt { Terminal::OrI(Arc::new(Miniscript::FALSE), Arc::clone(&self.ms)), types::Type::cast_likely(self.ms.ty)?, ), - comp_ext_data: CompilerExtData { - sat_cost: 1.0 + self.comp_ext_data.sat_cost, - dissat_cost: Some(2.0), - }, + sat_cost: 1.0 + self.sat_cost, + dissat_cost: Some(2.0), }) } @@ -371,140 +380,8 @@ impl AstElemExt { Terminal::OrI(Arc::clone(&self.ms), Arc::new(Miniscript::FALSE)), types::Type::cast_unlikely(self.ms.ty)?, ), - comp_ext_data: CompilerExtData { - sat_cost: 2.0 + self.comp_ext_data.sat_cost, - dissat_cost: Some(1.0), - }, - }) - } -} - -#[derive(Copy, Clone, Debug)] -pub struct CompilerExtData { - /// The number of bytes needed to satisfy the fragment in segwit format - /// (total length of all witness pushes, plus their own length prefixes) - sat_cost: f64, - /// The number of bytes needed to dissatisfy the fragment in segwit format - /// (total length of all witness pushes, plus their own length prefixes) - /// for fragments that can be dissatisfied without failing the script. - dissat_cost: Option, -} - -impl CompilerExtData { - const TRUE: Self = Self { sat_cost: 0.0, dissat_cost: None }; - - const FALSE: Self = Self { sat_cost: f64::MAX, dissat_cost: Some(0.0) }; - - pub fn pk_k() -> Self { - Self { - sat_cost: match Ctx::sig_type() { - SigType::Ecdsa => 73.0, - SigType::Schnorr => 1.0 /* */ + 64.0 /* sig */ + 1.0, /* */ - }, + sat_cost: 2.0 + self.sat_cost, dissat_cost: Some(1.0), - } - } - - pub fn pk_h() -> Self { - Self { - sat_cost: match Ctx::sig_type() { - SigType::Ecdsa => 73.0 + 34.0, - SigType::Schnorr => 66.0 + 33.0, - }, - dissat_cost: Some( - 1.0 + match Ctx::sig_type() { - SigType::Ecdsa => 34.0, - SigType::Schnorr => 33.0, - }, - ), - } - } - - fn multi(k: usize) -> Self { - Self { sat_cost: 1.0 + 73.0 * k as f64, dissat_cost: Some(1.0 * (k + 1) as f64) } - } - - fn multi_a(k: usize, n: usize) -> Self { - Self { - sat_cost: 66.0 * k as f64 + (n - k) as f64, - dissat_cost: Some(n as f64), /* ... := 0x00 ... 0x00 (n times) */ - } - } - - fn hash() -> Self { Self { sat_cost: 33.0, dissat_cost: Some(33.0) } } - - fn time() -> Self { Self { sat_cost: 0.0, dissat_cost: None } } - - pub fn and_b(left: Self, right: Self) -> Self { - Self { - sat_cost: left.sat_cost + right.sat_cost, - dissat_cost: match (left.dissat_cost, right.dissat_cost) { - (Some(l), Some(r)) => Some(l + r), - _ => None, - }, - } - } - - pub fn and_v(left: Self, right: Self) -> Self { - Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: None } - } - - fn or_b(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * (l.sat_cost + r.dissat_cost.unwrap()) - + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), - dissat_cost: Some(l.dissat_cost.unwrap() + r.dissat_cost.unwrap()), - } - } - - fn or_d(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * l.sat_cost - + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), - dissat_cost: r.dissat_cost.map(|rd| l.dissat_cost.unwrap() + rd), - } - } - - fn or_c(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * l.sat_cost - + f64::from(rprob) * (r.sat_cost + l.dissat_cost.unwrap()), - dissat_cost: None, - } - } - - #[allow(clippy::manual_map)] // Complex if/let is better as is. - fn or_i(l: Self, r: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - Self { - sat_cost: f64::from(lprob) * (2.0 + l.sat_cost) + f64::from(rprob) * (1.0 + r.sat_cost), - dissat_cost: if let (Some(ldis), Some(rdis)) = (l.dissat_cost, r.dissat_cost) { - if (2.0 + ldis) > (1.0 + rdis) { - Some(1.0 + rdis) - } else { - Some(2.0 + ldis) - } - } else if let Some(ldis) = l.dissat_cost { - Some(2.0 + ldis) - } else if let Some(rdis) = r.dissat_cost { - Some(1.0 + rdis) - } else { - None - }, - } - } - - pub fn and_or(a: Self, b: Self, c: Self, lprob: PositiveF64, rprob: PositiveF64) -> Self { - let adis = a - .dissat_cost - .expect("BUG: and_or first arg(a) must be dissatisfiable"); - Self { - sat_cost: f64::from(lprob) * (a.sat_cost + b.sat_cost) - + f64::from(rprob) * (adis + c.sat_cost), - dissat_cost: c.dissat_cost.map(|cdis| adis + cdis), - } - } - - pub fn and_n(left: Self, right: Self) -> Self { - Self { sat_cost: left.sat_cost + right.sat_cost, dissat_cost: left.dissat_cost } + }) } }