Skip to content

Commit 45164f6

Browse files
committed
refactor(fuzz): extract shared harness library
- Move input generators into arbitrary module, generic over the checkpoint data type - Move differential changeset and checkpoint-order assertions into checks module - Add fuzz_main! macro generating AFL/honggfuzz/libFuzzer entry points and a corpus-replay fallback main - Replace integer op dispatch with explicit Op enums - Remove leftover debug println
1 parent 86fdfd8 commit 45164f6

7 files changed

Lines changed: 372 additions & 367 deletions

File tree

fuzz/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ members = ["."]
1212

1313
[dependencies]
1414
libfuzzer-sys = { version = "0.4", optional = true }
15-
arbitrary = "1.4.1"
15+
arbitrary = { version = "1.4.1", features = ["derive"] }
1616
honggfuzz = { version = "0.5.61", optional = true }
1717
afl = { version = "0.18.2", optional = true }
1818
bdk_chain = { path = "../crates/chain" }
Lines changed: 49 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,33 @@
11
#![cfg_attr(feature = "libfuzzer_fuzz", no_main)]
22

3-
use std::collections::BTreeMap;
4-
5-
use arbitrary::{Arbitrary, Unstructured};
6-
use bdk_chain::bitcoin::block::{Header, Version};
73
use bdk_chain::bitcoin::hashes::Hash;
8-
use bdk_chain::bitcoin::{BlockHash, CompactTarget, TxMerkleNode};
9-
use bdk_chain::local_chain::{ChangeSet, LocalChain, MissingGenesisError};
10-
use bdk_chain::{BlockId, CheckPoint};
11-
12-
fn arbitrary_hash(u: &mut Unstructured) -> arbitrary::Result<BlockHash> {
13-
Ok(BlockHash::from_byte_array(<[u8; 32]>::arbitrary(u)?))
14-
}
15-
16-
/// Builds a header for `apply_header(_connected_to)`. Its `prev_blockhash` is usually taken
17-
/// from an existing checkpoint (so the header connects to the chain), sometimes arbitrary.
18-
fn arbitrary_header(u: &mut Unstructured, chain: &LocalChain) -> arbitrary::Result<(Header, u32)> {
19-
let (prev_blockhash, height) = if u.ratio(3, 4)? {
20-
let block_ids: Vec<BlockId> = chain.iter_checkpoints().map(|cp| cp.block_id()).collect();
21-
let connect_at = u.choose(&block_ids)?;
22-
(connect_at.hash, connect_at.height.saturating_add(1))
23-
} else {
24-
(arbitrary_hash(u)?, u32::arbitrary(u)?)
25-
};
26-
let header = Header {
27-
version: Version::from_consensus(i32::arbitrary(u)?),
28-
prev_blockhash,
29-
merkle_root: TxMerkleNode::from_byte_array(<[u8; 32]>::arbitrary(u)?),
30-
time: u32::arbitrary(u)?,
31-
bits: CompactTarget::from_consensus(u32::arbitrary(u)?),
32-
nonce: u32::arbitrary(u)?,
33-
};
34-
Ok((header, height))
4+
use bdk_chain::bitcoin::BlockHash;
5+
use bdk_chain::local_chain::{LocalChain, MissingGenesisError};
6+
use bdk_chain::BlockId;
7+
use bdk_chain_fuzz::arbitrary::{self, Arbitrary, Unstructured};
8+
use bdk_chain_fuzz::checks::{assert_changeset_against_chains, assert_checkpoint_order};
9+
10+
/// An operation to perform against the chain under test.
11+
#[derive(Arbitrary, Debug, Clone, Copy)]
12+
enum Op {
13+
/// `apply_update` with an independently constructed chain as the update.
14+
ApplyUpdate,
15+
/// `insert_block` with an arbitrary height and hash.
16+
InsertBlock,
17+
/// `disconnect_from` an existing checkpoint or an arbitrary block id.
18+
DisconnectFrom,
19+
/// `apply_header` with a header that usually connects to an existing checkpoint.
20+
ApplyHeader,
21+
/// `apply_header_connected_to` with an arbitrarily picked connection point.
22+
ApplyHeaderConnectedTo,
23+
/// `apply_update` with an update derived by mutating the chain's own tip, so the
24+
/// update shares `Arc` nodes with the original and exercises `merge_chains`'
25+
/// `eq_ptr` fast path.
26+
ApplyDerivedUpdate,
3527
}
3628

37-
fn arbitrary_chain(u: &mut Unstructured) -> arbitrary::Result<Option<LocalChain>> {
38-
let raw_blocks: BTreeMap<u32, [u8; 32]> = BTreeMap::arbitrary(u)?;
39-
println!("{:#?}", raw_blocks);
40-
let blocks: BTreeMap<u32, BlockHash> = raw_blocks
41-
.into_iter()
42-
.map(|(height, hash)| (height, BlockHash::from_byte_array(hash)))
43-
.collect();
44-
45-
let constructed = match u.int_in_range(0..=2)? {
46-
0 => LocalChain::from_blocks(blocks.clone()).ok(),
47-
1 => {
48-
let changeset = ChangeSet {
49-
blocks: blocks.iter().map(|(&h, &hash)| (h, Some(hash))).collect(),
50-
};
51-
LocalChain::from_changeset(changeset).ok()
52-
}
53-
_ => CheckPoint::from_blocks(blocks.clone())
54-
.ok()
55-
.and_then(|tip| LocalChain::from_tip(tip).ok()),
56-
};
57-
let chain = match constructed {
58-
Some(chain) => chain,
59-
None => return Ok(None),
60-
};
61-
62-
let tip = chain.tip();
63-
let (&tip_height, &tip_hash) = blocks.last_key_value().expect("chain is non-empty");
64-
assert_eq!(tip.block_id().height, tip_height);
65-
assert_eq!(tip.block_id().hash, tip_hash);
66-
assert_eq!(chain.genesis_hash(), blocks[&0]);
67-
68-
Ok(Some(chain))
69-
}
70-
71-
/// Picks a block id for an operation: either a checkpoint of `chain` or an arbitrary one.
72-
fn arbitrary_block_id(u: &mut Unstructured, chain: &LocalChain) -> arbitrary::Result<BlockId> {
73-
if bool::arbitrary(u)? {
74-
let block_ids: Vec<BlockId> = chain.iter_checkpoints().map(|cp| cp.block_id()).collect();
75-
Ok(*u.choose(&block_ids)?)
76-
} else {
77-
Ok(BlockId {
78-
height: u32::arbitrary(u)?,
79-
hash: arbitrary_hash(u)?,
80-
})
81-
}
82-
}
83-
84-
/// On success, `pre`-state plus the returned changeset must reconstruct the post-state.
85-
/// On failure, the chain must be left untouched.
86-
fn check_op_result<E>(pre: LocalChain, post: &LocalChain, result: &Result<ChangeSet, E>) {
87-
match result {
88-
Ok(changeset) => {
89-
let mut reconstructed = pre;
90-
reconstructed
91-
.apply_changeset(changeset)
92-
.expect("applying an op's changeset to the pre-state must succeed");
93-
assert_eq!(&reconstructed, post);
94-
}
95-
Err(_) => assert_eq!(&pre, post, "a failed op must not modify the chain"),
96-
}
97-
}
98-
99-
fn check_chain(chain: &LocalChain) {
100-
let heights: Vec<u32> = chain.iter_checkpoints().map(|cp| cp.height()).collect();
101-
assert!(
102-
heights.windows(2).all(|w| w[0] > w[1]),
103-
"checkpoint heights must be strictly decreasing from tip"
104-
);
105-
assert_eq!(heights.last(), Some(&0), "genesis must be present");
29+
fn assert_chain(chain: &LocalChain) {
30+
assert_checkpoint_order(chain);
10631

10732
let tip = chain.chain_tip();
10833
assert_eq!(tip, chain.tip().block_id());
@@ -137,7 +62,7 @@ fn do_test(data: &[u8]) {
13762
let mut chain: Option<LocalChain> = None;
13863
for _ in 0..op_count {
13964
if chain.is_none() {
140-
match arbitrary_chain(&mut u) {
65+
match arbitrary::blockhash_chain(&mut u) {
14166
Ok(Some(initial)) => chain = Some(initial),
14267
Ok(None) => continue,
14368
Err(_) => break,
@@ -146,30 +71,30 @@ fn do_test(data: &[u8]) {
14671
}
14772
let chain = chain.as_mut().expect("initialized above");
14873

149-
let op = match u.int_in_range::<u8>(0..=5) {
74+
let op = match Op::arbitrary(&mut u) {
15075
Ok(op) => op,
15176
Err(_) => break,
15277
};
15378
let pre = chain.clone();
15479
match op {
155-
0 => {
156-
let update = match arbitrary_chain(&mut u) {
80+
Op::ApplyUpdate => {
81+
let update = match arbitrary::blockhash_chain(&mut u) {
15782
Ok(Some(update)) => update,
15883
Ok(None) => continue,
15984
Err(_) => break,
16085
};
16186
let result = chain.apply_update(update.tip());
162-
check_op_result(pre, chain, &result);
87+
assert_changeset_against_chains(pre, chain, &result);
16388
}
164-
1 => {
89+
Op::InsertBlock => {
16590
let (height, hash) = match u32::arbitrary(&mut u)
166-
.and_then(|height| arbitrary_hash(&mut u).map(|hash| (height, hash)))
91+
.and_then(|height| arbitrary::hash(&mut u).map(|hash| (height, hash)))
16792
{
16893
Ok(block) => block,
16994
Err(_) => break,
17095
};
17196
let result = chain.insert_block(height, hash);
172-
check_op_result(pre, chain, &result);
97+
assert_changeset_against_chains(pre, chain, &result);
17398
match &result {
17499
Ok(_) => {
175100
assert_eq!(chain.get(height).map(|cp| cp.hash()), Some(hash));
@@ -183,13 +108,13 @@ fn do_test(data: &[u8]) {
183108
}
184109
}
185110
}
186-
2 => {
187-
let block_id = match arbitrary_block_id(&mut u, chain) {
111+
Op::DisconnectFrom => {
112+
let block_id = match arbitrary::block_id(&mut u, chain, &[]) {
188113
Ok(block_id) => block_id,
189114
Err(_) => break,
190115
};
191116
let result = chain.disconnect_from(block_id);
192-
check_op_result(pre, chain, &result);
117+
assert_changeset_against_chains(pre, chain, &result);
193118
match &result {
194119
Ok(changeset) if !changeset.blocks.is_empty() => {
195120
assert!(chain.tip().height() < block_id.height);
@@ -201,46 +126,44 @@ fn do_test(data: &[u8]) {
201126
}
202127
}
203128
}
204-
3 => {
205-
let (header, height) = match arbitrary_header(&mut u, chain) {
129+
Op::ApplyHeader => {
130+
let (header, height) = match arbitrary::connectable_header(&mut u, chain) {
206131
Ok(header) => header,
207132
Err(_) => break,
208133
};
209134
let result = chain.apply_header(&header, height);
210-
check_op_result(pre, chain, &result);
135+
assert_changeset_against_chains(pre, chain, &result);
211136
if result.is_ok() {
212137
assert_eq!(
213138
chain.get(height).map(|cp| cp.hash()),
214139
Some(header.block_hash())
215140
);
216141
}
217142
}
218-
4 => {
143+
Op::ApplyHeaderConnectedTo => {
219144
let params: arbitrary::Result<_> = (|| {
220-
let (header, height) = arbitrary_header(&mut u, chain)?;
221-
let connected_to = arbitrary_block_id(&mut u, chain)?;
145+
let (header, height) = arbitrary::connectable_header(&mut u, chain)?;
146+
let connected_to = arbitrary::block_id(&mut u, chain, &[])?;
222147
Ok((header, height, connected_to))
223148
})();
224149
let (header, height, connected_to) = match params {
225150
Ok(params) => params,
226151
Err(_) => break,
227152
};
228153
let result = chain.apply_header_connected_to(&header, height, connected_to);
229-
check_op_result(pre, chain, &result);
154+
assert_changeset_against_chains(pre, chain, &result);
230155
if result.is_ok() {
231156
assert_eq!(
232157
chain.get(height).map(|cp| cp.hash()),
233158
Some(header.block_hash())
234159
);
235160
}
236161
}
237-
_ => {
238-
// Derived update: mutate the chain's own tip so the update shares `Arc`
239-
// nodes with the original, exercising `merge_chains`' `eq_ptr` fast path.
162+
Op::ApplyDerivedUpdate => {
240163
let params: arbitrary::Result<_> = (|| {
241164
let insert = bool::arbitrary(&mut u)?;
242165
let height = u32::arbitrary(&mut u)?;
243-
let hash = arbitrary_hash(&mut u)?;
166+
let hash = arbitrary::hash(&mut u)?;
244167
Ok((insert, height, hash))
245168
})();
246169
let (insert, height, hash) = match params {
@@ -262,54 +185,18 @@ fn do_test(data: &[u8]) {
262185
}
263186
};
264187
let result = chain.apply_update(update_tip);
265-
check_op_result(pre, chain, &result);
188+
assert_changeset_against_chains(pre, chain, &result);
266189
if result.is_ok() {
267190
assert_eq!(chain.get(height).map(|cp| cp.hash()), Some(hash));
268191
}
269192
}
270193
}
271-
check_chain(chain);
194+
assert_chain(chain);
272195
}
273196

274197
if let Some(chain) = chain {
275-
check_chain(&chain);
276-
}
277-
}
278-
279-
#[cfg(feature = "afl_fuzz")]
280-
#[macro_use]
281-
extern crate afl;
282-
#[cfg(feature = "afl_fuzz")]
283-
fn main() {
284-
fuzz!(|data| { do_test(data) });
285-
}
286-
287-
#[cfg(feature = "honggfuzz_fuzz")]
288-
#[macro_use]
289-
extern crate honggfuzz;
290-
#[cfg(feature = "honggfuzz_fuzz")]
291-
fn main() {
292-
loop {
293-
fuzz!(|data| { do_test(data) });
198+
assert_chain(&chain);
294199
}
295200
}
296201

297-
#[cfg(feature = "libfuzzer_fuzz")]
298-
#[macro_use]
299-
extern crate libfuzzer_sys;
300-
#[cfg(feature = "libfuzzer_fuzz")]
301-
fuzz_target!(|data: &[u8]| do_test(data));
302-
303-
/// Replays corpus files passed as arguments. Used for coverage reports and
304-
/// reproducing crashes without a fuzzer attached.
305-
#[cfg(not(any(
306-
feature = "afl_fuzz",
307-
feature = "honggfuzz_fuzz",
308-
feature = "libfuzzer_fuzz"
309-
)))]
310-
fn main() {
311-
for path in std::env::args().skip(1) {
312-
let data = std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"));
313-
do_test(&data);
314-
}
315-
}
202+
bdk_chain_fuzz::fuzz_main!(do_test);

0 commit comments

Comments
 (0)