Skip to content

Commit 31f422f

Browse files
committed
feat(fuzzing): add new fuzz crate and initial test
- creates a new `fuzz` crate, it's meant to run fuzz testing over bdk_wallet targets, with `cargo fuzz` (libFuzzer). - creates an initial `wallet_update` fuzz target for `bdk_wallet`. - creates an initial `fuzzed_data_provider` and `fuzz_utils` files with useful methods to consume the fuzzed data into `bdk_wallet` API-specific types.
1 parent 4d3116a commit 31f422f

File tree

8 files changed

+423
-0
lines changed

8 files changed

+423
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ Cargo.lock
88
# Example persisted files.
99
*.db
1010
*.sqlite*
11+
12+
# fuzz testing related
13+
fuzz/target
14+
fuzz/corpus
15+
fuzz/artifacts
16+
fuzz/coverage

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
resolver = "2"
33
members = [
44
"wallet",
5+
"fuzz",
56
"examples/example_wallet_electrum",
67
"examples/example_wallet_esplora_blocking",
78
"examples/example_wallet_esplora_async",

fuzz/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "bdk_wallet_fuzz"
3+
homepage = "https://bitcoindevkit.org"
4+
version = "0.0.1-alpha.0"
5+
repository = "https://github.com/bitcoindevkit/bdk_wallet"
6+
description = "A fuzz testing library for the Bitcoin Development Kit Wallet"
7+
keywords = ["fuzz", "testing", "fuzzing", "bitcoin", "wallet"]
8+
publish = false
9+
readme = "README.md"
10+
license = "MIT OR Apache-2.0"
11+
authors = ["Bitcoin Dev Kit Developers"]
12+
edition = "2021"
13+
14+
[package.metadata]
15+
cargo-fuzz = true
16+
17+
[dependencies]
18+
libfuzzer-sys = "0.4"
19+
bdk_wallet = { path = "../wallet" }
20+
21+
[[bin]]
22+
name = "wallet"
23+
path = "fuzz_targets/wallet_update.rs"
24+
test = false
25+
doc = false
26+
bench = false

fuzz/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Fuzzing
2+
3+
## How does it work ?
4+
5+
## How do I run the fuzz tests locally ?
6+
7+
## How do I add a new fuzz test target ?
8+
9+
## How do I reproduce a crashing fuzz test ?

fuzz/fuzz_targets/wallet_update.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#![no_main]
2+
3+
use libfuzzer_sys::fuzz_target;
4+
use std::collections::{BTreeMap, VecDeque};
5+
6+
use bdk_wallet::{
7+
bitcoin::{Network, Txid},
8+
chain::TxUpdate,
9+
descriptor::DescriptorError,
10+
KeychainKind, Update, Wallet,
11+
};
12+
use bdk_wallet_fuzz::fuzz_utils::*;
13+
14+
// descriptors
15+
const INTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
16+
const EXTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
17+
18+
// network
19+
const NETWORK: Network = Network::Testnet;
20+
21+
fuzz_target!(|data: &[u8]| {
22+
// creates initial wallet.
23+
let wallet: Result<Wallet, DescriptorError> =
24+
Wallet::create(INTERNAL_DESCRIPTOR, EXTERNAL_DESCRIPTOR)
25+
.network(NETWORK)
26+
.create_wallet_no_persist();
27+
28+
// asserts that the wallet creation did not fail.
29+
let mut wallet = match wallet {
30+
Ok(wallet) => wallet,
31+
Err(_) => return,
32+
};
33+
34+
// fuzzed code goes here.
35+
let mut new_data = data;
36+
37+
// generated fuzzed keychain indices.
38+
let internal_indices = consume_keychain_indices(&mut new_data, KeychainKind::Internal);
39+
let external_indices = consume_keychain_indices(&mut new_data, KeychainKind::External);
40+
41+
let mut last_active_indices: BTreeMap<KeychainKind, u32> = BTreeMap::new();
42+
last_active_indices.extend(internal_indices);
43+
last_active_indices.extend(external_indices);
44+
45+
// generate fuzzed tx update.
46+
let txs = consume_txs(data, &mut wallet);
47+
48+
let unconfirmed_txids: VecDeque<Txid> = txs.iter().map(|tx| tx.compute_txid()).collect();
49+
50+
let txouts = consume_txouts(data);
51+
let anchors = consume_anchors(data, unconfirmed_txids.clone());
52+
let seen_ats = consume_seen_ats(data, unconfirmed_txids.clone());
53+
let evicted_ats = consume_evicted_ats(data, unconfirmed_txids.clone());
54+
55+
// build the tx update with fuzzed data
56+
let mut tx_update = TxUpdate::default();
57+
tx_update.txs = txs;
58+
tx_update.txouts = txouts;
59+
tx_update.anchors = anchors;
60+
tx_update.seen_ats = seen_ats;
61+
tx_update.evicted_ats = evicted_ats;
62+
63+
// generate fuzzed chain.
64+
let chain = consume_checkpoint(data, &mut wallet);
65+
66+
// apply fuzzed update.
67+
let update = Update {
68+
last_active_indices,
69+
tx_update,
70+
chain: Some(chain),
71+
};
72+
73+
wallet.apply_update(update).unwrap();
74+
});

fuzz/src/fuzz_utils.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
use std::{
2+
cmp,
3+
collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
4+
sync::Arc,
5+
};
6+
7+
use bdk_wallet::{
8+
bitcoin::{
9+
self, absolute::LockTime, hashes::Hash, transaction::Version, Amount, BlockHash, OutPoint,
10+
Transaction, TxIn, TxOut, Txid,
11+
},
12+
chain::{BlockId, CheckPoint, ConfirmationBlockTime},
13+
KeychainKind, Wallet,
14+
};
15+
16+
use crate::fuzzed_data_provider::{
17+
consume_bool, consume_bytes, consume_u32, consume_u64, consume_u8,
18+
};
19+
20+
pub fn consume_block_hash(data: &mut &[u8]) -> BlockHash {
21+
let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap();
22+
BlockHash::from_byte_array(bytes)
23+
}
24+
25+
pub fn consume_txid(data: &mut &[u8]) -> Txid {
26+
let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap();
27+
Txid::from_byte_array(bytes)
28+
}
29+
30+
pub fn consume_keychain_indices(
31+
data: &mut &[u8],
32+
keychain: KeychainKind,
33+
) -> BTreeMap<KeychainKind, u32> {
34+
let mut indices = BTreeMap::new();
35+
if consume_bool(data) {
36+
let count = consume_u8(data) as u32;
37+
let start = consume_u8(data) as u32;
38+
indices.extend((start..count).map(|idx| (keychain, idx)))
39+
}
40+
indices
41+
}
42+
43+
// TODO: (@leonardo) improve this implementation to not rely on UniqueHash
44+
pub fn consume_spk(data: &mut &[u8], wallet: &mut Wallet) -> bitcoin::ScriptBuf {
45+
let flags = data[0];
46+
*data = &data[1..];
47+
48+
match flags.trailing_zeros() {
49+
0 => wallet
50+
.next_unused_address(KeychainKind::External)
51+
.script_pubkey(),
52+
1 => wallet
53+
.next_unused_address(KeychainKind::Internal)
54+
.script_pubkey(),
55+
_ => {
56+
let bytes = consume_bytes(data, 32);
57+
bitcoin::ScriptBuf::from_bytes(bytes)
58+
}
59+
}
60+
}
61+
62+
// TODO: (@leonardo) improve this implementation to not rely on UniqueHash
63+
pub fn consume_txs(mut data: &[u8], wallet: &mut Wallet) -> Vec<Arc<Transaction>> {
64+
// TODO: (@leonardo) should this be a usize ?
65+
66+
let count = consume_u8(&mut data);
67+
let mut txs = Vec::with_capacity(count as usize);
68+
for _ in 0..count {
69+
let version = consume_u32(&mut data);
70+
// TODO: (@leonardo) should we use the Version::consensus_decode instead ?
71+
let version = Version(version as i32);
72+
73+
let locktime = consume_u32(&mut data);
74+
let locktime = LockTime::from_consensus(locktime);
75+
76+
let txin_count = consume_u8(&mut data);
77+
let mut tx_inputs = Vec::with_capacity(txin_count as usize);
78+
79+
for _ in 0..txin_count {
80+
let prev_txid = consume_txid(&mut data);
81+
let prev_vout = consume_u32(&mut data);
82+
let prev_output = OutPoint::new(prev_txid, prev_vout);
83+
let tx_input = TxIn {
84+
previous_output: prev_output,
85+
..Default::default()
86+
};
87+
tx_inputs.push(tx_input);
88+
}
89+
90+
let txout_count = consume_u8(&mut data);
91+
let mut tx_outputs = Vec::with_capacity(txout_count as usize);
92+
93+
for _ in 0..txout_count {
94+
let spk = consume_spk(&mut data, wallet);
95+
let sats = (consume_u8(&mut data) as u64) * 1_000;
96+
let amount = Amount::from_sat(sats);
97+
let tx_output = TxOut {
98+
value: amount,
99+
script_pubkey: spk,
100+
};
101+
tx_outputs.push(tx_output);
102+
}
103+
104+
let tx = Transaction {
105+
version,
106+
lock_time: locktime,
107+
input: tx_inputs,
108+
output: tx_outputs,
109+
};
110+
111+
txs.push(tx.into());
112+
}
113+
txs
114+
}
115+
116+
pub fn consume_txouts(mut data: &[u8]) -> BTreeMap<OutPoint, TxOut> {
117+
// TODO: (@leonardo) should this be a usize ?
118+
let count = consume_u8(&mut data);
119+
let mut txouts = BTreeMap::new();
120+
for _ in 0..count {
121+
let prev_txid = consume_txid(&mut data);
122+
let prev_vout = consume_u32(&mut data);
123+
let prev_output = OutPoint::new(prev_txid, prev_vout);
124+
125+
let sats = (consume_u8(&mut data) as u64) * 1_000;
126+
let amount = Amount::from_sat(sats);
127+
128+
// TODO: (@leonardo) should we use different spks ?
129+
let txout = TxOut {
130+
value: amount,
131+
script_pubkey: Default::default(),
132+
};
133+
134+
txouts.insert(prev_output, txout);
135+
}
136+
txouts
137+
}
138+
139+
pub fn consume_anchors(
140+
mut data: &[u8],
141+
mut unconfirmed_txids: VecDeque<Txid>,
142+
) -> BTreeSet<(ConfirmationBlockTime, Txid)> {
143+
let mut anchors = BTreeSet::new();
144+
145+
let count = consume_u8(&mut data);
146+
// FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls)
147+
for _ in 0..count {
148+
let block_height = consume_u32(&mut data);
149+
let block_hash = consume_block_hash(&mut data);
150+
151+
let block_id = BlockId {
152+
height: block_height,
153+
hash: block_hash,
154+
};
155+
156+
let confirmation_time = consume_u64(&mut data);
157+
158+
let anchor = ConfirmationBlockTime {
159+
block_id,
160+
confirmation_time,
161+
};
162+
163+
if let Some(txid) = unconfirmed_txids.pop_front() {
164+
anchors.insert((anchor, txid));
165+
} else {
166+
break;
167+
}
168+
}
169+
anchors
170+
}
171+
172+
pub fn consume_seen_ats(
173+
mut data: &[u8],
174+
mut unconfirmed_txids: VecDeque<Txid>,
175+
) -> HashSet<(Txid, u64)> {
176+
let mut seen_ats = HashSet::new();
177+
178+
let count = consume_u8(&mut data);
179+
// FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls)
180+
for _ in 0..count {
181+
let time = cmp::min(consume_u64(&mut data), i64::MAX as u64 - 1);
182+
183+
if let Some(txid) = unconfirmed_txids.pop_front() {
184+
seen_ats.insert((txid, time));
185+
} else {
186+
let txid = consume_txid(&mut data);
187+
seen_ats.insert((txid, time));
188+
}
189+
}
190+
seen_ats
191+
}
192+
193+
pub fn consume_evicted_ats(
194+
mut data: &[u8],
195+
mut unconfirmed_txids: VecDeque<Txid>,
196+
) -> HashSet<(Txid, u64)> {
197+
let mut evicted_at = HashSet::new();
198+
199+
let count = consume_u8(&mut data);
200+
// FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls)
201+
for _ in 0..count {
202+
let time = cmp::min(consume_u64(&mut data), i64::MAX as u64 - 1);
203+
if let Some(txid) = unconfirmed_txids.pop_front() {
204+
evicted_at.insert((txid, time));
205+
} else {
206+
let txid = consume_txid(&mut data);
207+
evicted_at.insert((txid, time));
208+
}
209+
}
210+
211+
evicted_at
212+
}
213+
214+
pub fn consume_checkpoint(mut data: &[u8], wallet: &mut Wallet) -> CheckPoint {
215+
let mut tip = wallet.latest_checkpoint();
216+
217+
let _tip_hash = tip.hash();
218+
let tip_height = tip.height();
219+
220+
let count = consume_u8(&mut data);
221+
// FIXME: (@leonardo) should we use while limited by a flag instead ? (as per antoine's impls)
222+
for i in 1..count {
223+
let height = tip_height + i as u32;
224+
let hash = consume_block_hash(&mut data);
225+
226+
let block_id = BlockId { height, hash };
227+
228+
tip = tip.push(block_id).unwrap();
229+
}
230+
tip
231+
}

0 commit comments

Comments
 (0)