Skip to content

Commit 6fe3410

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 14d6a62 commit 6fe3410

File tree

8 files changed

+430
-0
lines changed

8 files changed

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

0 commit comments

Comments
 (0)