Skip to content

Commit 8ea8cb6

Browse files
committed
test(fuzzing): add tx creation scenario to fuzz target
- add the `CreateTx` scenario to `bdk_wallet` fuzz target. - add two new macros: `try_consume_tx_builder` and `try_consume_sign_options`, in order to build the specific structures and types required for tx creation, signing and applying to wallet.
1 parent 187321f commit 8ea8cb6

File tree

3 files changed

+233
-62
lines changed

3 files changed

+233
-62
lines changed

fuzz/fuzz_targets/bdk_wallet.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ use std::{
77
};
88

99
use bdk_wallet::{
10-
bitcoin::{hashes::Hash as _, BlockHash, Network, Txid},
10+
bitcoin::{self, hashes::Hash as _, BlockHash, Network, Txid},
1111
chain::{BlockId, ConfirmationBlockTime, TxUpdate},
1212
rusqlite::Connection,
13-
KeychainKind, Update, Wallet,
13+
signer::TapLeavesOptions,
14+
KeychainKind, SignOptions, TxOrdering, Update, Wallet,
1415
};
1516

1617
use bdk_wallet::bitcoin::{
@@ -19,8 +20,8 @@ use bdk_wallet::bitcoin::{
1920

2021
use bdk_wallet_fuzz::{
2122
fuzz_utils::*, try_consume_anchors, try_consume_bool, try_consume_byte, try_consume_checkpoint,
22-
try_consume_seen_or_evicted_ats, try_consume_txouts, try_consume_txs, try_consume_u32,
23-
try_consume_u64, try_consume_u8,
23+
try_consume_seen_or_evicted_ats, try_consume_sign_options, try_consume_tx_builder,
24+
try_consume_txouts, try_consume_txs, try_consume_u32, try_consume_u64, try_consume_u8,
2425
};
2526

2627
// descriptors
@@ -114,8 +115,47 @@ fuzz_target!(|data: &[u8]| {
114115
wallet.apply_update(update).unwrap();
115116
}
116117
WalletAction::CreateTx => {
117-
// todo!()
118-
continue;
118+
// generate fuzzed tx builder
119+
let tx_builder = try_consume_tx_builder!(&mut new_data, &mut wallet);
120+
121+
// generate fuzzed psbt
122+
let mut psbt = match tx_builder.finish() {
123+
Ok(psbt) => psbt,
124+
Err(_) => continue,
125+
};
126+
127+
// generate fuzzed sign options
128+
// let sign_options = consume_sign_options(new_data);
129+
let sign_options = try_consume_sign_options!(data_iter);
130+
131+
// generate fuzzed signed psbt
132+
let _is_signed = match wallet.sign(&mut psbt, sign_options.clone()) {
133+
Ok(is_signed) => is_signed,
134+
Err(_) => continue,
135+
};
136+
137+
// generated fuzzed finalized psbt
138+
// extract and apply fuzzed tx
139+
match wallet.finalize_psbt(&mut psbt, sign_options) {
140+
Ok(is_finalized) => match is_finalized {
141+
true => match psbt.extract_tx() {
142+
Ok(tx) => {
143+
let mut update = Update::default();
144+
update.tx_update.txs.push(tx.into());
145+
wallet.apply_update(update).unwrap()
146+
}
147+
Err(e) => {
148+
assert!(matches!(
149+
e,
150+
bitcoin::psbt::ExtractTxError::AbsurdFeeRate { .. }
151+
));
152+
return;
153+
}
154+
},
155+
false => continue,
156+
},
157+
Err(_) => continue,
158+
}
119159
}
120160
WalletAction::PersistAndLoad => {
121161
let expected_balance = wallet.balance();

fuzz/src/fuzz_utils.rs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,193 @@ macro_rules! try_consume_checkpoint {
166166
}};
167167
}
168168

169+
#[macro_export]
170+
macro_rules! try_consume_sign_options {
171+
($data_iter:expr) => {{
172+
let mut sign_options = SignOptions::default();
173+
174+
if try_consume_bool!($data_iter) {
175+
sign_options.trust_witness_utxo = true;
176+
}
177+
178+
if try_consume_bool!($data_iter) {
179+
let height = try_consume_u32!($data_iter);
180+
sign_options.assume_height = Some(height);
181+
}
182+
183+
if try_consume_bool!($data_iter) {
184+
sign_options.allow_all_sighashes = true;
185+
}
186+
187+
if try_consume_bool!($data_iter) {
188+
sign_options.try_finalize = false;
189+
}
190+
191+
if try_consume_bool!($data_iter) {
192+
// FIXME: how can we use the other include/exclude variants here ?
193+
if try_consume_bool!($data_iter) {
194+
sign_options.tap_leaves_options = TapLeavesOptions::All;
195+
} else {
196+
sign_options.tap_leaves_options = TapLeavesOptions::None;
197+
}
198+
}
199+
200+
if try_consume_bool!($data_iter) {
201+
sign_options.sign_with_tap_internal_key = false;
202+
}
203+
204+
if try_consume_bool!($data_iter) {
205+
sign_options.allow_grinding = false;
206+
}
207+
208+
sign_options
209+
}};
210+
}
211+
212+
#[macro_export]
213+
macro_rules! try_consume_tx_builder {
214+
($data:expr, $wallet:expr) => {{
215+
let mut data_iter = $data.into_iter();
216+
217+
let utxo = $wallet.list_unspent().next();
218+
219+
let recipients_count = *try_consume_byte!(data_iter) as usize;
220+
let mut recipients = Vec::with_capacity(recipients_count);
221+
for _ in 0..recipients_count {
222+
let spk = consume_spk($data, $wallet);
223+
let amount = *try_consume_byte!(data_iter) as u64 * 1_000;
224+
let amount = bitcoin::Amount::from_sat(amount);
225+
recipients.push((spk, amount));
226+
}
227+
228+
let drain_to = consume_spk($data, $wallet);
229+
230+
let mut tx_builder = match try_consume_bool!(data_iter) {
231+
true => $wallet.build_tx(),
232+
false => {
233+
// FIXME: (@leonardo) get a randomized txid.
234+
let txid = $wallet
235+
.tx_graph()
236+
.full_txs()
237+
.next()
238+
.map(|tx_node| tx_node.txid);
239+
match txid {
240+
Some(txid) => match $wallet.build_fee_bump(txid) {
241+
Ok(builder) => builder,
242+
Err(_) => continue,
243+
},
244+
None => continue,
245+
}
246+
}
247+
};
248+
249+
if try_consume_bool!(data_iter) {
250+
let mut rate = *try_consume_byte!(data_iter) as u64;
251+
if try_consume_bool!(data_iter) {
252+
rate *= 1_000;
253+
}
254+
let rate =
255+
bitcoin::FeeRate::from_sat_per_vb(rate).expect("It should be a valid fee rate.");
256+
tx_builder.fee_rate(rate);
257+
}
258+
259+
if try_consume_bool!(data_iter) {
260+
let mut fee = *try_consume_byte!(data_iter) as u64;
261+
if try_consume_bool!(data_iter) {
262+
fee *= 1_000;
263+
}
264+
let fee = bitcoin::Amount::from_sat(fee);
265+
tx_builder.fee_absolute(fee);
266+
}
267+
268+
if try_consume_bool!(data_iter) {
269+
if let Some(ref utxo) = utxo {
270+
tx_builder
271+
.add_utxo(utxo.outpoint)
272+
.expect("It should be a known UTXO.");
273+
}
274+
}
275+
276+
// FIXME: add the fuzzed option for `TxBuilder.add_foreign_utxo`.
277+
278+
if try_consume_bool!(data_iter) {
279+
tx_builder.manually_selected_only();
280+
}
281+
282+
if try_consume_bool!(data_iter) {
283+
if let Some(ref utxo) = utxo {
284+
tx_builder.add_unspendable(utxo.outpoint);
285+
}
286+
}
287+
288+
if try_consume_bool!(data_iter) {
289+
let sighash =
290+
bitcoin::psbt::PsbtSighashType::from_u32(*try_consume_byte!(data_iter) as u32);
291+
tx_builder.sighash(sighash);
292+
}
293+
294+
if try_consume_bool!(data_iter) {
295+
let ordering = if try_consume_bool!(data_iter) {
296+
TxOrdering::Shuffle
297+
} else {
298+
TxOrdering::Untouched
299+
};
300+
tx_builder.ordering(ordering);
301+
}
302+
303+
if try_consume_bool!(data_iter) {
304+
let lock_time = try_consume_u32!(data_iter);
305+
let lock_time = bitcoin::absolute::LockTime::from_consensus(lock_time);
306+
tx_builder.nlocktime(lock_time);
307+
}
308+
309+
if try_consume_bool!(data_iter) {
310+
let version = try_consume_u32!(data_iter);
311+
tx_builder.version(version as i32);
312+
}
313+
314+
if try_consume_bool!(data_iter) {
315+
tx_builder.do_not_spend_change();
316+
}
317+
318+
if try_consume_bool!(data_iter) {
319+
tx_builder.only_spend_change();
320+
}
321+
322+
if try_consume_bool!(data_iter) {
323+
tx_builder.only_witness_utxo();
324+
}
325+
326+
if try_consume_bool!(data_iter) {
327+
tx_builder.include_output_redeem_witness_script();
328+
}
329+
330+
if try_consume_bool!(data_iter) {
331+
tx_builder.add_global_xpubs();
332+
}
333+
334+
if try_consume_bool!(data_iter) {
335+
tx_builder.drain_wallet();
336+
}
337+
338+
if try_consume_bool!(data_iter) {
339+
tx_builder.allow_dust(true);
340+
}
341+
342+
if try_consume_bool!(data_iter) {
343+
tx_builder.set_recipients(recipients);
344+
}
345+
346+
// FIXME: add the fuzzed option for `TxBuilder.add_data()` method.
347+
348+
if try_consume_bool!(data_iter) {
349+
tx_builder.drain_to(drain_to);
350+
}
351+
352+
tx_builder
353+
}};
354+
}
355+
169356
pub fn consume_txid(data: &mut &[u8]) -> Txid {
170357
let bytes: [u8; 32] = consume_bytes(data, 32).try_into().unwrap_or([0; 32]);
171358

fuzz/src/fuzzed_data_provider.rs

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -64,59 +64,3 @@ pub fn consume_bytes(data: &mut &[u8], num_bytes: usize) -> Vec<u8> {
6464

6565
bytes.to_vec()
6666
}
67-
68-
pub fn consume_u64(data: &mut &[u8]) -> u64 {
69-
// We need at least 8 bytes to read a u64
70-
if data.len() < 8 {
71-
return 0;
72-
}
73-
74-
let (u64_bytes, rest) = data.split_at(8);
75-
*data = rest;
76-
77-
u64::from_le_bytes([
78-
u64_bytes[0],
79-
u64_bytes[1],
80-
u64_bytes[2],
81-
u64_bytes[3],
82-
u64_bytes[4],
83-
u64_bytes[5],
84-
u64_bytes[6],
85-
u64_bytes[7],
86-
])
87-
}
88-
89-
pub fn consume_u32(data: &mut &[u8]) -> u32 {
90-
// We need at least 4 bytes to read a u32
91-
if data.len() < 4 {
92-
return 0;
93-
}
94-
95-
let (u32_bytes, rest) = data.split_at(4);
96-
*data = rest;
97-
98-
u32::from_le_bytes([u32_bytes[0], u32_bytes[1], u32_bytes[2], u32_bytes[3]])
99-
}
100-
101-
pub fn consume_u8(data: &mut &[u8]) -> u8 {
102-
// We need at least 1 byte to read a u8
103-
if data.is_empty() {
104-
return 0;
105-
}
106-
107-
let (u8_bytes, rest) = data.split_at(1);
108-
*data = rest;
109-
110-
u8::from_le_bytes([u8_bytes[0]])
111-
}
112-
113-
pub fn consume_bool(data: &mut &[u8]) -> bool {
114-
(1 & consume_u8(data)) != 0
115-
}
116-
117-
pub fn consume_byte(data: &mut &[u8]) -> u8 {
118-
let byte = data[0];
119-
*data = &data[1..];
120-
121-
byte
122-
}

0 commit comments

Comments
 (0)