Skip to content

Commit a364073

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 a364073

File tree

1 file changed

+91
-5
lines changed

1 file changed

+91
-5
lines changed

fuzz/src/router.rs

+91-5
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,7 +296,7 @@ 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,
@@ -309,6 +309,91 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
309309
&ProbabilisticScoringFeeParameters::default(),
310310
&random_seed_bytes,
311311
);
312+
if let Ok(route) = route {
313+
// If we generated a route, check that it is valid
314+
// TODO: Check CLTV deltas
315+
assert_eq!(route.route_params.as_ref(), Some(&route_params));
316+
let graph = net_graph.read_only();
317+
let mut blinded_path_payment_amts = new_hash_map();
318+
let mut total_fee = 0;
319+
let mut total_sent = 0;
320+
for path in &route.paths {
321+
total_fee += path.fee_msat();
322+
total_sent += path.final_value_msat();
323+
let unblinded_recipient = path.hops.last().expect("No hops").pubkey;
324+
let mut hops = path.hops.iter().peekable();
325+
let payee = &route_params.payment_params.payee;
326+
'path_check: while let Some(hop) = hops.next() {
327+
if let Some(next) = hops.peek().cloned() {
328+
let amt_sent: u64 = hops.clone().map(|hop| hop.fee_msat).sum();
329+
if let Payee::Clear { route_hints, .. } = payee {
330+
// If we paid to an invoice with clear route hints, check
331+
// whether we pulled from a route hint first, and if not fall
332+
// back to searching through the public network graph.
333+
for hint_path in route_hints.iter() {
334+
let mut hint_hops = hint_path.0.iter().peekable();
335+
while let Some(hint) = hint_hops.next() {
336+
let next_hint_hop_key = hint_hops
337+
.peek()
338+
.map(|hint_hop| hint_hop.src_node_id)
339+
.unwrap_or(unblinded_recipient);
340+
341+
let matches_hint = hint.src_node_id == hop.pubkey
342+
&& hint.short_channel_id == next.short_channel_id
343+
&& next_hint_hop_key == next.pubkey;
344+
let prop = hint.fees.proportional_millionths as u128;
345+
let base = hint.fees.base_msat as u128;
346+
let min_fee = amt_sent as u128 * prop / 1000000 + base;
347+
if matches_hint {
348+
assert!(min_fee <= hop.fee_msat as u128);
349+
continue 'path_check;
350+
}
351+
}
352+
}
353+
}
354+
let chan = graph.channel(hop.short_channel_id).expect("No chan");
355+
assert!(chan.one_to_two.is_some() && chan.two_to_one.is_some());
356+
let fees = if chan.node_one == NodeId::from_pubkey(&hop.pubkey) {
357+
chan.one_to_two.as_ref().unwrap().fees
358+
} else {
359+
chan.two_to_one.as_ref().unwrap().fees
360+
};
361+
let prop_fee = fees.proportional_millionths as u128;
362+
let base_fee = fees.base_msat as u128;
363+
let min_fee = amt_sent as u128 * prop_fee / 1_000_000 + base_fee;
364+
assert!(min_fee <= hop.fee_msat as u128);
365+
} else {
366+
if let Payee::Blinded { route_hints, .. } = payee {
367+
let tail = path.blinded_tail.as_ref().expect("No blinded path");
368+
if tail.hops.len() == 1 {
369+
// We don't consider the payinfo for one-hop blinded paths
370+
// since they're not "real" blinded paths.
371+
continue;
372+
}
373+
// TODO: We should add some kind of coverage of trampoline hops
374+
assert!(tail.trampoline_hops.is_empty());
375+
let hint_filter = |hint: &&BlindedPaymentPath| {
376+
// We store a unique counter in each encrypted_payload.
377+
let hint_id = &hint.blinded_hops()[0].encrypted_payload;
378+
*hint_id == tail.hops[0].encrypted_payload
379+
};
380+
let mut matching_hints = route_hints.iter().filter(hint_filter);
381+
let used_hint = matching_hints.next().unwrap();
382+
assert!(matching_hints.next().is_none());
383+
let key = &tail.hops[0].encrypted_payload;
384+
let used = blinded_path_payment_amts.entry(key).or_insert(0u64);
385+
let blind_intro_amt = tail.final_value_msat + hop.fee_msat;
386+
*used += blind_intro_amt;
387+
assert!(*used <= used_hint.payinfo.htlc_maximum_msat);
388+
assert!(blind_intro_amt >= used_hint.payinfo.htlc_minimum_msat);
389+
}
390+
break;
391+
}
392+
}
393+
}
394+
assert!(total_sent >= final_value_msat);
395+
assert!(total_fee <= route_params.max_total_routing_fee_msat.unwrap());
396+
}
312397
}
313398
};
314399
}
@@ -383,7 +468,8 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
383468
let dummy_pk = PublicKey::from_slice(&[2; 33]).unwrap();
384469
let last_hops: Vec<BlindedPaymentPath> = last_hops_unblinded
385470
.into_iter()
386-
.map(|hint| {
471+
.enumerate()
472+
.map(|(hint_idx, hint)| {
387473
let hop = &hint.0[0];
388474
let payinfo = BlindedPayInfo {
389475
fee_base_msat: hop.fees.base_msat,
@@ -398,7 +484,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
398484
for _ in 0..num_blinded_hops {
399485
blinded_hops.push(BlindedHop {
400486
blinded_node_id: dummy_pk,
401-
encrypted_payload: Vec::new(),
487+
encrypted_payload: hint_idx.to_ne_bytes().to_vec(),
402488
});
403489
}
404490
BlindedPaymentPath::from_raw(

0 commit comments

Comments
 (0)