Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,70 @@ jobs:
cache: true
- name: Check docs
run: RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps

fuzz:
needs: prepare
name: Fuzz (${{ matrix.fuzzer }}, ${{ matrix.target }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
fuzzer: [afl, honggfuzz, libfuzzer]
target: [local_chain_apply_update, local_chain_apply_update_header]
env:
FUZZ_TARGET: ${{ matrix.target }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
# cargo-fuzz (libFuzzer) requires nightly; AFL and honggfuzz work on stable.
toolchain: ${{ matrix.fuzzer == 'libfuzzer' && 'nightly' || needs.prepare.outputs.rust_version }}
override: true
cache: true
- name: Install honggfuzz build dependencies
if: matrix.fuzzer == 'honggfuzz'
run: sudo apt-get update && sudo apt-get install -y binutils-dev libunwind-dev
- name: Install cargo-afl
if: matrix.fuzzer == 'afl'
run: |
cargo install cargo-afl --force
cargo afl config --build --force
- name: Install cargo-hfuzz
if: matrix.fuzzer == 'honggfuzz'
run: cargo install honggfuzz
- name: Install cargo-fuzz
if: matrix.fuzzer == 'libfuzzer'
run: cargo install cargo-fuzz
- name: Fuzz for 5 minutes (AFL)
if: matrix.fuzzer == 'afl'
working-directory: ./fuzz
env:
AFL_NO_UI: 1
AFL_SKIP_CPUFREQ: 1
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES: 1
run: |
mkdir -p ci-seeds && printf 'bdk-fuzz-seed' > ci-seeds/seed
cargo afl build --features afl_fuzz --bin "$FUZZ_TARGET"
cargo afl fuzz -i ci-seeds -o afl-out -V 300 -- "target/debug/$FUZZ_TARGET"
crashes=$(find afl-out -path '*crashes*' -name 'id:*')
if [ -n "$crashes" ]; then
echo "AFL found crashes:"; echo "$crashes"; exit 1
fi
- name: Fuzz for 5 minutes (honggfuzz)
if: matrix.fuzzer == 'honggfuzz'
working-directory: ./fuzz
env:
HFUZZ_BUILD_ARGS: --features honggfuzz_fuzz
HFUZZ_RUN_ARGS: --run_time 300 --exit_upon_crash -v
run: |
cargo hfuzz run "$FUZZ_TARGET"
if [ -f "hfuzz_workspace/$FUZZ_TARGET/HONGGFUZZ.REPORT.TXT" ]; then
cat "hfuzz_workspace/$FUZZ_TARGET/HONGGFUZZ.REPORT.TXT"; exit 1
fi
- name: Fuzz for 5 minutes (libFuzzer)
if: matrix.fuzzer == 'libfuzzer'
run: cargo fuzz run "$FUZZ_TARGET" --features libfuzzer_fuzz -- -max_total_time=300
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Cargo.lock
*.sqlite*

crates/electrum/target
fuzz/target
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
37 changes: 37 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
name = "bdk_chain-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[workspace]
members = ["."]

[dependencies]
libfuzzer-sys = { version = "0.4", optional = true }
arbitrary = { version = "1.4.1", features = ["derive"] }
honggfuzz = { version = "0.5.61", optional = true }
afl = { version = "0.18.2", optional = true }
bdk_chain = { path = "../crates/chain" }

[features]
afl_fuzz = ["afl"]
honggfuzz_fuzz = ["honggfuzz"]
libfuzzer_fuzz = ["libfuzzer-sys"]

[[bin]]
name = "local_chain_apply_update"
path = "fuzz_targets/local_chain_apply_update.rs"
test = false
doc = false
bench = false

[[bin]]
name = "local_chain_apply_update_header"
path = "fuzz_targets/local_chain_apply_update_header.rs"
test = false
doc = false
bench = false
202 changes: 202 additions & 0 deletions fuzz/fuzz_targets/local_chain_apply_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#![cfg_attr(feature = "libfuzzer_fuzz", no_main)]

use bdk_chain::bitcoin::hashes::Hash;
use bdk_chain::bitcoin::BlockHash;
use bdk_chain::local_chain::{LocalChain, MissingGenesisError};
use bdk_chain::BlockId;
use bdk_chain_fuzz::arbitrary::{self, Arbitrary, Unstructured};
use bdk_chain_fuzz::checks::{assert_changeset_against_chains, assert_checkpoint_order};

/// An operation to perform against the chain under test.
#[derive(Arbitrary, Debug, Clone, Copy)]
enum Op {
/// `apply_update` with an independently constructed chain as the update.
ApplyUpdate,
/// `insert_block` with an arbitrary height and hash.
InsertBlock,
/// `disconnect_from` an existing checkpoint or an arbitrary block id.
DisconnectFrom,
/// `apply_header` with a header that usually connects to an existing checkpoint.
ApplyHeader,
/// `apply_header_connected_to` with an arbitrarily picked connection point.
ApplyHeaderConnectedTo,
/// `apply_update` with an update derived by mutating the chain's own tip, so the
/// update shares `Arc` nodes with the original and exercises `merge_chains`'
/// `eq_ptr` fast path.
ApplyDerivedUpdate,
}

fn assert_chain(chain: &LocalChain) {
assert_checkpoint_order(chain);

let tip = chain.chain_tip();
assert_eq!(tip, chain.tip().block_id());
for cp in chain.iter_checkpoints() {
assert_eq!(
chain.is_block_in_chain(cp.block_id(), tip),
Some(true),
"every checkpoint must be in the chain of its own tip"
);
let mut flipped = cp.hash().to_byte_array();
flipped[0] ^= 1;
let wrong = BlockId {
height: cp.height(),
hash: BlockHash::from_byte_array(flipped),
};
assert_eq!(
chain.is_block_in_chain(wrong, tip),
Some(false),
"a conflicting hash at an occupied height must not be in chain"
);
}
}

fn do_test(data: &[u8]) {
let mut u = Unstructured::new(data);

let op_count = match u.int_in_range(1..=16) {
Ok(count) => count,
Err(_) => return,
};

let mut chain: Option<LocalChain> = None;
for _ in 0..op_count {
if chain.is_none() {
match arbitrary::blockhash_chain(&mut u) {
Ok(Some(initial)) => chain = Some(initial),
Ok(None) => continue,
Err(_) => break,
}
continue;
}
let chain = chain.as_mut().expect("initialized above");

let op = match Op::arbitrary(&mut u) {
Ok(op) => op,
Err(_) => break,
};
let pre = chain.clone();
match op {
Op::ApplyUpdate => {
let update = match arbitrary::blockhash_chain(&mut u) {
Ok(Some(update)) => update,
Ok(None) => continue,
Err(_) => break,
};
let result = chain.apply_update(update.tip());
assert_changeset_against_chains(pre, chain, &result);
}
Op::InsertBlock => {
let (height, hash) = match u32::arbitrary(&mut u)
.and_then(|height| arbitrary::hash(&mut u).map(|hash| (height, hash)))
{
Ok(block) => block,
Err(_) => break,
};
let result = chain.insert_block(height, hash);
assert_changeset_against_chains(pre, chain, &result);
match &result {
Ok(_) => {
assert_eq!(chain.get(height).map(|cp| cp.hash()), Some(hash));
}
Err(err) => {
assert_eq!(
chain.get(err.height).map(|cp| cp.hash()),
Some(err.original_hash),
"insert conflict must report the existing checkpoint"
);
}
}
}
Op::DisconnectFrom => {
let block_id = match arbitrary::block_id(&mut u, chain, &[]) {
Ok(block_id) => block_id,
Err(_) => break,
};
let result = chain.disconnect_from(block_id);
assert_changeset_against_chains(pre, chain, &result);
match &result {
Ok(changeset) if !changeset.blocks.is_empty() => {
assert!(chain.tip().height() < block_id.height);
}
Ok(_) => {}
Err(MissingGenesisError) => {
assert_eq!(block_id.height, 0);
assert_eq!(block_id.hash, chain.genesis_hash());
}
}
}
Op::ApplyHeader => {
let (header, height) = match arbitrary::connectable_header(&mut u, chain) {
Ok(header) => header,
Err(_) => break,
};
let result = chain.apply_header(&header, height);
assert_changeset_against_chains(pre, chain, &result);
if result.is_ok() {
assert_eq!(
chain.get(height).map(|cp| cp.hash()),
Some(header.block_hash())
);
}
}
Op::ApplyHeaderConnectedTo => {
let params: arbitrary::Result<_> = (|| {
let (header, height) = arbitrary::connectable_header(&mut u, chain)?;
let connected_to = arbitrary::block_id(&mut u, chain, &[])?;
Ok((header, height, connected_to))
})();
let (header, height, connected_to) = match params {
Ok(params) => params,
Err(_) => break,
};
let result = chain.apply_header_connected_to(&header, height, connected_to);
assert_changeset_against_chains(pre, chain, &result);
if result.is_ok() {
assert_eq!(
chain.get(height).map(|cp| cp.hash()),
Some(header.block_hash())
);
}
}
Op::ApplyDerivedUpdate => {
let params: arbitrary::Result<_> = (|| {
let insert = bool::arbitrary(&mut u)?;
let height = u32::arbitrary(&mut u)?;
let hash = arbitrary::hash(&mut u)?;
Ok((insert, height, hash))
})();
let (insert, height, hash) = match params {
Ok(params) => params,
Err(_) => break,
};
let (update_tip, height) = if insert {
// Height 0 would panic (genesis is immutable in `CheckPoint::insert`).
let height = height.max(1);
(chain.tip().insert(height, hash), height)
} else {
let height = match chain.tip().height().checked_add(1 + height % 4) {
Some(height) => height,
None => continue,
};
match chain.tip().extend([(height, hash)]) {
Ok(tip) => (tip, height),
Err(_) => continue,
}
};
let result = chain.apply_update(update_tip);
assert_changeset_against_chains(pre, chain, &result);
if result.is_ok() {
assert_eq!(chain.get(height).map(|cp| cp.hash()), Some(hash));
}
}
}
assert_chain(chain);
}

if let Some(chain) = chain {
assert_chain(&chain);
}
}

bdk_chain_fuzz::fuzz_main!(do_test);
Loading