Skip to content

feat: add initial fuzz testing #271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ jobs:
env:
MATRIX_RUST_VERSION: ${{ matrix.rust.version }}
run: |
cargo build --workspace --exclude 'example_*' ${{ matrix.features }}
cargo test --workspace --exclude 'example_*' ${{ matrix.features }}
cargo build --workspace --exclude 'example_*' --exclude 'bdk_wallet_fuzz' ${{ matrix.features }}
cargo test --workspace --exclude 'example_*' --exclude 'bdk_wallet_fuzz' ${{ matrix.features }}

check-no-std:
needs: prepare
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/cron_daily_fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
on:
schedule:
- cron: "00 05 * * *" # At 05:00 (UTC) every day.
workflow_dispatch: # allows manual triggering

permissions: {}

name: Daily Fuzz

jobs:
fuzz:
name: Cargo Fuzz
runs-on: ubuntu-latest
env:
# The version of `cargo-fuzz` to install and use.
CARGO_FUZZ_VERSION: 0.13.1

# The number of seconds to run the fuzz target. 1800 seconds = 30 minutes.
FUZZ_TIME: 1800

strategy:
fail-fast: false
matrix:
include:
- fuzz_target: bdk_wallet

steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Install the nightly Rust channel
uses: actions-rs/toolchain@v1

Check failure

Code scanning / zizmor

action is not pinned to a hash (required by blanket policy) Error

action is not pinned to a hash (required by blanket policy)
with:
toolchain: nightly
override: true
profile: minimal

- name: Check cache for cargo-fuzz
id: cache-cargo-fuzz
uses: actions/cache@v4
with:
path: ${{ runner.tool_cache }}/cargo-fuzz
key: cargo-fuzz-bin-${{ env.CARGO_FUZZ_VERSION }}

- name: Install cargo-fuzz
if: steps.cache-cargo-fuzz.outputs.cache-hit != 'true'
run: |
cargo install --root "${{ runner.tool_cache }}/cargo-fuzz" --version $CARGO_FUZZ_VERSION cargo-fuzz --locked
env:
CARGO_FUZZ_VERSION: ${{ env.CARGO_FUZZ_VERSION }}

- name: Add cargo-fuzz to PATH
run: echo "${{ runner.tool_cache }}/cargo-fuzz/bin" >> $GITHUB_PATH

- name: Build & Run Fuzz Target
run: |
cargo fuzz build ${{ matrix.fuzz_target }}
cargo fuzz run ${{ matrix.fuzz_target }} -- -max_total_time=$FUZZ_TIME
env:
FUZZ_TIME: ${{ env.FUZZ_TIME }}

- name: Upload fuzzing artifacts on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: fuzzing-artifacts-${{ matrix.fuzz_target }}-${{ github.sha }}
path: fuzz/artifacts

# TODO: add a verify-execution job similar to rust-bitcoin's one
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ Cargo.lock
# Example persisted files.
*.db
*.sqlite*

# fuzz testing related
fuzz/target
fuzz/corpus
fuzz/artifacts
fuzz/coverage
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"wallet",
"fuzz",
"examples/example_wallet_electrum",
"examples/example_wallet_esplora_blocking",
"examples/example_wallet_esplora_async",
Expand Down
26 changes: 26 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "bdk_wallet_fuzz"
homepage = "https://bitcoindevkit.org"
version = "0.0.1-alpha.0"
repository = "https://github.com/bitcoindevkit/bdk_wallet"
description = "A fuzz testing library for the Bitcoin Development Kit Wallet"
keywords = ["fuzz", "testing", "fuzzing", "bitcoin", "wallet"]
publish = false
readme = "README.md"
license = "MIT OR Apache-2.0"
authors = ["Bitcoin Dev Kit Developers"]
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
bdk_wallet = { path = "../wallet", features = ["rusqlite"] }

[[bin]]
name = "bdk_wallet"
path = "fuzz_targets/bdk_wallet.rs"
test = false
doc = false
bench = false
9 changes: 9 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Fuzzing

## How does it work ?

## How do I run the fuzz tests locally ?

## How do I add a new fuzz test target ?

## How do I reproduce a crashing fuzz test ?
202 changes: 202 additions & 0 deletions fuzz/fuzz_targets/bdk_wallet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use std::{
cmp,
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
};

use bdk_wallet::{
bitcoin::{self, hashes::Hash as _, BlockHash, Network, Txid},
chain::{BlockId, ConfirmationBlockTime, TxUpdate},
rusqlite::Connection,
signer::TapLeavesOptions,
KeychainKind, SignOptions, TxOrdering, Update, Wallet,
};

use bdk_wallet::bitcoin::{
absolute::LockTime, transaction::Version, Amount, OutPoint, Transaction, TxIn, TxOut,
};

use bdk_wallet_fuzz::{
fuzz_utils::*, try_consume_anchors, try_consume_bool, try_consume_byte, try_consume_checkpoint,
try_consume_seen_or_evicted_ats, try_consume_sign_options, try_consume_tx_builder,
try_consume_txouts, try_consume_txs, try_consume_u32, try_consume_u64, try_consume_u8,
};

// descriptors
const INTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
const EXTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";

// network
const NETWORK: Network = Network::Testnet;

pub enum WalletAction {
ApplyUpdate,
CreateTx,
PersistAndLoad,
}

