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); + } } 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}; diff --git a/src/policy/compiler/ext_data.rs b/src/policy/compiler/ext_data.rs new file mode 100644 index 000000000..545b99bac --- /dev/null +++ b/src/policy/compiler/ext_data.rs @@ -0,0 +1,387 @@ +// 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>, + /// 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 { + /// 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.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, + (None, None) => 0.0, + } + } +} + +impl AstElemExt { + pub fn unsatisfiable() -> Self { + 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), sat_cost: 0.0, dissat_cost: None } + } + + pub fn pk_h(key: Pk) -> Self { + Self { + ms: Arc::new(Miniscript::pk_h(key)), + 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)), + 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)), sat_cost: 0.0, dissat_cost: None } + } + + pub fn older(t: crate::RelLockTime) -> Self { + 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)), sat_cost: 33.0, dissat_cost: Some(33.0) } + } + + pub fn hash256(h: Pk::Hash256) -> Self { + 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)), sat_cost: 33.0, dissat_cost: Some(33.0) } + } + + pub fn hash160(h: Pk::Hash160) -> Self { + 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)), + sat_cost: 1.0 + 73.0 * k as f64, + dissat_cost: Some(1.0 * (k + 1) as f64), + } + } + + pub fn multi_a(thresh: crate::Threshold) -> Self { + let k = thresh.k(); + let n = thresh.n(); + Self { + ms: Arc::new(Miniscript::multi_a(thresh)), + sat_cost: 66.0 * k as f64 + (n - k) as f64, + dissat_cost: Some(n as f64), /* ... := 0x00 ... 0x00 (n times) */ + } + } + + /// Helper functions to compose two Miniscript fragments, where we assume + /// by construction that all validation parameters are upheld. + fn compose_typeck_only(term: Terminal, ty: types::Type) -> Arc> { + let ext = types::ExtData::type_check(&term); + 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)), + types::Type::and_b(left.ms.ty, right.ms.ty)?, + ), + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: left.dissat_cost.zip(right.dissat_cost).map(|(l, r)| l + r), + }) + } + + 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)), + types::Type::and_v(left.ms.ty, right.ms.ty)?, + ), + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: None, + }) + } + + /// 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), + ), + types::Type::and_or(left.ms.ty, right.ms.ty, types::Type::FALSE)?, + ), + sat_cost: left.sat_cost + right.sat_cost, + dissat_cost: left.dissat_cost, + }) + } + + pub fn and_or( + a: &Self, + b: &Self, + c: &Self, + 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)), + 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), + }) + } + + 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)), + types::Type::or_b(left.ms.ty, right.ms.ty)?, + ), + 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()), + }) + } + + 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)), + types::Type::or_d(left.ms.ty, right.ms.ty)?, + ), + 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), + }) + } + + 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)), + types::Type::or_c(left.ms.ty, right.ms.ty)?, + ), + 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, + l_weight: PositiveF64, + r_weight: PositiveF64, + ) -> Result { + Ok(Self { + 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)?, + ), + 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), + } + }, + }) + } + + 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.sat_cost; + dissat_cost += sub.dissat_cost.unwrap(); + } + + Self { + ms: Arc::new(ms), + 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( + Terminal::Alt(Arc::clone(&self.ms)), + types::Type::cast_alt(self.ms.ty)?, + ), + ..*self + }) + } + + 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)?, + ), + ..*self + }) + } + + 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)?, + ), + ..*self + }) + } + + 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)?, + ), + sat_cost: 2.0 + self.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)?, + ), + dissat_cost: None, + ..*self + }) + } + + 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)?, + ), + dissat_cost: Some(1.0), + ..*self + }) + } + + 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)?, + ), + ..*self + }) + } + + 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)?, + ), + dissat_cost: None, + ..*self + }) + } + + 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)?, + ), + sat_cost: 1.0 + self.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)?, + ), + sat_cost: 2.0 + self.sat_cost, + dissat_cost: Some(1.0), + }) + } +} diff --git a/src/policy/compiler.rs b/src/policy/compiler/mod.rs similarity index 70% rename from src/policy/compiler.rs rename to src/policy/compiler/mod.rs index 530be3d83..c5056d418 100644 --- a/src/policy/compiler.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,9 +14,9 @@ use std::error; use sync::Arc; +use self::ext_data::AstElemExt; 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::types::{self, Type}; use crate::miniscript::ScriptContext; use crate::policy::Concrete; use crate::prelude::*; @@ -298,498 +300,20 @@ 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(k: usize, n: usize, mut sub_ck: S) -> Self - where - S: FnMut(usize) -> Self, - { - let k_over_n = k as f64 / n as f64; - let mut sat_cost = 0.0; - let mut dissat_cost = 0.0; - for i in 0..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)] -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, ] } @@ -863,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); } @@ -897,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); } } @@ -996,12 +520,9 @@ 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); - 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); @@ -1024,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; @@ -1035,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(k, n, |i| sub_ext_data[i]), - }; + let ast_ext = + AstElemExt::threshold(ms, f64::from(PositiveF64::k_over_n(thresh)), &best_ws); insert_wrap!(ast_ext); } @@ -1274,6 +783,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");