Skip to content

Commit 6f58072

Browse files
Merge pull request #2366 from valentinewallace/2023-05-blinded-pathfinding-fuzz
Blinded pathfinding fuzzing
2 parents 0607ed7 + a35b92c commit 6f58072

File tree

3 files changed

+146
-82
lines changed

3 files changed

+146
-82
lines changed

fuzz/src/router.rs

+139-76
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ use bitcoin::blockdata::script::Builder;
1111
use bitcoin::blockdata::transaction::TxOut;
1212
use bitcoin::hash_types::BlockHash;
1313

14+
use lightning::blinded_path::{BlindedHop, BlindedPath};
1415
use lightning::chain::transaction::OutPoint;
1516
use lightning::ln::channelmanager::{self, ChannelDetails, ChannelCounterparty};
17+
use lightning::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
1618
use lightning::ln::msgs;
19+
use lightning::offers::invoice::BlindedPayInfo;
1720
use lightning::routing::gossip::{NetworkGraph, RoutingFees};
1821
use lightning::routing::utxo::{UtxoFuture, UtxoLookup, UtxoLookupError, UtxoResult};
1922
use lightning::routing::router::{find_route, PaymentParameters, RouteHint, RouteHintHop, RouteParameters};
@@ -197,6 +200,91 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
197200
let mut node_pks = HashSet::new();
198201
let mut scid = 42;
199202

