Skip to content

Commit 2e5f57d

Browse files
committed
Improve fuzz test coverage by building a resolver
1 parent 1eb8621 commit 2e5f57d

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

fuzz/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ stdin_fuzz = []
1616

1717
[dependencies]
1818
bitcoin = { version = "0.32", default-features = false, features = ["std"] }
19+
lightning-invoice = { version = "0.33", default-features = false }
1920
bitcoin-payment-instructions = { path = "../", default-features = false }
2021

2122
afl = { version = "0.12", optional = true }

fuzz/src/parse.rs

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@
99

1010
use bitcoin::Network;
1111

12-
use bitcoin_payment_instructions::hrn_resolution::DummyHrnResolver;
12+
use bitcoin_payment_instructions::amount::Amount;
13+
use bitcoin_payment_instructions::hrn::HumanReadableName;
14+
use bitcoin_payment_instructions::hrn_resolution::{
15+
DummyHrnResolver, HrnResolution, HrnResolutionFuture, HrnResolver, LNURLResolutionFuture,
16+
};
1317
use bitcoin_payment_instructions::PaymentInstructions;
1418

19+
use lightning_invoice::Bolt11Invoice;
20+
1521
use std::future::Future;
1622
use std::pin::Pin;
23+
use std::sync::Mutex;
1724
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
1825

1926
// Emulate Waker::noop until we fuzz on 1.85
@@ -25,13 +32,90 @@ fn clone_fn(p: *const ()) -> RawWaker {
2532

2633
fn dummy_fn(_: *const ()) {}
2734

35+
struct Resolver<'a>(
36+
Mutex<(Option<Result<HrnResolution, &'static str>>, Option<Result<&'a [u8], &'static str>>)>,
37+
);
38+
39+
impl HrnResolver for Resolver<'_> {
40+
fn resolve_hrn<'a>(&'a self, _: &'a HumanReadableName) -> HrnResolutionFuture<'a> {
41+
Box::pin(async {
42+
let us = self.0.lock().unwrap();
43+
us.0.take().unwrap()
44+
})
45+
}
46+
47+
fn resolve_lnurl<'a>(&'a self, _: String, _: Amount, _: [u8; 32]) -> LNURLResolutionFuture<'a> {
48+
Box::pin(async {
49+
let us = self.0.lock().unwrap();
50+
Bolt11Invoice::from_str(us.1.take().unwrap()).map_err(|_| "Failed to parse invoice")
51+
})
52+
}
53+
}
54+
2855
#[inline]
29-
pub fn do_test(data: &[u8]) {
56+
pub fn do_test(mut data: &[u8]) {
57+
if data.len() < 2 {
58+
return;
59+
}
60+
61+
let mut bolt11 = None;
62+
63+
let resolution = if (data[0] & 0b1100_0000) == 0b1100_0000 {
64+
Err("HRN resolution failed in fuzzing")
65+
} else if (data[0] & 0b1100_0000) == 0b1000_0000 {
66+
let result_len = (((data[0] & 0b0011_1111) as usize) << 8) | (data[1] as usize);
67+
if data.len() <= result_len + 2 {
68+
return;
69+
}
70+
let result = if let Ok(s) = String::from_utf8(data[2..result_len + 2].to_vec()) {
71+
s
72+
} else {
73+
return;
74+
};
75+
76+
data = &data[result_len + 2..];
77+
Ok(HrnResolution::DNSSEC { result, proof: Some(vec![8; 32]) })
78+
} else {
79+
if data.len() <= 16 + 2 {
80+
return;
81+
}
82+
let lnurl_resolution_fails = (data[0] & 0b1100_0000) == 0b0100_0000;
83+
let min = Amount::from_milli_sats(u64::from_le_bytes((&data[..8]).try_into().unwrap()));
84+
data = &data[8..];
85+
let max = Amount::from_milli_sats(u64::from_le_bytes((&data[..8]).try_into().unwrap()));
86+
data = &data[8..];
87+
88+
let bolt11_len = ((data[0] as usize) << 8) | (data[1] as usize);
89+
if data.len() <= bolt11_len + 2 {
90+
return;
91+
}
92+
93+
bolt11 = if lnurl_resolution_fails {
94+
Some(Err("LNURL resolution failed in fuzzing"))
95+
} else {
96+
Some(Ok(&data[2..bolt11_len + 2]))
97+
};
98+
data = &data[bolt11_len + 2..];
99+
100+
let mut expected_description_hash = [0; 32];
101+
expected_description_hash[31] = 42;
102+
103+
Ok(HrnResolution::LNURLPay {
104+
min_value: if let Ok(min) = min { min } else { return },
105+
max_value: if let Ok(max) = max { max } else { return },
106+
expected_description_hash,
107+
recipient_description: Some("Payment in fuzzing".to_owned()),
108+
callback: "https://callback.uri/in/fuzzing".to_owned(),
109+
})
110+
};
111+
112+
let resolver = Resolver(Mutex::new((Some(resolution), bolt11)));
113+
30114
if let Ok(s) = std::str::from_utf8(data) {
31115
let waker = unsafe { Waker::from_raw(clone_fn(std::ptr::null())) };
32116

33-
let fut = PaymentInstructions::parse(s, Network::Bitcoin, &DummyHrnResolver, true);
34-
// With a DummyHrnResolver, all instructions should resolve on the first `poll`.
117+
let fut = PaymentInstructions::parse(s, Network::Bitcoin, &resolver, true);
118+
// With our resolver, all instructions should resolve on the first `poll`.
35119
let res = Future::poll(Pin::new(&mut Box::pin(fut)), &mut Context::from_waker(&waker));
36120
assert!(matches!(res, Poll::Ready(_)));
37121

0 commit comments

Comments
 (0)