diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index ef34fa32..40941971 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -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 diff --git a/.github/workflows/cron_daily_fuzz.yml b/.github/workflows/cron_daily_fuzz.yml new file mode 100644 index 00000000..131517f5 --- /dev/null +++ b/.github/workflows/cron_daily_fuzz.yml @@ -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 + 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index e2d4d770..7fbd9dff 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ Cargo.lock # Example persisted files. *.db *.sqlite* + +# fuzz testing related +fuzz/target +fuzz/corpus +fuzz/artifacts +fuzz/coverage diff --git a/Cargo.toml b/Cargo.toml index fbde1ace..2b878054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "wallet", + "fuzz", "examples/example_wallet_electrum", "examples/example_wallet_esplora_blocking", "examples/example_wallet_esplora_async", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..23e57758 --- /dev/null +++ b/fuzz/Cargo.toml @@ -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 diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..39ad998c --- /dev/null +++ b/fuzz/README.md @@ -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 ? diff --git a/fuzz/fuzz_targets/bdk_wallet.rs b/fuzz/fuzz_targets/bdk_wallet.rs new file mode 100644 index 00000000..787a41ba --- /dev/null +++ b/fuzz/fuzz_targets/bdk_wallet.rs @@ -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 { + 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 = 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> = + try_consume_txs!(&mut new_data, &mut wallet); + + let mut unconfirmed_txids: VecDeque = + 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); + } + } + } +}); diff --git a/fuzz/src/fuzz_utils.rs b/fuzz/src/fuzz_utils.rs new file mode 100644 index 00000000..378ff4f8 --- /dev/null +++ b/fuzz/src/fuzz_utils.rs @@ -0,0 +1,389 @@ +use bdk_wallet::{ + bitcoin::{self, hashes::Hash, BlockHash, Txid}, + KeychainKind, Wallet, +}; + +use crate::fuzzed_data_provider::consume_bytes; + +#[macro_export] +macro_rules! try_consume_txs { + ($data:expr, $wallet:expr) => {{ + let mut data_iter = $data.into_iter(); + + let txs_count = try_consume_u8!(data_iter) as usize; + let mut txs = Vec::with_capacity(txs_count); + + for _ in 0..txs_count { + let version = try_consume_u32!(data_iter); + let version = Version(version as i32); + + let lock_time = try_consume_u32!(data_iter); + let lock_time = LockTime::from_consensus(lock_time); + + let txin_count = try_consume_u8!(data_iter) as usize; + let mut input = Vec::with_capacity(txin_count); + + for _ in 0..txin_count { + let prev_txid = consume_txid($data); + let prev_vout = try_consume_u32!(data_iter); + let prev_output = OutPoint::new(prev_txid, prev_vout); + let tx_input = TxIn { + previous_output: prev_output, + ..Default::default() + }; + input.push(tx_input); + } + + let txout_count = try_consume_u8!(data_iter) as usize; + let mut output = Vec::with_capacity(txout_count); + + for _ in 0..txout_count { + let spk = consume_spk($data, $wallet); + let sats = (try_consume_u8!(data_iter) as u64) * 1_000; + let amount = Amount::from_sat(sats); + let tx_output = TxOut { + value: amount, + script_pubkey: spk, + }; + output.push(tx_output); + } + + let tx = Transaction { + version, + lock_time, + input, + output, + }; + + txs.push(tx.into()); + } + txs + }}; +} + +#[macro_export] +macro_rules! try_consume_txouts { + ($data:expr) => {{ + let mut data_iter = $data.into_iter(); + let mut txouts = BTreeMap::new(); + + let txouts_count = try_consume_u8!(data_iter); + for _ in 0..txouts_count { + let prev_txid = consume_txid($data); + let prev_vout = try_consume_u32!(data_iter); + let prev_output = OutPoint::new(prev_txid, prev_vout); + + let sats = (try_consume_u8!(data_iter) as u64) * 1_000; + let amount = Amount::from_sat(sats); + + // TODO: (@leonardo) should it use fuzzed spks ? + let txout = TxOut { + value: amount, + script_pubkey: Default::default(), + }; + + txouts.insert(prev_output, txout); + } + txouts + }}; +} + +#[macro_export] +macro_rules! try_consume_anchors { + ($data:expr, $unconfirmed_txids:expr) => {{ + let mut data_iter = $data.into_iter(); + let mut anchors = BTreeSet::new(); + + let count = try_consume_u8!(data_iter); + for _ in 0..count { + let block_height = try_consume_u32!(data_iter); + let block_hash = consume_block_hash($data); + + let block_id = BlockId { + height: block_height, + hash: block_hash, + }; + + let confirmation_time = try_consume_u64!(data_iter); + + let anchor = ConfirmationBlockTime { + block_id, + confirmation_time, + }; + + if let Some(txid) = $unconfirmed_txids.pop_front() { + anchors.insert((anchor, txid)); + } else { + break; + } + } + anchors + }}; +} + +#[macro_export] +macro_rules! try_consume_seen_or_evicted_ats { + ($data:expr, $unconfirmed_txids:expr) => {{ + let mut data_iter = $data.into_iter(); + let mut seen_or_evicted_ats = HashSet::new(); + + let count = try_consume_u8!(data_iter); + for _ in 0..count { + let time = cmp::min(try_consume_u64!(data_iter), i64::MAX as u64 - 1); + + if let Some(txid) = $unconfirmed_txids.pop_front() { + seen_or_evicted_ats.insert((txid, time)); + } else { + let txid = consume_txid($data); + seen_or_evicted_ats.insert((txid, time)); + } + } + seen_or_evicted_ats + }}; +} + +#[macro_export] +macro_rules! try_consume_checkpoint { + ($data:expr, $wallet:expr) => {{ + let mut data_iter = $data.into_iter(); + + let mut tip = $wallet.latest_checkpoint(); + let _tip_hash = tip.hash(); + let tip_height = tip.height(); + + let count = try_consume_u8!(data_iter); + // FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's + // impls) + for i in 1..count { + let height = tip_height + i as u32; + let hash = consume_block_hash($data); + + let block_id = BlockId { height, hash }; + + tip = tip.push(block_id).unwrap(); + } + tip + }}; +} + +#[macro_export] +macro_rules! try_consume_sign_options { + ($data_iter:expr) => {{ + let mut sign_options = SignOptions::default(); + + if try_consume_bool!($data_iter) { + sign_options.trust_witness_utxo = true; + } + + if try_consume_bool!($data_iter) { + let height = try_consume_u32!($data_iter); + sign_options.assume_height = Some(height); + } + + if try_consume_bool!($data_iter) { + sign_options.allow_all_sighashes = true; + } + + if try_consume_bool!($data_iter) { + sign_options.try_finalize = false; + } + + if try_consume_bool!($data_iter) { + // FIXME: how can we use the other include/exclude variants here ? + if try_consume_bool!($data_iter) { + sign_options.tap_leaves_options = TapLeavesOptions::All; + } else { + sign_options.tap_leaves_options = TapLeavesOptions::None; + } + } + + if try_consume_bool!($data_iter) { + sign_options.sign_with_tap_internal_key = false; + } + + if try_consume_bool!($data_iter) { + sign_options.allow_grinding = false; + } + + sign_options + }}; +} + +#[macro_export] +macro_rules! try_consume_tx_builder { + ($data:expr, $wallet:expr) => {{ + let mut data_iter = $data.into_iter(); + + let utxo = $wallet.list_unspent().next(); + + let recipients_count = *try_consume_byte!(data_iter) as usize; + let mut recipients = Vec::with_capacity(recipients_count); + for _ in 0..recipients_count { + let spk = consume_spk($data, $wallet); + let amount = *try_consume_byte!(data_iter) as u64 * 1_000; + let amount = bitcoin::Amount::from_sat(amount); + recipients.push((spk, amount)); + } + + let drain_to = consume_spk($data, $wallet); + + let mut tx_builder = match try_consume_bool!(data_iter) { + true => $wallet.build_tx(), + false => { + // FIXME: (@leonardo) get a randomized txid. + let txid = $wallet + .tx_graph() + .full_txs() + .next() + .map(|tx_node| tx_node.txid); + match txid { + Some(txid) => match $wallet.build_fee_bump(txid) { + Ok(builder) => builder, + Err(_) => continue, + }, + None => continue, + } + } + }; + + if try_consume_bool!(data_iter) { + let mut rate = *try_consume_byte!(data_iter) as u64; + if try_consume_bool!(data_iter) { + rate *= 1_000; + } + let rate = + bitcoin::FeeRate::from_sat_per_vb(rate).expect("It should be a valid fee rate."); + tx_builder.fee_rate(rate); + } + + if try_consume_bool!(data_iter) { + let mut fee = *try_consume_byte!(data_iter) as u64; + if try_consume_bool!(data_iter) { + fee *= 1_000; + } + let fee = bitcoin::Amount::from_sat(fee); + tx_builder.fee_absolute(fee); + } + + if try_consume_bool!(data_iter) { + if let Some(ref utxo) = utxo { + tx_builder + .add_utxo(utxo.outpoint) + .expect("It should be a known UTXO."); + } + } + + // FIXME: add the fuzzed option for `TxBuilder.add_foreign_utxo`. + + if try_consume_bool!(data_iter) { + tx_builder.manually_selected_only(); + } + + if try_consume_bool!(data_iter) { + if let Some(ref utxo) = utxo { + tx_builder.add_unspendable(utxo.outpoint); + } + } + + if try_consume_bool!(data_iter) { + let sighash = + bitcoin::psbt::PsbtSighashType::from_u32(*try_consume_byte!(data_iter) as u32); + tx_builder.sighash(sighash); + } + + if try_consume_bool!(data_iter) { + let ordering = if try_consume_bool!(data_iter) { + TxOrdering::Shuffle + } else { + TxOrdering::Untouched + }; + tx_builder.ordering(ordering); + } + + if try_consume_bool!(data_iter) { + let lock_time = try_consume_u32!(data_iter); + let lock_time = bitcoin::absolute::LockTime::from_consensus(lock_time); + tx_builder.nlocktime(lock_time); + } + + if try_consume_bool!(data_iter) { + let version = try_consume_u32!(data_iter); + tx_builder.version(version as i32); + } + + if try_consume_bool!(data_iter) { + tx_builder.do_not_spend_change(); + } + + if try_consume_bool!(data_iter) { + tx_builder.only_spend_change(); + } + + if try_consume_bool!(data_iter) { + tx_builder.only_witness_utxo(); + } + + if try_consume_bool!(data_iter) { + tx_builder.include_output_redeem_witness_script(); + } + + if try_consume_bool!(data_iter) { + tx_builder.add_global_xpubs(); + } + + if try_consume_bool!(data_iter) { + tx_builder.drain_wallet(); + } + + if try_consume_bool!(data_iter) { + tx_builder.allow_dust(true); + } + + if try_consume_bool!(data_iter) { + tx_builder.set_recipients(recipients); + } + + // FIXME: add the fuzzed option for `TxBuilder.add_data()` method. + + if try_consume_bool!(data_iter) { + tx_builder.drain_to(drain_to); + } + + tx_builder + }}; +} + +pub fn consume_txid(data: &mut &[u8]) -> Txid { + let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap_or([0; 32]); + + Txid::from_byte_array(bytes) +} + +pub fn consume_spk(data: &mut &[u8], wallet: &mut Wallet) -> bitcoin::ScriptBuf { + if data.is_empty() { + let bytes = consume_bytes(data, 32); + return bitcoin::ScriptBuf::from_bytes(bytes); + } + + let flags = data[0]; + *data = &data[1..]; + + match flags.trailing_zeros() { + 0 => wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(), + 1 => wallet + .next_unused_address(KeychainKind::Internal) + .script_pubkey(), + _ => { + let bytes = consume_bytes(data, 32); + bitcoin::ScriptBuf::from_bytes(bytes) + } + } +} + +pub fn consume_block_hash(data: &mut &[u8]) -> BlockHash { + let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap_or([0; 32]); + + BlockHash::from_byte_array(bytes) +} diff --git a/fuzz/src/fuzzed_data_provider.rs b/fuzz/src/fuzzed_data_provider.rs new file mode 100644 index 00000000..a705ce0a --- /dev/null +++ b/fuzz/src/fuzzed_data_provider.rs @@ -0,0 +1,66 @@ +#[macro_export] +macro_rules! try_consume_byte { + ($data_iter:expr) => { + match $data_iter.next() { + Some(byte) => byte, + None => return, + } + }; +} + +#[macro_export] +macro_rules! try_consume_bool { + ($data_iter:expr) => { + match $data_iter.next() { + Some(byte) => *byte != 0, + None => return, + } + }; +} + +#[macro_export] +macro_rules! try_consume_u8 { + ($data_iter:expr) => { + match $data_iter.next() { + Some(byte) => *byte, + None => return, + } + }; +} + +#[macro_export] +macro_rules! try_consume_u32 { + ($data_iter:expr) => {{ + let mut bytes = [0u8; 4]; + for i in 0..4 { + match $data_iter.next() { + Some(byte) => bytes[i] = *byte, + None => return, + } + } + u32::from_le_bytes(bytes) + }}; +} + +#[macro_export] +macro_rules! try_consume_u64 { + ($data_iter:expr) => {{ + let mut bytes = [0u8; 8]; + for i in 0..8 { + match $data_iter.next() { + Some(byte) => bytes[i] = *byte, + None => return, + } + } + u64::from_le_bytes(bytes) + }}; +} + +pub fn consume_bytes(data: &mut &[u8], num_bytes: usize) -> Vec { + let num_bytes = num_bytes.min(data.len()); + + let (bytes, remaining) = data.split_at(num_bytes); + *data = remaining; + + bytes.to_vec() +} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs new file mode 100644 index 00000000..d52377b3 --- /dev/null +++ b/fuzz/src/lib.rs @@ -0,0 +1,2 @@ +pub mod fuzz_utils; +pub mod fuzzed_data_provider; diff --git a/justfile b/justfile index e7501ea3..d0d26128 100644 --- a/justfile +++ b/justfile @@ -11,7 +11,7 @@ build: # Check code: formatting, compilation, linting, and commit signature check: cargo +nightly fmt --all -- --check - cargo check --workspace --exclude 'example_*' --all-features + cargo check --workspace --exclude 'example_*' --exclude 'bdk_wallet_fuzz' --all-features cargo clippy --all-features --all-targets -- -D warnings @[ "$(git log --pretty='format:%G?' -1 HEAD)" = "N" ] && \ echo "\n⚠️ Unsigned commit: BDK requires that commits be signed." || \ @@ -23,7 +23,7 @@ fmt: # Run all tests on the workspace with all features test: - cargo test --workspace --exclude 'example_*' --all-features + cargo test --workspace --exclude 'example_*' --exclude 'bdk_wallet_fuzz' --all-features # Run pre-push suite: format, check, and test pre-push: fmt check test \ No newline at end of file