Skip to content

Commit 7b02e89

Browse files
committed
Cache whether a node is a first-hop target in the per-node state
When processing the main loop during routefinding, for each node, we check whether it happens to be our peer in one of our channels. This ensures we never fail to find a route that takes a hop through a private channel of ours, to a private node, then through invoice-provided route hints to reach the ultimate payee. Because this is incredibly hot code, doing a full `HashMap` lookup to check if each node is a first-hop target ends up eating a good chunk of time during routing. Luckily, we can trivially avoid this cost. Because we're already looking up the per-node state in the `dist` map, we can store a bool in each first-hop target's state, avoiding the lookup unless we know its going to succeed. This requires storing a dummy entry in `dist`, which feels somewhat strange, but is ultimately fine as we should never be looking at per-node state unless we've already found a path to that node, updating the fields in doign so.
1 parent 95ad5f0 commit 7b02e89

File tree

1 file changed

+53
-12
lines changed

1 file changed

+53
-12
lines changed

lightning/src/routing/router.rs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,6 +1659,14 @@ struct PathBuildingHop<'a> {
16591659
/// decrease as well. Thus, we have to explicitly track which nodes have been processed and
16601660
/// avoid processing them again.
16611661
was_processed: bool,
1662+
/// When processing a node as the next best-score candidate, we want to quickly check if it is
1663+
/// a direct counterparty of ours, using our local channel information immediately if we can.
1664+
///
1665+
/// In order to do so efficiently, we cache whether a node is a direct counterparty here at the
1666+
/// start of a route-finding pass. Unlike all other fields in this struct, this field is never
1667+
/// updated after being initialized - it is set at the start of a route-finding pass and only
1668+
/// read thereafter.
1669+
is_first_hop_target: bool,
16621670
/// Used to compare channels when choosing the for routing.
16631671
/// Includes paying for the use of a hop and the following hops, as well as
16641672
/// an estimated cost of reaching this hop.
@@ -2452,7 +2460,7 @@ where L::Target: Logger {
24522460
.saturating_add(curr_min);
24532461

24542462
let dist_entry = &mut dist[$candidate.src_node_counter() as usize];
2455-
let mut old_entry = if let Some(hop) = dist_entry {
2463+
let old_entry = if let Some(hop) = dist_entry {
24562464
hop
24572465
} else {
24582466
// If there was previously no known way to access the source node
@@ -2470,6 +2478,7 @@ where L::Target: Logger {
24702478
path_htlc_minimum_msat,
24712479
path_penalty_msat: u64::max_value(),
24722480
was_processed: false,
2481+
is_first_hop_target: false,
24732482
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
24742483
value_contribution_msat,
24752484
});
@@ -2634,12 +2643,14 @@ where L::Target: Logger {
26342643
let fee_to_target_msat;
26352644
let next_hops_path_htlc_minimum_msat;
26362645
let next_hops_path_penalty_msat;
2646+
let is_first_hop_target;
26372647
let skip_node = if let Some(elem) = &mut dist[$node.node_counter as usize] {
26382648
let was_processed = elem.was_processed;
26392649
elem.was_processed = true;
26402650
fee_to_target_msat = elem.total_fee_msat;
26412651
next_hops_path_htlc_minimum_msat = elem.path_htlc_minimum_msat;
26422652
next_hops_path_penalty_msat = elem.path_penalty_msat;
2653+
is_first_hop_target = elem.is_first_hop_target;
26432654
was_processed
26442655
} else {
26452656
// Entries are added to dist in add_entry!() when there is a channel from a node.
@@ -2650,21 +2661,24 @@ where L::Target: Logger {
26502661
fee_to_target_msat = 0;
26512662
next_hops_path_htlc_minimum_msat = 0;
26522663
next_hops_path_penalty_msat = 0;
2664+
is_first_hop_target = false;
26532665
false
26542666
};
26552667

26562668
if !skip_node {
2657-
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
2658-
for details in first_channels {
2659-
debug_assert_eq!(*peer_node_counter, $node.node_counter);
2660-
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
2661-
details, payer_node_id: &our_node_id, payer_node_counter,
2662-
target_node_counter: $node.node_counter,
2663-
});
2664-
add_entry!(&candidate, fee_to_target_msat,
2665-
$next_hops_value_contribution,
2666-
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2667-
$next_hops_cltv_delta, $next_hops_path_length);
2669+
if is_first_hop_target {
2670+
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
2671+
for details in first_channels {
2672+
debug_assert_eq!(*peer_node_counter, $node.node_counter);
2673+
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
2674+
details, payer_node_id: &our_node_id, payer_node_counter,
2675+
target_node_counter: $node.node_counter,
2676+
});
2677+
add_entry!(&candidate, fee_to_target_msat,
2678+
$next_hops_value_contribution,
2679+
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2680+
$next_hops_cltv_delta, $next_hops_path_length);
2681+
}
26682682
}
26692683
}
26702684

@@ -2711,6 +2725,33 @@ where L::Target: Logger {
27112725
for e in dist.iter_mut() {
27122726
*e = None;
27132727
}
2728+
for (_, (chans, peer_node_counter)) in first_hop_targets.iter() {
2729+
// In order to avoid looking up whether each node is a first-hop target, we store a
2730+
// dummy entry in dist for each first-hop target, allowing us to do this lookup for
2731+
// free since we're already looking at the `was_processed` flag.
2732+
//
2733+
// Note that all the fields (except `is_first_hop_target`) will be overwritten whenever
2734+
// we find a path to the target, so are left as dummies here.
2735+
dist[*peer_node_counter as usize] = Some(PathBuildingHop {
2736+
candidate: CandidateRouteHop::FirstHop(FirstHopCandidate {
2737+
details: &chans[0],
2738+
payer_node_id: &our_node_id,
2739+
target_node_counter: u32::max_value(),
2740+
payer_node_counter: u32::max_value(),
2741+
}),
2742+
target_node_counter: None,
2743+
fee_msat: 0,
2744+
next_hops_fee_msat: u64::max_value(),
2745+
hop_use_fee_msat: u64::max_value(),
2746+
total_fee_msat: u64::max_value(),
2747+
path_htlc_minimum_msat: u64::max_value(),
2748+
path_penalty_msat: u64::max_value(),
2749+
was_processed: false,
2750+
is_first_hop_target: true,
2751+
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
2752+
value_contribution_msat: 0,
2753+
});
2754+
}
27142755
hit_minimum_limit = false;
27152756

27162757
// If first hop is a private channel and the only way to reach the payee, this is the only

0 commit comments

Comments
 (0)