Skip to content

Commit e33e66c

Browse files
committed
Merge #802: [Taptree API project] a bunch of test improvements cleanups
bb9a76c descriptor: add lifting unit test (Andrew Poelstra) d1dadc6 fuzz: add regression test for taptree behavior (Andrew Poelstra) e0e6338 fuzz: in descriptor parsing regression test, try lifting the result (Andrew Poelstra) 0b29186 fuzz: update README for current Nix instructions (Andrew Poelstra) 1af2b19 fuzz: use FuzzPk in regression_descriptor_parse.rs (Andrew Poelstra) e072944 fuzz: pull FuzzPk out of miniscript_satisfy target (Andrew Poelstra) 7e1c1ba ci: add `taptree_of_horror` to list of examples to run (Andrew Poelstra) Pull request description: I have a new branch prepared which overhauls the `TapTree` API, allowing users with a `tr` descriptor to iterate over all the branches, computing control blocks and leaf hashes as they go, which simplifies non-PSBT implementations of satisfiers (and also simplifies our own PSBT implementation). This is the first collection of patches, which simply adds some fuzz and unit tests covering things that I had trouble with during development. ACKs for top commit: sanket1729: ACK bb9a76c Tree-SHA512: 693962714f2a8227dcbc72d1cd7fec592e3c9e10b5c1ecb830a22aca57fd79c5c70a1d640cfd82d9339b668dfbdebc65157bf55744ab14b76f2ccb1dbb341587
2 parents 0010f1c + bb9a76c commit e33e66c

File tree

9 files changed

+213
-75
lines changed

9 files changed

+213
-75
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ parse_descriptor,
2525
parse_descriptor_priv,
2626
parse_descriptor_secret,
2727
regression_descriptor_parse,
28+
regression_taptree,
2829
roundtrip_concrete,
2930
roundtrip_descriptor,
3031
roundtrip_miniscript_script,

contrib/test_vars.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ FEATURES_WITHOUT_STD="compiler trace serde rand base64"
1212

1313
# Run these examples.
1414
# Note `examples/big` should not be run.
15-
EXAMPLES="htlc:std,compiler parse:std sign_multisig:std verify_tx:std xpub_descriptors:std taproot:std,compiler psbt_sign_finalize:std,base64"
15+
EXAMPLES="htlc:std,compiler parse:std sign_multisig:std verify_tx:std xpub_descriptors:std taproot:std,compiler psbt_sign_finalize:std,base64 taptree_of_horror:std,compiler"

fuzz/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ path = "fuzz_targets/parse_descriptor_secret.rs"
4646
name = "regression_descriptor_parse"
4747
path = "fuzz_targets/regression_descriptor_parse.rs"
4848

49+
[[bin]]
50+
name = "regression_taptree"
51+
path = "fuzz_targets/regression_taptree.rs"
52+
4953
[[bin]]
5054
name = "roundtrip_concrete"
5155
path = "fuzz_targets/roundtrip_concrete.rs"

fuzz/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ To build honggfuzz, you must have libunwind on your system, as well as
1313
libopcodes and libbfd from binutils **2.38** on your system. The most
1414
recently-released binutils 2.39 has changed their API in a breaking way.
1515

16-
On Nix, you can obtain these libraries by running
16+
On Nix, you can obtain these libraries, and disable some hardening flags
17+
which conflict with the way honggfuzz builds its targets, by running
1718

1819
nix-shell -p libopcodes_2_38 -p libunwind
20+
# In the nix-shell run these
21+
NIX_HARDENING_ENABLE=''${NIX_HARDENING_ENABLE/fortify/}
22+
NIX_HARDENING_ENABLE=''${NIX_HARDENING_ENABLE/fortify3/}
1923

2024
and then run fuzz.sh as above.
2125

fuzz/fuzz_targets/miniscript_satisfy.rs

+5-63
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,14 @@
11
#![allow(unexpected_cfgs)]
22

3-
use std::fmt;
4-
use std::str::FromStr;
53
use std::sync::atomic::{AtomicUsize, Ordering};
64