203+
macro_rules! first_hops {
204+
($first_hops_vec: expr) => {
205+
match get_slice!(1)[0] {
206+
0 => None,
207+
count => {
208+
for _ in 0..count {
209+
scid += 1;
210+
let rnid = node_pks.iter().skip(u16::from_be_bytes(get_slice!(2).try_into().unwrap()) as usize % node_pks.len()).next().unwrap();
211+
let capacity = u64::from_be_bytes(get_slice!(8).try_into().unwrap());
212+
$first_hops_vec.push(ChannelDetails {
213+
channel_id: [0; 32],
214+
counterparty: ChannelCounterparty {
215+
node_id: *rnid,
216+
features: channelmanager::provided_init_features(&UserConfig::default()),
217+
unspendable_punishment_reserve: 0,
218+
forwarding_info: None,
219+
outbound_htlc_minimum_msat: None,
220+
outbound_htlc_maximum_msat: None,
221+
},
222+
funding_txo: Some(OutPoint { txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), index: 0 }),
223+
channel_type: None,
224+
short_channel_id: Some(scid),
225+
inbound_scid_alias: None,
226+
outbound_scid_alias: None,
227+
channel_value_satoshis: capacity,
228+
user_channel_id: 0, inbound_capacity_msat: 0,
229+
unspendable_punishment_reserve: None,
230+
confirmations_required: None,
231+
confirmations: None,
232+
force_close_spend_delay: None,
233+
is_outbound: true, is_channel_ready: true,
234+
is_usable: true, is_public: true,
235+
balance_msat: 0,
236+
outbound_capacity_msat: capacity.saturating_mul(1000),
237+
next_outbound_htlc_limit_msat: capacity.saturating_mul(1000),
238+
next_outbound_htlc_minimum_msat: 0,
239+
inbound_htlc_minimum_msat: None,
240+
inbound_htlc_maximum_msat: None,
241+
config: None,
242+
feerate_sat_per_1000_weight: None,
243+
channel_shutdown_state: Some(channelmanager::ChannelShutdownState::NotShuttingDown),
244+
});
245+
}
246+
Some(&$first_hops_vec[..])
247+
},
248+
}
249+
}
250+
}
251+
252+
macro_rules! last_hops {
253+
($last_hops: expr) => {
254+
let count = get_slice!(1)[0];
255+
for _ in 0..count {
256+
scid += 1;
257+
let rnid = node_pks.iter().skip(slice_to_be16(get_slice!(2))as usize % node_pks.len()).next().unwrap();
258+
$last_hops.push(RouteHint(vec![RouteHintHop {
259+
src_node_id: *rnid,
260+
short_channel_id: scid,
261+
fees: RoutingFees {
262+
base_msat: slice_to_be32(get_slice!(4)),
263+
proportional_millionths: slice_to_be32(get_slice!(4)),
264+
},
265+
cltv_expiry_delta: slice_to_be16(get_slice!(2)),
266+
htlc_minimum_msat: Some(slice_to_be64(get_slice!(8))),
267+
htlc_maximum_msat: None,
268+
}]));
269+
}
270+
}
271+
}
272+
273+
macro_rules! find_routes {
274+
($first_hops: expr, $node_pks: expr, $route_params: expr) => {
275+
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &net_graph, &logger);
276+
let random_seed_bytes: [u8; 32] = [get_slice!(1)[0]; 32];
277+
for target in $node_pks {
278+
let final_value_msat = slice_to_be64(get_slice!(8));
279+
let final_cltv_expiry_delta = slice_to_be32(get_slice!(4));
280+
let route_params = $route_params(final_value_msat, final_cltv_expiry_delta, target);
281+
let _ = find_route(&our_pubkey, &route_params, &net_graph,
282+
$first_hops.map(|c| c.iter().collect::<Vec<_>>()).as_ref().map(|a| a.as_slice()),
283+
&logger, &scorer, &ProbabilisticScoringFeeParameters::default(), &random_seed_bytes);
284+
}
285+
}
286+
}
287+
200288
loop {
201289
match get_slice!(1)[0] {
202290
0 => {
@@ -230,86 +318,61 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
230318
net_graph.channel_failed_permanent(short_channel_id);
231319
},
232320
_ if node_pks.is_empty() => {},
233-
_ => {
321+
x if x < 250 => {
234322
let mut first_hops_vec = Vec::new();
235-
let first_hops = match get_slice!(1)[0] {
236-
0 => None,
237-
count => {
238-
for _ in 0..count {
239-
scid += 1;
240-
let rnid = node_pks.iter().skip(u16::from_be_bytes(get_slice!(2).try_into().unwrap()) as usize % node_pks.len()).next().unwrap();
241-
let capacity = u64::from_be_bytes(get_slice!(8).try_into().unwrap());
242-
first_hops_vec.push(ChannelDetails {
243-
channel_id: [0; 32],
244-
counterparty: ChannelCounterparty {
245-
node_id: *rnid,
246-
features: channelmanager::provided_init_features(&UserConfig::default()),
247-
unspendable_punishment_reserve: 0,
248-
forwarding_info: None,
249-
outbound_htlc_minimum_msat: None,
250-
outbound_htlc_maximum_msat: None,
251-
},
252-
funding_txo: Some(OutPoint { txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(), index: 0 }),
253-
channel_type: None,
254-
short_channel_id: Some(scid),
255-
inbound_scid_alias: None,
256-
outbound_scid_alias: None,
257-
channel_value_satoshis: capacity,
258-
user_channel_id: 0, inbound_capacity_msat: 0,
259-
unspendable_punishment_reserve: None,
260-
confirmations_required: None,
261-
confirmations: None,
262-
force_close_spend_delay: None,
263-
is_outbound: true, is_channel_ready: true,
264-
is_usable: true, is_public: true,
265-
balance_msat: 0,
266-
outbound_capacity_msat: capacity.saturating_mul(1000),
267-
next_outbound_htlc_limit_msat: capacity.saturating_mul(1000),
268-
next_outbound_htlc_minimum_msat: 0,
269-
inbound_htlc_minimum_msat: None,
270-
inbound_htlc_maximum_msat: None,
271-
config: None,
272-
feerate_sat_per_1000_weight: None,
273-
channel_shutdown_state: Some(channelmanager::ChannelShutdownState::NotShuttingDown),
274-
});
275-
}
276-
Some(&first_hops_vec[..])
277-
},
278-
};
323+
// Use macros here and in the blinded match arm to ensure values are fetched from the fuzz
324+
// input in the same order, for better coverage.
325+
let first_hops = first_hops!(first_hops_vec);
279326
let mut last_hops = Vec::new();
280-
{
281-
let count = get_slice!(1)[0];
282-
for _ in 0..count {
283-
scid += 1;
284-
let rnid = node_pks.iter().skip(slice_to_be16(get_slice!(2))as usize % node_pks.len()).next().unwrap();
285-
last_hops.push(RouteHint(vec![RouteHintHop {
286-
src_node_id: *rnid,
287-
short_channel_id: scid,
288-
fees: RoutingFees {
289-
base_msat: slice_to_be32(get_slice!(4)),
290-
proportional_millionths: slice_to_be32(get_slice!(4)),
291-
},
292-
cltv_expiry_delta: slice_to_be16(get_slice!(2)),
293-
htlc_minimum_msat: Some(slice_to_be64(get_slice!(8))),
294-
htlc_maximum_msat: None,
295-
}]));
296-
}
297-
}
298-
let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &net_graph, &logger);
299-
let random_seed_bytes: [u8; 32] = [get_slice!(1)[0]; 32];
300-
for target in node_pks.iter() {
301-
let final_value_msat = slice_to_be64(get_slice!(8));
302-
let final_cltv_expiry_delta = slice_to_be32(get_slice!(4));
303-
let route_params = RouteParameters {
304-
payment_params: PaymentParameters::from_node_id(*target, final_cltv_expiry_delta)
327+
last_hops!(last_hops);
328+
find_routes!(first_hops, node_pks.iter(), |final_amt, final_delta, target: &PublicKey| {
329+
RouteParameters {
330+
payment_params: PaymentParameters::from_node_id(*target, final_delta)
305331
.with_route_hints(last_hops.clone()).unwrap(),
306-
final_value_msat,
307-
};
308-
let _ = find_route(&our_pubkey, &route_params, &net_graph,
309-
first_hops.map(|c| c.iter().collect::<Vec<_>>()).as_ref().map(|a| a.as_slice()),
310-
&logger, &scorer, &ProbabilisticScoringFeeParameters::default(), &random_seed_bytes);
311-
}
332+
final_value_msat: final_amt,
333+
}
334+
});
312335
},
336+
x => {
337+
let mut first_hops_vec = Vec::new();
338+
let first_hops = first_hops!(first_hops_vec);
339+
let mut last_hops_unblinded = Vec::new();
340+
last_hops!(last_hops_unblinded);
341+
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
342+
let last_hops: Vec<(BlindedPayInfo, BlindedPath)> = last_hops_unblinded.into_iter().map(|hint| {
343+
let hop = &hint.0[0];
344+
let payinfo = BlindedPayInfo {
345+
fee_base_msat: hop.fees.base_msat,
346+
fee_proportional_millionths: hop.fees.proportional_millionths,
347+
htlc_minimum_msat: hop.htlc_minimum_msat.unwrap(),
348+
htlc_maximum_msat: hop.htlc_minimum_msat.unwrap().saturating_mul(100),
349+
cltv_expiry_delta: hop.cltv_expiry_delta,
350+
features: BlindedHopFeatures::empty(),
351+
};
352+
let num_blinded_hops = x % 250;
353+
let mut blinded_hops = Vec::new();
354+
for _ in 0..num_blinded_hops {
355+
blinded_hops.push(BlindedHop {
356+
blinded_node_id: dummy_pk,
357+
encrypted_payload: Vec::new()
358+
});
359+
}
360+
(payinfo, BlindedPath {
361+
introduction_node_id: hop.src_node_id,
362+
blinding_point: dummy_pk,
363+
blinded_hops,
364+
})
365+
}).collect();
366+
let mut features = Bolt12InvoiceFeatures::empty();
367+
features.set_basic_mpp_optional();
368+
find_routes!(first_hops, vec![dummy_pk].iter(), |final_amt, _, _| {
369+
RouteParameters {
370+
payment_params: PaymentParameters::blinded(last_hops.clone())
371+
.with_bolt12_features(features.clone()).unwrap(),
372+
final_value_msat: final_amt,
373+
}
374+
});
375+
}
313376
}
314377
}
315378
}