impl WalletAction {
fn from_byte(byte: &u8) -> Option<WalletAction> {
if *byte == 0x00 {
Some(WalletAction::ApplyUpdate)
} else if *byte == 0x01 {
Some(WalletAction::CreateTx)
} else if *byte == 0x02 {
Some(WalletAction::PersistAndLoad)
} else {
None
}
}
}

fuzz_target!(|data: &[u8]| {
// creates initial wallet.
let mut db_conn = Connection::open_in_memory()
.expect("Should start an in-memory database connection successfully!");
let wallet = Wallet::create(EXTERNAL_DESCRIPTOR, INTERNAL_DESCRIPTOR)
.network(NETWORK)
.create_wallet(&mut db_conn);

// asserts that the wallet creation did not fail.
let mut wallet = match wallet {
Ok(wallet) => wallet,
Err(_) => return,
};

// fuzzed code goes here.
let mut new_data = data;
let mut data_iter = new_data.iter();
while let Some(wallet_action) = WalletAction::from_byte(try_consume_byte!(data_iter)) {
match wallet_action {
WalletAction::ApplyUpdate => {
// generated fuzzed keychain indices.
let mut last_active_indices: BTreeMap<KeychainKind, u32> = BTreeMap::new();
for keychain in [KeychainKind::Internal, KeychainKind::External] {
if try_consume_bool!(data_iter) {
let count = try_consume_u8!(data_iter) as u32;
let start = try_consume_u8!(data_iter) as u32;
last_active_indices.extend((start..count).map(|idx| (keychain, idx)))
}
}

// generate fuzzed tx update.
let txs: Vec<std::sync::Arc<Transaction>> =
try_consume_txs!(&mut new_data, &mut wallet);

let mut unconfirmed_txids: VecDeque<Txid> =
txs.iter().map(|tx| tx.compute_txid()).collect();

let txouts = try_consume_txouts!(&mut new_data);
let anchors = try_consume_anchors!(&mut new_data, unconfirmed_txids);
let seen_ats = try_consume_seen_or_evicted_ats!(&mut new_data, unconfirmed_txids);
let evicted_ats =
try_consume_seen_or_evicted_ats!(&mut new_data, unconfirmed_txids);

// build the tx update with fuzzed data
let mut tx_update = TxUpdate::default();
tx_update.txs = txs;
tx_update.txouts = txouts;
tx_update.anchors = anchors;
tx_update.seen_ats = seen_ats;
tx_update.evicted_ats = evicted_ats;

// generate fuzzed chain.
let chain = try_consume_checkpoint!(&mut new_data, wallet);

// apply fuzzed update.
let update = Update {
last_active_indices,
tx_update,
chain: Some(chain),
};

wallet.apply_update(update).unwrap();
}
WalletAction::CreateTx => {
// generate fuzzed tx builder
let tx_builder = try_consume_tx_builder!(&mut new_data, &mut wallet);

// generate fuzzed psbt
let mut psbt = match tx_builder.finish() {
Ok(psbt) => psbt,
Err(_) => continue,
};

// generate fuzzed sign options
// let sign_options = consume_sign_options(new_data);
let sign_options = try_consume_sign_options!(data_iter);

// generate fuzzed signed psbt
let _is_signed = match wallet.sign(&mut psbt, sign_options.clone()) {
Ok(is_signed) => is_signed,
Err(_) => continue,
};

// generated fuzzed finalized psbt
// extract and apply fuzzed tx
match wallet.finalize_psbt(&mut psbt, sign_options) {
Ok(is_finalized) => match is_finalized {
true => match psbt.extract_tx() {
Ok(tx) => {
let mut update = Update::default();
update.tx_update.txs.push(tx.into());
wallet.apply_update(update).unwrap()
}
Err(e) => {
assert!(matches!(
e,
bitcoin::psbt::ExtractTxError::AbsurdFeeRate { .. }
));
return;
}
},
false => continue,
},
Err(_) => continue,
}
}
WalletAction::PersistAndLoad => {
let expected_balance = wallet.balance();
let expected_internal_index = wallet.next_derivation_index(KeychainKind::Internal);
let expected_external_index = wallet.next_derivation_index(KeychainKind::External);
let expected_tip = wallet.latest_checkpoint();
let expected_genesis_hash =
BlockHash::from_byte_array(NETWORK.chain_hash().to_bytes());

// generate fuzzed persist
if let Err(e) = wallet.persist(&mut db_conn) {
assert!(
matches!(e, bdk_wallet::rusqlite::Error::ToSqlConversionFailure(..)),
"It should always persist successfully!"
);
return;
};

// generate fuzzed load
wallet = Wallet::load()
.descriptor(KeychainKind::External, Some(EXTERNAL_DESCRIPTOR))
.descriptor(KeychainKind::Internal, Some(INTERNAL_DESCRIPTOR))
.check_network(NETWORK)
.check_genesis_hash(expected_genesis_hash)
.load_wallet(&mut db_conn)
.expect("It should always load from persistence successfully!")
.expect("It should load the wallet successfully!");

// verify the persisted data is accurate
assert_eq!(wallet.network(), NETWORK);
assert_eq!(wallet.balance(), expected_balance);
assert_eq!(
wallet.next_derivation_index(KeychainKind::Internal),
expected_internal_index
);
assert_eq!(
wallet.next_derivation_index(KeychainKind::External),
expected_external_index
);
assert_eq!(wallet.latest_checkpoint(), expected_tip);
}
}
}
});
Loading
Loading