5+
use descriptor_fuzz::FuzzPk;
76
use honggfuzz::fuzz;
8-
use miniscript::bitcoin::hashes::{hash160, ripemd160, sha256, Hash};
7+
use miniscript::bitcoin::hashes::hash160;
98
use miniscript::bitcoin::locktime::{absolute, relative};
109
use miniscript::bitcoin::taproot::Signature;
1110
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-
}
11+
use miniscript::{Miniscript, Satisfier, Segwitv0, Tap};
7012

7113
struct FuzzSatisfier<'b> {
7214
idx: AtomicUsize,
@@ -198,14 +140,14 @@ fn do_test(data: &[u8]) {
198140

199141
let s = String::from_utf8_lossy(s);
200142
if control & 1 == 1 {
201-
let ms = match Miniscript::<FuzzPk, Segwitv0>::from_str(&s) {
143+
let ms = match s.parse::<Miniscript<FuzzPk, Segwitv0>>() {
202144
Ok(d) => d,
203145
Err(_) => return,
204146
};
205147

206148
let _ = ms.build_template(&fuzz_sat);
207149
} else {
208-
let ms = match Miniscript::<FuzzPk, Tap>::from_str(&s) {
150+
let ms = match s.parse::<Miniscript<FuzzPk, Tap>>() {
209151
Ok(d) => d,
210152
Err(_) => return,
211153
};
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use core::str::FromStr;
22

33
use honggfuzz::fuzz;
4-
use miniscript::{Descriptor, DescriptorPublicKey};
5-
use old_miniscript::{Descriptor as OldDescriptor, DescriptorPublicKey as OldDescriptorPublicKey};
4+
use miniscript::Descriptor;
5+
use old_miniscript::Descriptor as OldDescriptor;
66

7-
type Desc = Descriptor<DescriptorPublicKey>;
8-
type OldDesc = OldDescriptor<OldDescriptorPublicKey>;
7+
type Desc = Descriptor<descriptor_fuzz::FuzzPk>;
8+
type OldDesc = OldDescriptor<descriptor_fuzz::FuzzPk>;
99

1010
fn do_test(data: &[u8]) {
1111
let data_str = String::from_utf8_lossy(data);
@@ -14,12 +14,33 @@ fn do_test(data: &[u8]) {
1414
(Ok(x), Err(e)) => panic!("new logic parses {} as {:?}, old fails with {}", data_str, x, e),
1515
(Err(e), Ok(x)) => panic!("old logic parses {} as {:?}, new fails with {}", data_str, x, e),
1616
(Ok(new), Ok(old)) => {
17+
use miniscript::policy::Liftable as _;
18+
use old_miniscript::policy::Liftable as _;
19+
1720
assert_eq!(
1821
old.to_string(),
1922
new.to_string(),
2023
"input {} (left is old, right is new)",
2124
data_str
22-
)
25+
);
26+
27+
match (new.lift(), old.lift()) {
28+
(Err(_), Err(_)) => {}
29+
(Ok(x), Err(e)) => {
30+
panic!("new logic lifts {} as {:?}, old fails with {}", data_str, x, e)
31+
}
32+
(Err(e), Ok(x)) => {
33+
panic!("old logic lifts {} as {:?}, new fails with {}", data_str, x, e)
34+
}
35+
(Ok(new), Ok(old)) => {
36+
assert_eq!(
37+
old.to_string(),
38+
new.to_string(),
39+
"lifted input {} (left is old, right is new)",
40+
data_str
41+
)
42+
}
43+
}
2344
}
2445
}
2546
}
@@ -35,9 +56,5 @@ fn main() {
3556
#[cfg(test)]
3657
mod tests {
3758
#[test]
38-
fn duplicate_crash() {
39-
crate::do_test(
40-
b"tr(02dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd,{1,unun:0})",
41-
)
42-
}
59+
fn duplicate_crash() { crate::do_test(b"tr(d,{0,{0,0}})") }
4360
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use descriptor_fuzz::FuzzPk;
2+
use honggfuzz::fuzz;
3+
use miniscript::descriptor::Tr;
4+
use old_miniscript::descriptor::Tr as OldTr;
5+
6+
fn do_test(data: &[u8]) {
7+
let data_str = String::from_utf8_lossy(data);
8+
match (data_str.parse::<Tr<FuzzPk>>(), data_str.parse::<OldTr<FuzzPk>>()) {
9+
(Err(_), Err(_)) => {}
10+
(Ok(_), Err(_)) => {} // 12.x logic rejects some parses for sanity reasons
11+
(Err(e), Ok(x)) => panic!("old logic parses {} as {:?}, new fails with {}", data_str, x, e),
12+
(Ok(new), Ok(old)) => {
13+
let new_si = new.spend_info();
14+
let old_si = old.spend_info();
15+
assert_eq!(
16+
old_si.internal_key(),
17+
new_si.internal_key(),
18+
"merkle root mismatch (left is old, new is right)",
19+
);
20+
assert_eq!(
21+
old_si.merkle_root(),
22+
new_si.merkle_root(),
23+
"merkle root mismatch (left is old, new is right)",
24+
);
25+
assert_eq!(
26+
old_si.output_key(),
27+
new_si.output_key(),
28+
"merkle root mismatch (left is old, new is right)",
29+
);
30+
}
31+
}
32+
}
33+
34+
fn main() {
35+
loop {
36+
fuzz!(|data| {
37+
do_test(data);
38+
});
39+
}
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
#[test]
45+
fn duplicate_crash() { crate::do_test(b"tr(0,{0,0})"); }
46+
}

fuzz/src/lib.rs

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Written in 2019 by Andrew Poelstra <[email protected]>
2+
// SPDX-License-Identifier: CC0-1.0
3+
4+
//! Miniscript Fuzzing Library
5+
//!
6+
//! This contains data structures and utilities used by the fuzz tests.
7+
8+
use core::{fmt, str};
9+
10+
use miniscript::bitcoin::hashes::{hash160, ripemd160, sha256, Hash};
11+
use miniscript::bitcoin::{secp256k1, PublicKey};
12+
use miniscript::{hash256, MiniscriptKey, ToPublicKey};
13+
14+
/// A public key which is encoded as a single hex byte (two hex characters).
15+
///
16+
/// Implements `ToPublicKey` but (for now) always maps to the same bitcoin public key.
17+
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug, Hash)]
18+
pub struct FuzzPk {
19+
compressed: bool,
20+
}
21+
22+
impl FuzzPk {
23+
pub fn new_from_control_byte(control: u8) -> Self { Self { compressed: control & 1 == 1 } }
24+
}
25+
26+
impl str::FromStr for FuzzPk {
27+
type Err = std::num::ParseIntError;
28+
fn from_str(s: &str) -> Result<Self, Self::Err> {
29+
let byte = u8::from_str_radix(s, 16)?;
30+
Ok(Self::new_from_control_byte(byte))
31+
}
32+
}
33+
34+
impl fmt::Display for FuzzPk {
35+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { "[fuzz pubkey]".fmt(f) }
36+
}
37+
38+
impl MiniscriptKey for FuzzPk {
39+
type Sha256 = u8;
40+
type Ripemd160 = u8;
41+
type Hash160 = u8;
42+
type Hash256 = u8;
43+
}
44+
45+
impl ToPublicKey for FuzzPk {
46+
fn to_public_key(&self) -> PublicKey {
47+
let secp_pk = secp256k1::PublicKey::from_slice(&[
48+
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x78,
49+
0xce, 0x56, 0x3f, 0x89, 0xa0, 0xed, 0x94, 0x14, 0xf5, 0xaa, 0x28, 0xad, 0x0d, 0x96,
50+
0xd6, 0x79, 0x5f, 0x9c, 0x63, 0x3f, 0x39, 0x79, 0xbf, 0x72, 0xae, 0x82, 0x02, 0x98,
51+
0x3d, 0xc9, 0x89, 0xae, 0xc7, 0xf2, 0xff, 0x2e, 0xd9, 0x1b, 0xdd, 0x69, 0xce, 0x02,
52+
0xfc, 0x07, 0x00, 0xca, 0x10, 0x0e, 0x59, 0xdd, 0xf3,
53+
])
54+
.unwrap();
55+
PublicKey { inner: secp_pk, compressed: self.compressed }
56+
}
57+
58+
fn to_sha256(hash: &Self::Sha256) -> sha256::Hash { sha256::Hash::from_byte_array([*hash; 32]) }
59+
60+
fn to_hash256(hash: &Self::Hash256) -> hash256::Hash {
61+
hash256::Hash::from_byte_array([*hash; 32])
62+
}
63+
64+
fn to_ripemd160(hash: &Self::Ripemd160) -> ripemd160::Hash {
65+
ripemd160::Hash::from_byte_array([*hash; 20])
66+
}
67+
68+
fn to_hash160(hash: &Self::Ripemd160) -> hash160::Hash {
69+
hash160::Hash::from_byte_array([*hash; 20])
70+
}
71+
}
72+
73+
impl old_miniscript::MiniscriptKey for FuzzPk {
74+
type Sha256 = u8;
75+
type Ripemd160 = u8;
76+
type Hash160 = u8;
77+
type Hash256 = u8;
78+
}
79+
80+
impl old_miniscript::ToPublicKey for FuzzPk {
81+
fn to_public_key(&self) -> PublicKey {
82+
let secp_pk = secp256k1::PublicKey::from_slice(&[
83+
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3b, 0x78,
84+
0xce, 0x56, 0x3f, 0x89, 0xa0, 0xed, 0x94, 0x14, 0xf5, 0xaa, 0x28, 0xad, 0x0d, 0x96,
85+
0xd6, 0x79, 0x5f, 0x9c, 0x63, 0x3f, 0x39, 0x79, 0xbf, 0x72, 0xae, 0x82, 0x02, 0x98,
86+
0x3d, 0xc9, 0x89, 0xae, 0xc7, 0xf2, 0xff, 0x2e, 0xd9, 0x1b, 0xdd, 0x69, 0xce, 0x02,
87+
0xfc, 0x07, 0x00, 0xca, 0x10, 0x0e, 0x59, 0xdd, 0xf3,
88+
])
89+
.unwrap();
90+
PublicKey { inner: secp_pk, compressed: self.compressed }
91+
}
92+
93+
fn to_sha256(hash: &Self::Sha256) -> sha256::Hash { sha256::Hash::from_byte_array([*hash; 32]) }
94+
95+
fn to_hash256(hash: &Self::Hash256) -> old_miniscript::hash256::Hash {
96+
old_miniscript::hash256::Hash::from_byte_array([*hash; 32])
97+
}
98+
99+
fn to_ripemd160(hash: &Self::Ripemd160) -> ripemd160::Hash {
100+
ripemd160::Hash::from_byte_array([*hash; 20])
101+
}
102+
103+
fn to_hash160(hash: &Self::Ripemd160) -> hash160::Hash {
104+
hash160::Hash::from_byte_array([*hash; 20])
105+
}
106+
}

src/descriptor/mod.rs

+18
Original file line numberDiff line numberDiff line change
@@ -2100,6 +2100,24 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
21002100
.unwrap_err();
21012101
}
21022102

2103+
#[test]
2104+
fn tr_lift() {
2105+
use crate::policy::Liftable as _;
2106+
2107+
// Taproot structure is erased but key order preserved..
2108+
let desc = Descriptor::<String>::from_str("tr(ROOT,{pk(A1),{pk(B1),pk(B2)}})").unwrap();
2109+
let lift = desc.lift().unwrap();
2110+
assert_eq!(lift.to_string(), "or(pk(ROOT),or(pk(A1),pk(B1),pk(B2)))",);
2111+
let desc = Descriptor::<String>::from_str("tr(ROOT,{{pk(A1),pk(B1)},pk(B2)})").unwrap();
2112+
let lift = desc.lift().unwrap();
2113+
assert_eq!(lift.to_string(), "or(pk(ROOT),or(pk(A1),pk(B1),pk(B2)))",);
2114+
2115+
// And normalization happens
2116+
let desc = Descriptor::<String>::from_str("tr(ROOT,{0,{0,0}})").unwrap();
2117+
let lift = desc.lift().unwrap();
2118+
assert_eq!(lift.to_string(), "or(pk(ROOT),UNSATISFIABLE)",);
2119+
}
2120+
21032121
#[test]
21042122
fn test_context_pks() {
21052123
let comp_key = bitcoin::PublicKey::from_str(

0 commit comments

Comments
 (0)