lightning/src/blinded_path/mod.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,26 @@ pub struct BlindedPath {
3636
/// message or payment's next hop and forward it along.
3737
///
3838
/// [`encrypted_payload`]: BlindedHop::encrypted_payload
39-
pub(crate) introduction_node_id: PublicKey,
39+
pub introduction_node_id: PublicKey,
4040
/// Used by the introduction node to decrypt its [`encrypted_payload`] to forward the onion
4141
/// message or payment.
4242
///
4343
/// [`encrypted_payload`]: BlindedHop::encrypted_payload
44-
pub(crate) blinding_point: PublicKey,
44+
pub blinding_point: PublicKey,
4545
/// The hops composing the blinded path.
46-
pub(crate) blinded_hops: Vec<BlindedHop>,
46+
pub blinded_hops: Vec<BlindedHop>,
4747
}
4848

4949
/// Used to construct the blinded hops portion of a blinded path. These hops cannot be identified
5050
/// by outside observers and thus can be used to hide the identity of the recipient.
5151
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
5252
pub struct BlindedHop {
5353
/// The blinded node id of this hop in a blinded path.
54-
pub(crate) blinded_node_id: PublicKey,
54+
pub blinded_node_id: PublicKey,
5555
/// The encrypted payload intended for this hop in a blinded path.
5656
// The node sending to this blinded path will later encode this payload into the onion packet for
5757
// this hop.
58-
pub(crate) encrypted_payload: Vec<u8>,
58+
pub encrypted_payload: Vec<u8>,
5959
}
6060

6161
impl BlindedPath {

lightning/src/routing/router.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,8 @@ impl PaymentParameters {
656656
.with_expiry_time(invoice.created_at().as_secs().saturating_add(invoice.relative_expiry().as_secs()))
657657
}
658658

659-
fn blinded(blinded_route_hints: Vec<(BlindedPayInfo, BlindedPath)>) -> Self {
659+
/// Creates parameters for paying to a blinded payee from the provided blinded route hints.
660+
pub fn blinded(blinded_route_hints: Vec<(BlindedPayInfo, BlindedPath)>) -> Self {
660661
Self {
661662
payee: Payee::Blinded { route_hints: blinded_route_hints, features: None },
662663
expiry_time: None,

0 commit comments

Comments
 (0)