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..471e66fe --- /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 = "wallet" +path = "fuzz_targets/wallet_update.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/wallet_update.rs b/fuzz/fuzz_targets/wallet_update.rs new file mode 100644 index 00000000..b6b1c15d --- /dev/null +++ b/fuzz/fuzz_targets/wallet_update.rs @@ -0,0 +1,178 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use std::collections::{BTreeMap, VecDeque}; + +use bdk_wallet::{ + bitcoin::{self, hashes::Hash, BlockHash, Network, Txid}, + chain::{local_chain::CannotConnectError, TxUpdate}, + rusqlite::Connection, + KeychainKind, Update, Wallet, +}; +use bdk_wallet_fuzz::fuzz_utils::*; + +// 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; + +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(INTERNAL_DESCRIPTOR, EXTERNAL_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; + while let Some(action) = consume_action(&mut new_data) { + match action { + WalletAction::ApplyUpdate => { + let mut new_data = data; + create_and_apply_update(&mut new_data, &mut wallet).unwrap() + } + WalletAction::CreateTx => { + let new_data = data; + + // generate fuzzed tx builder + let tx_builder = consume_tx_builder(new_data, &mut wallet); + let tx_builder = match tx_builder { + Some(tx_builder) => tx_builder, + None => continue, + }; + + // 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); + + // 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 + wallet + .persist(&mut db_conn) + .expect("It should always persist successfully!"); + + // generate fuzzed load + wallet = Wallet::load() + .descriptor(KeychainKind::Internal, Some(INTERNAL_DESCRIPTOR)) + .descriptor(KeychainKind::External, Some(EXTERNAL_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); + } + } + } +}); + +fn create_and_apply_update( + data: &mut &[u8], + wallet: &mut Wallet, +) -> Result<(), CannotConnectError> { + let new_data = data; + + // generated fuzzed keychain indices. + let internal_indices = consume_keychain_indices(new_data, KeychainKind::Internal); + let external_indices = consume_keychain_indices(new_data, KeychainKind::External); + + let mut last_active_indices: BTreeMap = BTreeMap::new(); + last_active_indices.extend(internal_indices); + last_active_indices.extend(external_indices); + + // generate fuzzed tx update. + let txs = consume_txs(new_data, wallet); + + let unconfirmed_txids: VecDeque = txs.iter().map(|tx| tx.compute_txid()).collect(); + + let txouts = consume_txouts(new_data); + let anchors = consume_anchors(new_data, unconfirmed_txids.clone()); + let seen_ats = consume_seen_ats(new_data, unconfirmed_txids.clone()); + let evicted_ats = consume_evicted_ats(new_data, unconfirmed_txids.clone()); + + // 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 = consume_checkpoint(new_data, wallet); + + // apply fuzzed update. + let update = Update { + last_active_indices, + tx_update, + chain: Some(chain), + }; + + wallet.apply_update(update) +} + +fn create_and_apply_tx() { + todo!() +} diff --git a/fuzz/src/fuzz_utils.rs b/fuzz/src/fuzz_utils.rs new file mode 100644 index 00000000..3eef0a87 --- /dev/null +++ b/fuzz/src/fuzz_utils.rs @@ -0,0 +1,460 @@ +use std::{ + cmp, + collections::{BTreeMap, BTreeSet, HashSet, VecDeque}, + sync::Arc, +}; + +use bdk_wallet::{ + bitcoin::{ + self, absolute::LockTime, hashes::Hash, transaction::Version, Amount, BlockHash, OutPoint, + Transaction, TxIn, TxOut, Txid, + }, + chain::{BlockId, CheckPoint, ConfirmationBlockTime}, + coin_selection::BranchAndBoundCoinSelection, + signer::TapLeavesOptions, + KeychainKind, SignOptions, TxBuilder, TxOrdering, Wallet, +}; + +use crate::fuzzed_data_provider::{ + consume_bool, consume_byte, consume_bytes, consume_u32, consume_u64, consume_u8, +}; + +pub fn consume_block_hash(data: &mut &[u8]) -> BlockHash { + let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap_or_default(); + + BlockHash::from_byte_array(bytes) +} + +pub fn consume_txid(data: &mut &[u8]) -> Txid { + let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap_or_default(); + + Txid::from_byte_array(bytes) +} + +pub fn consume_keychain_indices( + data: &mut &[u8], + keychain: KeychainKind, +) -> BTreeMap { + let mut indices = BTreeMap::new(); + if consume_bool(data) { + let count = consume_u8(data) as u32; + let start = consume_u8(data) as u32; + indices.extend((start..count).map(|idx| (keychain, idx))) + } + indices +} + +// TODO: (@leonardo) improve this implementation to not rely on UniqueHash +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) + } + } +} + +// TODO: (@leonardo) improve this implementation to not rely on UniqueHash +pub fn consume_txs(mut data: &[u8], wallet: &mut Wallet) -> Vec> { + // TODO: (@leonardo) should this be a usize ? + + let count = consume_u8(&mut data); + let mut txs = Vec::with_capacity(count as usize); + for _ in 0..count { + let version = consume_u32(&mut data); + // TODO: (@leonardo) should we use the Version::consensus_decode instead ? + let version = Version(version as i32); + + let locktime = consume_u32(&mut data); + let locktime = LockTime::from_consensus(locktime); + + let txin_count = consume_u8(&mut data); + let mut tx_inputs = Vec::with_capacity(txin_count as usize); + + for _ in 0..txin_count { + let prev_txid = consume_txid(&mut data); + let prev_vout = consume_u32(&mut data); + let prev_output = OutPoint::new(prev_txid, prev_vout); + let tx_input = TxIn { + previous_output: prev_output, + ..Default::default() + }; + tx_inputs.push(tx_input); + } + + let txout_count = consume_u8(&mut data); + let mut tx_outputs = Vec::with_capacity(txout_count as usize); + + for _ in 0..txout_count { + let spk = consume_spk(&mut data, wallet); + let sats = (consume_u8(&mut data) as u64) * 1_000; + let amount = Amount::from_sat(sats); + let tx_output = TxOut { + value: amount, + script_pubkey: spk, + }; + tx_outputs.push(tx_output); + } + + let tx = Transaction { + version, + lock_time: locktime, + input: tx_inputs, + output: tx_outputs, + }; + + txs.push(tx.into()); + } + txs +} + +pub fn consume_txouts(mut data: &[u8]) -> BTreeMap { + // TODO: (@leonardo) should this be a usize ? + let count = consume_u8(&mut data); + let mut txouts = BTreeMap::new(); + for _ in 0..count { + let prev_txid = consume_txid(&mut data); + let prev_vout = consume_u32(&mut data); + let prev_output = OutPoint::new(prev_txid, prev_vout); + + let sats = (consume_u8(&mut data) as u64) * 1_000; + let amount = Amount::from_sat(sats); + + // TODO: (@leonardo) should we use different spks ? + let txout = TxOut { + value: amount, + script_pubkey: Default::default(), + }; + + txouts.insert(prev_output, txout); + } + txouts +} + +pub fn consume_anchors( + mut data: &[u8], + mut unconfirmed_txids: VecDeque, +) -> BTreeSet<(ConfirmationBlockTime, Txid)> { + let mut anchors = BTreeSet::new(); + + let count = consume_u8(&mut data); + // FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls) + for _ in 0..count { + let block_height = consume_u32(&mut data); + let block_hash = consume_block_hash(&mut data); + + let block_id = BlockId { + height: block_height, + hash: block_hash, + }; + + let confirmation_time = consume_u64(&mut data); + + let anchor = ConfirmationBlockTime { + block_id, + confirmation_time, + }; + + if let Some(txid) = unconfirmed_txids.pop_front() { + anchors.insert((anchor, txid)); + } else { + break; + } + } + anchors +} + +pub fn consume_seen_ats( + mut data: &[u8], + mut unconfirmed_txids: VecDeque, +) -> HashSet<(Txid, u64)> { + let mut seen_ats = HashSet::new(); + + let count = consume_u8(&mut data); + // FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls) + for _ in 0..count { + let time = cmp::min(consume_u64(&mut data), i64::MAX as u64 - 1); + + if let Some(txid) = unconfirmed_txids.pop_front() { + seen_ats.insert((txid, time)); + } else { + let txid = consume_txid(&mut data); + seen_ats.insert((txid, time)); + } + } + seen_ats +} + +pub fn consume_evicted_ats( + mut data: &[u8], + mut unconfirmed_txids: VecDeque, +) -> HashSet<(Txid, u64)> { + let mut evicted_at = HashSet::new(); + + let count = consume_u8(&mut data); + // FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls) + for _ in 0..count { + let time = cmp::min(consume_u64(&mut data), i64::MAX as u64 - 1); + if let Some(txid) = unconfirmed_txids.pop_front() { + evicted_at.insert((txid, time)); + } else { + let txid = consume_txid(&mut data); + evicted_at.insert((txid, time)); + } + } + + evicted_at +} + +pub fn consume_checkpoint(mut data: &[u8], wallet: &mut Wallet) -> CheckPoint { + let mut tip = wallet.latest_checkpoint(); + + let _tip_hash = tip.hash(); + let tip_height = tip.height(); + + let count = consume_u8(&mut data); + // 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(&mut data); + + let block_id = BlockId { height, hash }; + + tip = tip.push(block_id).unwrap(); + } + tip +} + +pub enum WalletAction { + ApplyUpdate, + CreateTx, + PersistAndLoad, +} + +pub fn consume_action(data: &mut &[u8]) -> Option { + if data.is_empty() { + return None; + } + + let byte = consume_byte(data); + + if byte == 0x00 { + Some(WalletAction::ApplyUpdate) + } else if byte == 0x01 { + Some(WalletAction::CreateTx) + } else if byte == 0x02 { + Some(WalletAction::PersistAndLoad) + } else { + None + } +} + +pub fn consume_tx_builder<'a>( + data: &'a [u8], + wallet: &'a mut Wallet, +) -> Option> { + let mut new_data = data; + + let utxo = wallet.list_unspent().next(); + + let recipients_count = consume_byte(&mut new_data) as usize; + let mut recipients = Vec::with_capacity(recipients_count); + for _ in 0..recipients_count { + let spk = consume_spk(&mut new_data, wallet); + let amount = consume_byte(&mut new_data) as u64 * 1_000; + let amount = bitcoin::Amount::from_sat(amount); + recipients.push((spk, amount)); + } + + let drain_to = consume_spk(&mut new_data, wallet); + + let mut tx_builder = match consume_bool(&mut new_data) { + true => wallet.build_tx(), + false => { + 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(_) => return None, + }, + None => return None, + } + } + }; + // let mut tx_builder = match consume_bool(new_data) { + // true => wallet.build_tx(), + // false => { + // match wallet.tx_graph().full_txs().next().map(|tx| tx.txid) { + // let fee_bump + // Some(txid) => match wallet.build_fee_bump(txid) { + // Ok(tx_builder) => tx_builder, + // Err(_) => wallet.build_tx(), + // }, + // None => return None, + // } + // } + // }; + + if consume_bool(&mut new_data) { + let mut rate = consume_byte(&mut new_data) as u64; + if consume_bool(&mut new_data) { + rate *= 1_000; + } + let rate = + bitcoin::FeeRate::from_sat_per_vb(rate).expect("the `FeeRate` should be within range!"); + tx_builder.fee_rate(rate); + } + + if consume_bool(&mut new_data) { + // FIXME: this can't be * 100 as as i initially set it to be as rust-bitcoin + // panics internally on overflowing Amount additions. + let mut fee = consume_byte(&mut new_data) as u64; + if consume_bool(&mut new_data) { + fee *= 1_000; + } + let fee = bitcoin::Amount::from_sat(fee); + tx_builder.fee_absolute(fee); + } + + if consume_bool(&mut new_data) { + if let Some(ref utxo) = utxo { + tx_builder.add_utxo(utxo.outpoint).expect("known utxo."); + } + } + + // FIXME: add the fuzzed option for `TxBuilder.add_foreign_utxo`. + + if consume_bool(&mut new_data) { + tx_builder.manually_selected_only(); + } + + if consume_bool(&mut new_data) { + if let Some(ref utxo) = utxo { + tx_builder.add_unspendable(utxo.outpoint); + } + } + + if consume_bool(&mut new_data) { + let sighash = bitcoin::psbt::PsbtSighashType::from_u32(consume_byte(&mut new_data) as u32); + tx_builder.sighash(sighash); + } + + if consume_bool(&mut new_data) { + let ordering = if consume_bool(&mut new_data) { + TxOrdering::Shuffle + } else { + TxOrdering::Untouched + }; + tx_builder.ordering(ordering); + } + + if consume_bool(&mut new_data) { + let lock_time = consume_u32(&mut new_data); + let lock_time = bitcoin::absolute::LockTime::from_consensus(lock_time); + tx_builder.nlocktime(lock_time); + } + + if consume_bool(&mut new_data) { + let version = consume_u32(&mut new_data); + tx_builder.version(version as i32); + } + + if consume_bool(&mut new_data) { + tx_builder.do_not_spend_change(); + } + + if consume_bool(&mut new_data) { + tx_builder.only_spend_change(); + } + + if consume_bool(&mut new_data) { + tx_builder.only_witness_utxo(); + } + + if consume_bool(&mut new_data) { + tx_builder.include_output_redeem_witness_script(); + } + + if consume_bool(&mut new_data) { + tx_builder.add_global_xpubs(); + } + + if consume_bool(&mut new_data) { + tx_builder.drain_wallet(); + } + + if consume_bool(&mut new_data) { + tx_builder.allow_dust(true); + } + + if consume_bool(&mut new_data) { + tx_builder.set_recipients(recipients); + } + + // FIXME: add the fuzzed option for `TxBuilder.add_data()` method. + + if consume_bool(&mut new_data) { + tx_builder.drain_to(drain_to); + } + + Some(tx_builder) +} + +pub fn consume_sign_options(data: &[u8]) -> SignOptions { + let mut new_data = data; + let mut sign_options = SignOptions::default(); + + if consume_bool(&mut new_data) { + sign_options.trust_witness_utxo = true; + } + + if consume_bool(&mut new_data) { + let height = consume_u32(&mut new_data); + sign_options.assume_height = Some(height); + } + + if consume_bool(&mut new_data) { + sign_options.allow_all_sighashes = true; + } + + if consume_bool(&mut new_data) { + sign_options.try_finalize = false; + } + + if consume_bool(&mut new_data) { + // FIXME: how can we use the other include/exclude variants here ? + if consume_bool(&mut new_data) { + sign_options.tap_leaves_options = TapLeavesOptions::All; + } else { + sign_options.tap_leaves_options = TapLeavesOptions::None; + } + } + + if consume_bool(&mut new_data) { + sign_options.sign_with_tap_internal_key = false; + } + + if consume_bool(&mut new_data) { + sign_options.allow_grinding = false; + } + + sign_options +} diff --git a/fuzz/src/fuzzed_data_provider.rs b/fuzz/src/fuzzed_data_provider.rs new file mode 100644 index 00000000..e94f8e85 --- /dev/null +++ b/fuzz/src/fuzzed_data_provider.rs @@ -0,0 +1,71 @@ +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() +} + +pub fn consume_u64(data: &mut &[u8]) -> u64 { + // We need at least 8 bytes to read a u64 + if data.len() < 8 { + return 0; + } + + let (u64_bytes, rest) = data.split_at(8); + *data = rest; + + u64::from_le_bytes([ + u64_bytes[0], + u64_bytes[1], + u64_bytes[2], + u64_bytes[3], + u64_bytes[4], + u64_bytes[5], + u64_bytes[6], + u64_bytes[7], + ]) +} + +pub fn consume_u32(data: &mut &[u8]) -> u32 { + // We need at least 4 bytes to read a u32 + if data.len() < 4 { + return 0; + } + + let (u32_bytes, rest) = data.split_at(4); + *data = rest; + + u32::from_le_bytes([u32_bytes[0], u32_bytes[1], u32_bytes[2], u32_bytes[3]]) +} + +pub fn consume_u8(data: &mut &[u8]) -> u8 { + // We need at least 1 byte to read a u8 + if data.is_empty() { + return 0; + } + + let (u8_bytes, rest) = data.split_at(1); + *data = rest; + + u8::from_le_bytes([u8_bytes[0]]) +} + +pub fn consume_bool(data: &mut &[u8]) -> bool { + (1 & consume_u8(data)) != 0 +} + +pub fn consume_byte(data: &mut &[u8]) -> u8 { + consume_bytes(data, 1)[0] +} + +#[allow(dead_code)] +fn scale_u32(byte: u8) -> u32 { + (byte as u32) * 0x01000000 +} + +#[allow(dead_code)] +fn scale_u64(byte: u8) -> u64 { + (byte as u64) * 0x0100000000000000 +} 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;