Skip to content

Commit 5fd2dfd

Browse files
committed
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.
1 parent 4fb4101 commit 5fd2dfd

File tree

3 files changed

+255
-0
lines changed

3 files changed

+255
-0
lines changed

.github/workflows/cron-daily-fuzz.yml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
fuzz_target: [
2121
compile_descriptor,
2222
compile_taproot,
23+
miniscript_satisfy,
2324
parse_descriptor,
2425
parse_descriptor_priv,
2526
parse_descriptor_secret,

fuzz/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ path = "fuzz_targets/compile_descriptor.rs"
2626
name = "compile_taproot"
2727
path = "fuzz_targets/compile_taproot.rs"
2828

29+
[[bin]]
30+
name = "miniscript_satisfy"
31+
path = "fuzz_targets/miniscript_satisfy.rs"
32+
2933
[[bin]]
3034
name = "parse_descriptor"
3135
path = "fuzz_targets/parse_descriptor.rs"
+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
#![allow(unexpected_cfgs)]
2+
3+
use std::fmt;
4+
use std::str::FromStr;
5+
use std::sync::atomic::{AtomicUsize, Ordering};
6+
7+
use honggfuzz::fuzz;
8+
use miniscript::bitcoin::hashes::{hash160, ripemd160, sha256, Hash};
9+
use miniscript::bitcoin::locktime::{absolute, relative};
10+
use miniscript::bitcoin::taproot::Signature;
11+
use miniscript::bitcoin::{secp256k1, PublicKey, TapLeafHash, TapSighashType, XOnlyPublicKey};
12+
use miniscript::{hash256, Miniscript, MiniscriptKey, Satisfier, Segwitv0, Tap, ToPublicKey};
13+
14+
// FIXME pull this out into a library used by all the fuzztests
15+
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)]
16+
struct FuzzPk {
17+
compressed: bool,
18+
}
19+
20+
impl FuzzPk {
21+
pub fn new_from_control_byte(control: u8) -> Self { Self { compressed: control & 1 == 1 } }
22+
}
23+
24+
impl FromStr for FuzzPk {
25+
type Err = std::num::ParseIntError;
26+
fn from_str(s: &str) -> Result<Self, Self::Err> {
27+
let byte = u8::from_str_radix(s, 16)?;
28+
Ok(Self::new_from_control_byte(byte))
29+
}
30+
}
31+
32+
impl fmt::Display for FuzzPk {
33+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { "[fuzz pubkey]".fmt(f) }
34+
}
35+
36+
impl MiniscriptKey for FuzzPk {
37+
type Sha256 = u8;
38+
type Ripemd160 = u8;
39+
type Hash160 = u8;
40+
type Hash256 = u8;
41+
}
42+
43+
impl ToPublicKey for FuzzPk {
44+
fn to_public_key(&self) -> PublicKey {
45+
let secp_pk = secp256k1::PublicKey::from_slice(&[
46+
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x78,
47+
0xce, 0x56, 0x3f, 0x89, 0xa0, 0xed, 0x94, 0x14, 0xf5, 0xaa, 0x28, 0xad, 0x0d, 0x96,
48+
0xd6, 0x79, 0x5f, 0x9c, 0x63, 0x3f, 0x39, 0x79, 0xbf, 0x72, 0xae, 0x82, 0x02, 0x98,
49+
0x3d, 0xc9, 0x89, 0xae, 0xc7, 0xf2, 0xff, 0x2e, 0xd9, 0x1b, 0xdd, 0x69, 0xce, 0x02,
50+
0xfc, 0x07, 0x00, 0xca, 0x10, 0x0e, 0x59, 0xdd, 0xf3,
51+
])
52+
.unwrap();
53+
PublicKey { inner: secp_pk, compressed: self.compressed }
54+
}
55+
56+
fn to_sha256(hash: &Self::Sha256) -> sha256::Hash { sha256::Hash::from_byte_array([*hash; 32]) }
57+
58+
fn to_hash256(hash: &Self::Hash256) -> hash256::Hash {
59+
hash256::Hash::from_byte_array([*hash; 32])
60+
}
61+
62+
fn to_ripemd160(hash: &Self::Ripemd160) -> ripemd160::Hash {
63+
ripemd160::Hash::from_byte_array([*hash; 20])
64+
}
65+
66+
fn to_hash160(hash: &Self::Ripemd160) -> hash160::Hash {
67+
hash160::Hash::from_byte_array([*hash; 20])
68+
}
69+
}
70+
71+
struct FuzzSatisfier<'b> {
72+
idx: AtomicUsize,
73+
buf: &'b [u8],
74+
}
75+
76+
impl FuzzSatisfier<'_> {
77+
fn read_byte(&self) -> Option<u8> {
78+
let idx = self.idx.fetch_add(1, Ordering::SeqCst);
79+
self.buf.get(idx).copied()
80+
}
81+
}
82+
83+
impl Satisfier<FuzzPk> for FuzzSatisfier<'_> {
84+
fn lookup_tap_key_spend_sig(&self) -> Option<Signature> {
85+
let b = self.read_byte()?;
86+
if b & 1 == 1 {
87+
// FIXME in later version of rust-secp we can use from_byte_array
88+
let secp_sig = secp256k1::schnorr::Signature::from_slice(&[0xab; 64]).unwrap();
89+
Some(Signature { signature: secp_sig, sighash_type: TapSighashType::Default })
90+
} else {
91+
None
92+
}
93+
}
94+
95+
fn lookup_tap_leaf_script_sig(&self, _: &FuzzPk, _: &TapLeafHash) -> Option<Signature> {
96+
self.lookup_tap_key_spend_sig()
97+
}
98+
99+
// todo
100+
//fn lookup_tap_control_block_map(
101+
// &self,
102+
//) -> Option<&BTreeMap<ControlBlock, (ScriptBuf, LeafVersion)>>;
103+
104+
#[rustfmt::skip]
105+
fn lookup_raw_pkh_pk(&self, _: &hash160::Hash) -> Option<PublicKey> {
106+
let b = self.read_byte()?;
107+
if b & 1 == 1 {
108+
// Decoding an uncompresssed key is extremely fast, while decoding
109+
// a compressed one is pretty slow.
110+
let secp_pk = secp256k1::PublicKey::from_slice(&[
111+
0x04,
112+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
113+
0x00, 0x00, 0x00, 0x3b, 0x78, 0xce, 0x56, 0x3f,
114+
0x89, 0xa0, 0xed, 0x94, 0x14, 0xf5, 0xaa, 0x28,
115+
0xad, 0x0d, 0x96, 0xd6, 0x79, 0x5f, 0x9c, 0x63,
116+
117+
0x3f, 0x39, 0x79, 0xbf, 0x72, 0xae, 0x82, 0x02,
118+
0x98, 0x3d, 0xc9, 0x89, 0xae, 0xc7, 0xf2, 0xff,
119+
0x2e, 0xd9, 0x1b, 0xdd, 0x69, 0xce, 0x02, 0xfc,
120+
0x07, 0x00, 0xca, 0x10, 0x0e, 0x59, 0xdd, 0xf3,
121+
]).unwrap();
122+
123+
if b & 2 == 2 {
124+
Some(PublicKey::new(secp_pk))
125+
} else{
126+
Some(PublicKey::new_uncompressed(secp_pk))
127+
}
128+
} else {
129+
None
130+
}
131+
}
132+
133+
fn lookup_raw_pkh_x_only_pk(&self, h: &hash160::Hash) -> Option<XOnlyPublicKey> {
134+
self.lookup_raw_pkh_pk(h)
135+
.map(|pk| pk.inner.x_only_public_key().0)
136+
}
137+
138+
// todo
139+
//fn lookup_raw_pkh_ecdsa_sig(&self, h: &hash160::Hash) -> Option<(PublicKey, ecdsa::Signature)>;
140+
141+
fn lookup_raw_pkh_tap_leaf_script_sig(
142+
&self,
143+
(h, _): &(hash160::Hash, TapLeafHash),
144+
) -> Option<(XOnlyPublicKey, Signature)> {
145+
self.lookup_raw_pkh_x_only_pk(h)
146+
.zip(self.lookup_tap_key_spend_sig())
147+
}
148+
149+
fn lookup_sha256(&self, b: &u8) -> Option<[u8; 32]> {
150+
if *b & 1 == 1 {
151+
Some([*b; 32])
152+
} else {
153+
None
154+
}
155+
}
156+
157+
fn lookup_hash256(&self, b: &u8) -> Option<[u8; 32]> {
158+
if *b & 1 == 1 {
159+
Some([*b; 32])
160+
} else {
161+
None
162+
}
163+
}
164+
165+
fn lookup_ripemd160(&self, b: &u8) -> Option<[u8; 32]> {
166+
if *b & 1 == 1 {
167+
Some([*b; 32])
168+
} else {
169+
None
170+
}
171+
}
172+
173+
fn lookup_hash160(&self, b: &u8) -> Option<[u8; 32]> {
174+
if *b & 1 == 1 {
175+
Some([*b; 32])
176+
} else {
177+
None
178+
}
179+
}
180+
181+
fn check_older(&self, t: relative::LockTime) -> bool { t.to_consensus_u32() & 1 == 1 }
182+
183+
fn check_after(&self, t: absolute::LockTime) -> bool { t.to_consensus_u32() & 1 == 1 }
184+
}
185+
186+
fn do_test(data: &[u8]) {
187+
if data.len() < 3 {
188+
return;
189+
}
190+
let control = data[0];
191+
let len = (usize::from(data[1]) << 8) + usize::from(data[2]);
192+
if data.len() < 3 + len {
193+
return;
194+
}
195+
196+
let s = &data[3..3 + len];
197+
let fuzz_sat = FuzzSatisfier { idx: AtomicUsize::new(0), buf: &data[3 + len..] };
198+
199+
let s = String::from_utf8_lossy(s);
200+
if control & 1 == 1 {
201+
let ms = match Miniscript::<FuzzPk, Segwitv0>::from_str(&s) {
202+
Ok(d) => d,
203+
Err(_) => return,
204+
};
205+
206+
let _ = ms.build_template(&fuzz_sat);
207+
} else {
208+
let ms = match Miniscript::<FuzzPk, Tap>::from_str(&s) {
209+
Ok(d) => d,
210+
Err(_) => return,
211+
};
212+
213+
let _ = ms.build_template(&fuzz_sat);
214+
};
215+
}
216+
217+
fn main() {
218+
loop {
219+
fuzz!(|data| {
220+
do_test(data);
221+
});
222+
}
223+
}
224+
225+
#[cfg(test)]
226+
mod tests {
227+
fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
228+
let mut b = 0;
229+
for (idx, c) in hex.as_bytes().iter().enumerate() {
230+
b <<= 4;
231+
match *c {
232+
b'A'..=b'F' => b |= c - b'A' + 10,
233+
b'a'..=b'f' => b |= c - b'a' + 10,
234+
b'0'..=b'9' => b |= c - b'0',
235+
_ => panic!("Bad hex"),
236+
}
237+
if (idx & 1) == 1 {
238+
out.push(b);
239+
b = 0;
240+
}
241+
}
242+
}
243+
244+
#[test]
245+
fn duplicate_crash() {
246+
let mut a = Vec::new();
247+
extend_vec_from_hex("02000b74687265736828312c302956565656565656565656565556565656565656565656565656100e00000000000056565656565656565656000000", &mut a);
248+
super::do_test(&a);
249+
}
250+
}

0 commit comments

Comments
 (0)