-
Notifications
You must be signed in to change notification settings - Fork 36
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
oleonardolima
wants to merge
6
commits into
bitcoindevkit:master
Choose a base branch
from
oleonardolima:feat/add-initial-fuzz-testing
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0f9bd99
feat(fuzzing): add new fuzz crate and initial test
oleonardolima 550925b
test(fuzzing): improve `bdk_wallet` fuzz target
oleonardolima 187321f
test(fuzzing): add persist/load scenario to fuzz target
oleonardolima 8ea8cb6
test(fuzzing): add tx creation scenario to fuzz target
oleonardolima c02f843
chore(ci+justfile): add `bdk_wallet_fuzz` to excluded packages
oleonardolima 7f1fa74
ci(fuzz): add daily fuzz job
oleonardolima File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 failureCode 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
}); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.