Skip to content

Commit 2b885dc

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 2b885dc

File tree

8 files changed

+436
-0
lines changed

8 files changed

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

0 commit comments

Comments
 (0)