Skip to content

Commit 3258a59

Browse files
committed
Test generated-route valididty in fuzzing
While our router fuzzer is pretty good at hitting internal assertions in our pathfinder, it doesn't actually do anything to check that the returned route is valid (or meets the requirements given to the pathfinder). Here we add some initial checks covering the feerates of the hops taken in the returned route.
1 parent 76956e4 commit 3258a59

File tree

1 file changed

+86
-7
lines changed

1 file changed

+86
-7
lines changed

fuzz/src/router.rs

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use lightning::ln::channel_state::{ChannelCounterparty, ChannelDetails, ChannelS
1919
use lightning::ln::channelmanager;
2020
use lightning::ln::msgs;
2121
use lightning::ln::types::ChannelId;
22-
use lightning::routing::gossip::{NetworkGraph, RoutingFees};
22+
use lightning::routing::gossip::{NetworkGraph, NodeId, RoutingFees};
2323
use lightning::routing::router::{
24-
find_route, PaymentParameters, RouteHint, RouteHintHop, RouteParameters,
24+
find_route, Payee, PaymentParameters, RouteHint, RouteHintHop, RouteParameters,
2525
};
2626
use lightning::routing::scoring::{
2727
ProbabilisticScorer, ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters,
@@ -296,19 +296,97 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
296296
let final_value_msat = slice_to_be64(get_slice!(8));
297297
let final_cltv_expiry_delta = slice_to_be32(get_slice!(4));
298298
let route_params = $route_params(final_value_msat, final_cltv_expiry_delta, target);
299-
let _ = find_route(
299+
let route = find_route(
300300
&our_pubkey,
301301
&route_params,
302302
&net_graph,
303-
$first_hops
304-
.map(|c| c.iter().collect::<Vec<_>>())
303+
$first_hops.map(|c| c.iter().collect::<Vec<_>>())
305304
.as_ref()
306305
.map(|a| a.as_slice()),
307306
&logger,
308307
&scorer,
309308
&ProbabilisticScoringFeeParameters::default(),
310309
&random_seed_bytes,
311310
);
311+
if let Ok(route) = route {
312+
// If we generated a route, check that it is valid
313+
// TODO: Check CLTV deltas
314+
assert_eq!(route.route_params.as_ref(), Some(&route_params));
315+
let graph = net_graph.read_only();
316+
let mut blinded_path_payment_amts = new_hash_map();
317+
let mut total_fee = 0;
318+
let mut total_sent = 0;
319+
for path in &route.paths {
320+
total_fee += path.fee_msat();
321+
total_sent += path.final_value_msat();
322+
let unblinded_recipient = path.hops.last().expect("No hops").pubkey;
323+
let mut hops = path.hops.iter().peekable();
324+
'path_check: while let Some(hop) = hops.next() {
325+
if let Some(next_hop) = hops.peek().cloned() {
326+
let amt_to_send: u64 = hops.clone().map(|hop| hop.fee_msat).sum();
327+
if let Payee::Clear { route_hints, .. } = &route_params.payment_params.payee {
328+
// If we paid to an invoice with clear route hints, check
329+
// whether we pulled from a route hint first, and if not fall
330+
// back to searching through the public network graph.
331+
for hint in route_hints.iter() {
332+
let mut hint_hops = hint.0.iter().peekable();
333+
while let Some(hint_hop) = hint_hops.next() {
334+
let next_hint_hop_key = hint_hops.peek()
335+
.map(|hint_hop| hint_hop.src_node_id)
336+
.unwrap_or(unblinded_recipient);
337+
338+
let matches_hint = hint_hop.src_node_id == hop.pubkey
339+
&& hint_hop.short_channel_id == next_hop.short_channel_id
340+
&& next_hint_hop_key == next_hop.pubkey;
341+
if matches_hint {
342+
let min_fee = amt_to_send
343+
* (hint_hop.fees.proportional_millionths as u64) / 1_000_000
344+
+ hint_hop.fees.base_msat as u64;
345+
assert!(min_fee <= hop.fee_msat);
346+
continue 'path_check;
347+
}
348+
}
349+
}
350+
}
351+
let chan = graph.channel(hop.short_channel_id).expect("No chan");
352+
assert!(chan.one_to_two.is_some() && chan.two_to_one.is_some());
353+
let fees = if chan.node_one == NodeId::from_pubkey(&hop.pubkey) {
354+
chan.one_to_two.as_ref().unwrap().fees
355+
} else {
356+
chan.two_to_one.as_ref().unwrap().fees
357+
};
358+
let min_fee = amt_to_send * (fees.proportional_millionths as u64) / 1_000_000 + fees.base_msat as u64;
359+
assert!(min_fee <= hop.fee_msat);
360+
} else {
361+
if let Payee::Blinded { route_hints, .. } = &route_params.payment_params.payee {
362+
let tail = path.blinded_tail.as_ref().expect("No blinded path");
363+
if tail.hops.len() == 1 {
364+
// We don't consider the payinfo for one-hop blinded paths
365+
// since they're not "real" blinded paths.
366+
continue;
367+
}
368+
let blinded_intro_amt = tail.final_value_msat + hop.fee_msat;
369+
// TODO: We should add some kind of coverage of trampoline hops
370+
assert!(tail.trampoline_hops.is_empty());
371+
let hint_filter = |hint: &&BlindedPaymentPath| {
372+
hint.blinded_hops()[0].encrypted_payload == tail.hops[0].encrypted_payload
373+
};
374+
let mut matching_hints = route_hints.iter().filter(hint_filter);
375+
let matching_hint = matching_hints.next().unwrap();
376+
assert!(matching_hints.next().is_none());
377+
let key = &tail.hops[0].encrypted_payload;
378+
let used = blinded_path_payment_amts.entry(key).or_insert(0u64);
379+
*used += blinded_intro_amt;
380+
assert!(*used <= matching_hint.payinfo.htlc_maximum_msat);
381+
assert!(blinded_intro_amt >= matching_hint.payinfo.htlc_minimum_msat);
382+
}
383+
break;
384+
}
385+
}
386+
}
387+
assert!(total_sent >= final_value_msat);
388+
assert!(total_fee <= route_params.max_total_routing_fee_msat.unwrap_or(u64::MAX));
389+
}
312390
}
313391
};
314392
}
@@ -383,7 +461,8 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
383461
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
384462
let last_hops: Vec<BlindedPaymentPath> = last_hops_unblinded
385463
.into_iter()
386-
.map(|hint| {
464+
.enumerate()
465+
.map(|(hint_idx, hint)| {
387466
let hop = &hint.0[0];
388467
let payinfo = BlindedPayInfo {
389468
fee_base_msat: hop.fees.base_msat,
@@ -398,7 +477,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
398477
for _ in 0..num_blinded_hops {
399478
blinded_hops.push(BlindedHop {
400479
blinded_node_id: dummy_pk,
401-
encrypted_payload: Vec::new(),
480+
encrypted_payload: hint_idx.to_ne_bytes().to_vec(),
402481
});
403482
}
404483
BlindedPaymentPath::from_raw(

0 commit comments

Comments
 (0)