From 68a7a403b2b5a21b3a89a200f4749b51dc2f1bb2 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 26 Mar 2025 20:39:20 +0000 Subject: [PATCH 1/4] clippy: a couple tiny fixes Clippy prefers we do string slicing after conversion to bytes, not before, which may be more efficient (at least, in elimantes some panic paths which we know are impossible since our strings are ASCII, but the compiler probably doesn't). It also changes some nested lists in docs to use 1/2/3 numbering because that's what rustdoc recognizes (and clippy complains about weird indentation with the existing a/b/c sublist). --- src/descriptor/checksum.rs | 2 +- src/descriptor/mod.rs | 2 ++ src/miniscript/analyzable.rs | 8 ++++---- src/psbt/mod.rs | 2 ++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/descriptor/checksum.rs b/src/descriptor/checksum.rs index 04c76dc09..59264c450 100644 --- a/src/descriptor/checksum.rs +++ b/src/descriptor/checksum.rs @@ -112,7 +112,7 @@ pub fn verify_checksum(s: &str) -> Result<&str, Error> { } let mut eng = Engine::new(); - eng.input_unchecked(s[..last_hash_pos].as_bytes()); + eng.input_unchecked(&s.as_bytes()[..last_hash_pos]); let expected = eng.checksum_chars(); diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 1b6bc788c..6ffa8eaba 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -549,6 +549,7 @@ impl Descriptor { /// Returns a plan if the provided assets are sufficient to produce a non-malleable satisfaction /// /// If the assets aren't sufficient for generating a Plan, the descriptor is returned + #[allow(clippy::result_large_err)] // our "error type" is the original descriptor pub fn plan

(self, provider: &P) -> Result where P: AssetProvider, @@ -577,6 +578,7 @@ impl Descriptor { /// Returns a plan if the provided assets are sufficient to produce a malleable satisfaction /// /// If the assets aren't sufficient for generating a Plan, the descriptor is returned + #[allow(clippy::result_large_err)] // our "error type" is the original descriptor pub fn plan_mall

(self, provider: &P) -> Result where P: AssetProvider, diff --git a/src/miniscript/analyzable.rs b/src/miniscript/analyzable.rs index 8fe61867d..2e2aa9326 100644 --- a/src/miniscript/analyzable.rs +++ b/src/miniscript/analyzable.rs @@ -18,8 +18,8 @@ use crate::{Miniscript, MiniscriptKey, ScriptContext, Terminal}; /// This allows parsing miniscripts if /// 1. It is unsafe(does not require a digital signature to spend it) /// 2. It contains a unspendable path because of either -/// a. Resource limitations -/// b. Timelock Mixing +/// 1. Resource limitations +/// 2. Timelock Mixing /// 3. The script is malleable and thereby some of satisfaction weight /// guarantees are not satisfied. /// 4. It has repeated public keys @@ -124,8 +124,8 @@ impl ExtParams { /// We currently mark Miniscript as Non-Analyzable if /// 1. It is unsafe(does not require a digital signature to spend it) /// 2. It contains a unspendable path because of either -/// a. Resource limitations -/// b. Timelock Mixing +/// 1. Resource limitations +/// 2. Timelock Mixing /// 3. The script is malleable and thereby some of satisfaction weight /// guarantees are not satisfied. /// 4. It has repeated publickeys diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 65e8c854d..4eaf2f186 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -480,6 +480,7 @@ pub trait PsbtExt { /// Returns a tuple containing /// - Original psbt /// - Input Error detailing why the input finalization failed + #[allow(clippy::result_large_err)] // our "error type" includes the original PSBT fn finalize_inp( self, secp: &secp256k1::Secp256k1, @@ -494,6 +495,7 @@ pub trait PsbtExt { ) -> Result<(), Error>; /// Same as [`PsbtExt::finalize_inp`], but allows for malleable satisfactions + #[allow(clippy::result_large_err)] // our "error type" includes the original PSBT fn finalize_inp_mall( self, secp: &secp256k1::Secp256k1, From 9c9a2f5a9517b0c39d6e342657ac88c1dec99bbc Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 27 Mar 2025 18:53:44 +0000 Subject: [PATCH 2/4] doc: fix broken link --- src/descriptor/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 6ffa8eaba..55ae6d009 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -683,7 +683,7 @@ impl Descriptor { /// See [`at_derivation_index`] and `[derived_descriptor`] for more documentation. /// /// [`at_derivation_index`]: Self::at_derivation_index - /// [`derived_descriptor`]: crate::DerivedDescriptor::derived_descriptor + /// [`derived_descriptor`]: Self::derived_descriptor /// /// # Errors /// From 43cad38e2f2e3f17ff1e156b813fb6e322cd7e2e Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 26 Mar 2025 22:49:57 +0000 Subject: [PATCH 3/4] satisfy: clean up a bunch of satisfaction code There is a bunch of essentially-repeated "combine two satisfactions" code throughout satisfy.rs. It's hard to tell at a glance what order the concatenantions are in, even though this is essential, or whether the combination is actually done correctly. (In particular, we routinely use the opposite order for Witness::combine and for ||'ing the has_sig, and we routinely assume that timelock conditions are None for dissatisfactions, which is true, but it's still a lot to keep in your head.) --- src/miniscript/satisfy.rs | 168 +++++++++++++++----------------------- 1 file changed, 67 insertions(+), 101 deletions(-) diff --git a/src/miniscript/satisfy.rs b/src/miniscript/satisfy.rs index d1413401b..c6a0bd3bf 100644 --- a/src/miniscript/satisfy.rs +++ b/src/miniscript/satisfy.rs @@ -892,6 +892,34 @@ pub struct Satisfaction { } impl Satisfaction> { + /// The empty satisfaction. + /// + /// This has the property that, when concatenated on either side with another satisfaction + /// X, the result will be X. + fn empty() -> Self { + Satisfaction { + has_sig: false, + relative_timelock: None, + absolute_timelock: None, + stack: Witness::Stack(vec![]), + } + } + + /// Forms a satisfaction which is the concatenation of two satisfactions, with `other`'s + /// stack before `self`'s. + /// + /// This order allows callers to write `left.concatenate_rev(right)` which feels more + /// natural than the opposite order, and more importantly, allows this method to be + /// used when folding over an iterator of multiple satisfactions. + fn concatenate_rev(self, other: Self) -> Self { + Satisfaction { + has_sig: self.has_sig || other.has_sig, + relative_timelock: cmp::max(self.relative_timelock, other.relative_timelock), + absolute_timelock: cmp::max(self.absolute_timelock, other.absolute_timelock), + stack: Witness::combine(other.stack, self.stack), + } + } + pub(crate) fn build_template( term: &Terminal, provider: &P, @@ -1044,20 +1072,9 @@ impl Satisfaction> { } } else { // Otherwise flatten everything out - Satisfaction { - has_sig: ret_stack.iter().any(|sat| sat.has_sig), - relative_timelock: ret_stack - .iter() - .filter_map(|sat| sat.relative_timelock) - .max(), - absolute_timelock: ret_stack - .iter() - .filter_map(|sat| sat.absolute_timelock) - .max(), - stack: ret_stack - .into_iter() - .fold(Witness::empty(), |acc, next| Witness::combine(next.stack, acc)), - } + ret_stack + .into_iter() + .fold(Satisfaction::empty(), Satisfaction::concatenate_rev) } } @@ -1128,20 +1145,9 @@ impl Satisfaction> { // combine the witness // no non-malleability checks needed - Satisfaction { - has_sig: ret_stack.iter().any(|sat| sat.has_sig), - relative_timelock: ret_stack - .iter() - .filter_map(|sat| sat.relative_timelock) - .max(), - absolute_timelock: ret_stack - .iter() - .filter_map(|sat| sat.absolute_timelock) - .max(), - stack: ret_stack - .into_iter() - .fold(Witness::empty(), |acc, next| Witness::combine(next.stack, acc)), - } + ret_stack + .into_iter() + .fold(Satisfaction::empty(), Satisfaction::concatenate_rev) } fn minimum(sat1: Self, sat2: Self) -> Self { @@ -1363,12 +1369,7 @@ impl Satisfaction> { Self::satisfy_helper(&l.node, stfr, root_has_sig, leaf_hash, min_fn, thresh_fn); let r_sat = Self::satisfy_helper(&r.node, stfr, root_has_sig, leaf_hash, min_fn, thresh_fn); - Satisfaction { - stack: Witness::combine(r_sat.stack, l_sat.stack), - has_sig: l_sat.has_sig || r_sat.has_sig, - relative_timelock: cmp::max(l_sat.relative_timelock, r_sat.relative_timelock), - absolute_timelock: cmp::max(l_sat.absolute_timelock, r_sat.absolute_timelock), - } + l_sat.concatenate_rev(r_sat) } Terminal::AndOr(ref a, ref b, ref c) => { let a_sat = @@ -1386,27 +1387,7 @@ impl Satisfaction> { let c_sat = Self::satisfy_helper(&c.node, stfr, root_has_sig, leaf_hash, min_fn, thresh_fn); - min_fn( - Satisfaction { - stack: Witness::combine(b_sat.stack, a_sat.stack), - has_sig: a_sat.has_sig || b_sat.has_sig, - relative_timelock: cmp::max( - a_sat.relative_timelock, - b_sat.relative_timelock, - ), - absolute_timelock: cmp::max( - a_sat.absolute_timelock, - b_sat.absolute_timelock, - ), - }, - Satisfaction { - stack: Witness::combine(c_sat.stack, a_nsat.stack), - has_sig: a_nsat.has_sig || c_sat.has_sig, - // timelocks can't be dissatisfied, so here we ignore a_nsat and only consider c_sat - relative_timelock: c_sat.relative_timelock, - absolute_timelock: c_sat.absolute_timelock, - }, - ) + min_fn(a_sat.concatenate_rev(b_sat), a_nsat.concatenate_rev(c_sat)) } Terminal::OrB(ref l, ref r) => { let l_sat = @@ -1434,18 +1415,8 @@ impl Satisfaction> { assert!(!r_nsat.has_sig); min_fn( - Satisfaction { - stack: Witness::combine(r_sat.stack, l_nsat.stack), - has_sig: r_sat.has_sig, - relative_timelock: r_sat.relative_timelock, - absolute_timelock: r_sat.absolute_timelock, - }, - Satisfaction { - stack: Witness::combine(r_nsat.stack, l_sat.stack), - has_sig: l_sat.has_sig, - relative_timelock: l_sat.relative_timelock, - absolute_timelock: l_sat.absolute_timelock, - }, + Satisfaction::concatenate_rev(l_nsat, r_sat), + Satisfaction::concatenate_rev(l_sat, r_nsat), ) } Terminal::OrD(ref l, ref r) | Terminal::OrC(ref l, ref r) => { @@ -1464,15 +1435,7 @@ impl Satisfaction> { assert!(!l_nsat.has_sig); - min_fn( - l_sat, - Satisfaction { - stack: Witness::combine(r_sat.stack, l_nsat.stack), - has_sig: r_sat.has_sig, - relative_timelock: r_sat.relative_timelock, - absolute_timelock: r_sat.absolute_timelock, - }, - ) + min_fn(l_sat, Satisfaction::concatenate_rev(l_nsat, r_sat)) } Terminal::OrI(ref l, ref r) => { let l_sat = @@ -1495,7 +1458,24 @@ impl Satisfaction> { ) } Terminal::Thresh(ref thresh) => { - thresh_fn(thresh, stfr, root_has_sig, leaf_hash, min_fn) + if thresh.k() == thresh.n() { + // this is just an and + thresh + .iter() + .map(|s| { + Self::satisfy_helper( + &s.node, + stfr, + root_has_sig, + leaf_hash, + min_fn, + thresh_fn, + ) + }) + .fold(Satisfaction::empty(), Satisfaction::concatenate_rev) + } else { + thresh_fn(thresh, stfr, root_has_sig, leaf_hash, min_fn) + } } Terminal::Multi(ref thresh) => { // Collect all available signatures @@ -1685,12 +1665,7 @@ impl Satisfaction> { min_fn, thresh_fn, ); - Satisfaction { - stack: Witness::combine(odissat.stack, vsat.stack), - has_sig: vsat.has_sig || odissat.has_sig, - relative_timelock: None, - absolute_timelock: None, - } + vsat.concatenate_rev(odissat) } Terminal::AndB(ref l, ref r) | Terminal::OrB(ref l, ref r) @@ -1712,12 +1687,7 @@ impl Satisfaction> { min_fn, thresh_fn, ); - Satisfaction { - stack: Witness::combine(rnsat.stack, lnsat.stack), - has_sig: rnsat.has_sig || lnsat.has_sig, - relative_timelock: None, - absolute_timelock: None, - } + lnsat.concatenate_rev(rnsat) } Terminal::OrI(ref l, ref r) => { let lnsat = Self::dissatisfy_helper( @@ -1753,23 +1723,19 @@ impl Satisfaction> { // Dissatisfactions don't need to non-malleable. Use minimum_mall always Satisfaction::minimum_mall(dissat_1, dissat_2) } - Terminal::Thresh(ref thresh) => Satisfaction { - stack: thresh.iter().fold(Witness::empty(), |acc, sub| { - let nsat = Self::dissatisfy_helper( - &sub.node, + Terminal::Thresh(ref thresh) => thresh + .iter() + .map(|s| { + Self::dissatisfy_helper( + &s.node, stfr, root_has_sig, leaf_hash, min_fn, thresh_fn, - ); - assert!(!nsat.has_sig); - Witness::combine(nsat.stack, acc) - }), - has_sig: false, - relative_timelock: None, - absolute_timelock: None, - }, + ) + }) + .fold(Satisfaction::empty(), Satisfaction::concatenate_rev), Terminal::Multi(ref thresh) => Satisfaction { stack: Witness::Stack(vec![Placeholder::PushZero; thresh.k() + 1]), has_sig: false, From 426479d436c6a10a2e444542ada293bcef8ffbd1 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 26 Mar 2025 21:57:09 +0000 Subject: [PATCH 4/4] fuzz: add miniscript_satisfy target This commit adds a `miniscript_satisfy` target which includes a fair bit of infrastructure to generate fuzz data (specifically cheap public keys which are easy to put into a string, and also a satisfier). Arguably this stuff should be pulled out of the fuzz test into a library. Punting on that until later when we move to cargo-fuzz. --- .github/workflows/cron-daily-fuzz.yml | 1 + fuzz/Cargo.toml | 4 + fuzz/fuzz_targets/miniscript_satisfy.rs | 250 ++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 fuzz/fuzz_targets/miniscript_satisfy.rs diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml index 26e926ca1..6be4714f6 100644 --- a/.github/workflows/cron-daily-fuzz.yml +++ b/.github/workflows/cron-daily-fuzz.yml @@ -20,6 +20,7 @@ jobs: fuzz_target: [ compile_descriptor, compile_taproot, +miniscript_satisfy, parse_descriptor, parse_descriptor_priv, parse_descriptor_secret, diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c9c593fac..fd0c6cf07 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -26,6 +26,10 @@ path = "fuzz_targets/compile_descriptor.rs" name = "compile_taproot" path = "fuzz_targets/compile_taproot.rs" +[[bin]] +name = "miniscript_satisfy" +path = "fuzz_targets/miniscript_satisfy.rs" + [[bin]] name = "parse_descriptor" path = "fuzz_targets/parse_descriptor.rs" diff --git a/fuzz/fuzz_targets/miniscript_satisfy.rs b/fuzz/fuzz_targets/miniscript_satisfy.rs new file mode 100644 index 000000000..7410c3efd --- /dev/null +++ b/fuzz/fuzz_targets/miniscript_satisfy.rs @@ -0,0 +1,250 @@ +#![allow(unexpected_cfgs)] + +use std::fmt; +use std::str::FromStr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use honggfuzz::fuzz; +use miniscript::bitcoin::hashes::{hash160, ripemd160, sha256, Hash}; +use miniscript::bitcoin::locktime::{absolute, relative}; +use miniscript::bitcoin::taproot::Signature; +use miniscript::bitcoin::{secp256k1, PublicKey, TapLeafHash, TapSighashType, XOnlyPublicKey}; +use miniscript::{hash256, Miniscript, MiniscriptKey, Satisfier, Segwitv0, Tap, ToPublicKey}; + +// FIXME pull this out into a library used by all the fuzztests +#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)] +struct FuzzPk { + compressed: bool, +} + +impl FuzzPk { + pub fn new_from_control_byte(control: u8) -> Self { Self { compressed: control & 1 == 1 } } +} + +impl FromStr for FuzzPk { + type Err = std::num::ParseIntError; + fn from_str(s: &str) -> Result { + let byte = u8::from_str_radix(s, 16)?; + Ok(Self::new_from_control_byte(byte)) + } +} + +impl fmt::Display for FuzzPk { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { "[fuzz pubkey]".fmt(f) } +} + +impl MiniscriptKey for FuzzPk { + type Sha256 = u8; + type Ripemd160 = u8; + type Hash160 = u8; + type Hash256 = u8; +} + +impl ToPublicKey for FuzzPk { + fn to_public_key(&self) -> PublicKey { + let secp_pk = secp256k1::PublicKey::from_slice(&[ + 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x78, + 0xce, 0x56, 0x3f, 0x89, 0xa0, 0xed, 0x94, 0x14, 0xf5, 0xaa, 0x28, 0xad, 0x0d, 0x96, + 0xd6, 0x79, 0x5f, 0x9c, 0x63, 0x3f, 0x39, 0x79, 0xbf, 0x72, 0xae, 0x82, 0x02, 0x98, + 0x3d, 0xc9, 0x89, 0xae, 0xc7, 0xf2, 0xff, 0x2e, 0xd9, 0x1b, 0xdd, 0x69, 0xce, 0x02, + 0xfc, 0x07, 0x00, 0xca, 0x10, 0x0e, 0x59, 0xdd, 0xf3, + ]) + .unwrap(); + PublicKey { inner: secp_pk, compressed: self.compressed } + } + + fn to_sha256(hash: &Self::Sha256) -> sha256::Hash { sha256::Hash::from_byte_array([*hash; 32]) } + + fn to_hash256(hash: &Self::Hash256) -> hash256::Hash { + hash256::Hash::from_byte_array([*hash; 32]) + } + + fn to_ripemd160(hash: &Self::Ripemd160) -> ripemd160::Hash { + ripemd160::Hash::from_byte_array([*hash; 20]) + } + + fn to_hash160(hash: &Self::Ripemd160) -> hash160::Hash { + hash160::Hash::from_byte_array([*hash; 20]) + } +} + +struct FuzzSatisfier<'b> { + idx: AtomicUsize, + buf: &'b [u8], +} + +impl FuzzSatisfier<'_> { + fn read_byte(&self) -> Option { + let idx = self.idx.fetch_add(1, Ordering::SeqCst); + self.buf.get(idx).copied() + } +} + +impl Satisfier for FuzzSatisfier<'_> { + fn lookup_tap_key_spend_sig(&self) -> Option { + let b = self.read_byte()?; + if b & 1 == 1 { + // FIXME in later version of rust-secp we can use from_byte_array + let secp_sig = secp256k1::schnorr::Signature::from_slice(&[0xab; 64]).unwrap(); + Some(Signature { signature: secp_sig, sighash_type: TapSighashType::Default }) + } else { + None + } + } + + fn lookup_tap_leaf_script_sig(&self, _: &FuzzPk, _: &TapLeafHash) -> Option { + self.lookup_tap_key_spend_sig() + } + + // todo + //fn lookup_tap_control_block_map( + // &self, + //) -> Option<&BTreeMap>; + + #[rustfmt::skip] + fn lookup_raw_pkh_pk(&self, _: &hash160::Hash) -> Option { + let b = self.read_byte()?; + if b & 1 == 1 { + // Decoding an uncompresssed key is extremely fast, while decoding + // a compressed one is pretty slow. + let secp_pk = secp256k1::PublicKey::from_slice(&[ + 0x04, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3b, 0x78, 0xce, 0x56, 0x3f, + 0x89, 0xa0, 0xed, 0x94, 0x14, 0xf5, 0xaa, 0x28, + 0xad, 0x0d, 0x96, 0xd6, 0x79, 0x5f, 0x9c, 0x63, + + 0x3f, 0x39, 0x79, 0xbf, 0x72, 0xae, 0x82, 0x02, + 0x98, 0x3d, 0xc9, 0x89, 0xae, 0xc7, 0xf2, 0xff, + 0x2e, 0xd9, 0x1b, 0xdd, 0x69, 0xce, 0x02, 0xfc, + 0x07, 0x00, 0xca, 0x10, 0x0e, 0x59, 0xdd, 0xf3, + ]).unwrap(); + + if b & 2 == 2 { + Some(PublicKey::new(secp_pk)) + } else{ + Some(PublicKey::new_uncompressed(secp_pk)) + } + } else { + None + } + } + + fn lookup_raw_pkh_x_only_pk(&self, h: &hash160::Hash) -> Option { + self.lookup_raw_pkh_pk(h) + .map(|pk| pk.inner.x_only_public_key().0) + } + + // todo + //fn lookup_raw_pkh_ecdsa_sig(&self, h: &hash160::Hash) -> Option<(PublicKey, ecdsa::Signature)>; + + fn lookup_raw_pkh_tap_leaf_script_sig( + &self, + (h, _): &(hash160::Hash, TapLeafHash), + ) -> Option<(XOnlyPublicKey, Signature)> { + self.lookup_raw_pkh_x_only_pk(h) + .zip(self.lookup_tap_key_spend_sig()) + } + + fn lookup_sha256(&self, b: &u8) -> Option<[u8; 32]> { + if *b & 1 == 1 { + Some([*b; 32]) + } else { + None + } + } + + fn lookup_hash256(&self, b: &u8) -> Option<[u8; 32]> { + if *b & 1 == 1 { + Some([*b; 32]) + } else { + None + } + } + + fn lookup_ripemd160(&self, b: &u8) -> Option<[u8; 32]> { + if *b & 1 == 1 { + Some([*b; 32]) + } else { + None + } + } + + fn lookup_hash160(&self, b: &u8) -> Option<[u8; 32]> { + if *b & 1 == 1 { + Some([*b; 32]) + } else { + None + } + } + + fn check_older(&self, t: relative::LockTime) -> bool { t.to_consensus_u32() & 1 == 1 } + + fn check_after(&self, t: absolute::LockTime) -> bool { t.to_consensus_u32() & 1 == 1 } +} + +fn do_test(data: &[u8]) { + if data.len() < 3 { + return; + } + let control = data[0]; + let len = (usize::from(data[1]) << 8) + usize::from(data[2]); + if data.len() < 3 + len { + return; + } + + let s = &data[3..3 + len]; + let fuzz_sat = FuzzSatisfier { idx: AtomicUsize::new(0), buf: &data[3 + len..] }; + + let s = String::from_utf8_lossy(s); + if control & 1 == 1 { + let ms = match Miniscript::::from_str(&s) { + Ok(d) => d, + Err(_) => return, + }; + + let _ = ms.build_template(&fuzz_sat); + } else { + let ms = match Miniscript::::from_str(&s) { + Ok(d) => d, + Err(_) => return, + }; + + let _ = ms.build_template(&fuzz_sat); + }; +} + +fn main() { + loop { + fuzz!(|data| { + do_test(data); + }); + } +} + +#[cfg(test)] +mod tests { + fn extend_vec_from_hex(hex: &str, out: &mut Vec) { + let mut b = 0; + for (idx, c) in hex.as_bytes().iter().enumerate() { + b <<= 4; + match *c { + b'A'..=b'F' => b |= c - b'A' + 10, + b'a'..=b'f' => b |= c - b'a' + 10, + b'0'..=b'9' => b |= c - b'0', + _ => panic!("Bad hex"), + } + if (idx & 1) == 1 { + out.push(b); + b = 0; + } + } + } + + #[test] + fn duplicate_crash() { + let mut a = Vec::new(); + extend_vec_from_hex("", &mut a); + super::do_test(&a); + } +}