Skip to content

Commit df9c15d

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 78c0eaa commit df9c15d

File tree

1 file changed

+52
-11
lines changed

1 file changed

+52
-11
lines changed

lightning/src/routing/router.rs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,14 @@ struct PathBuildingHop<'a> {
17721772
/// decrease as well. Thus, we have to explicitly track which nodes have been processed and
17731773
/// avoid processing them again.
17741774
was_processed: bool,
1775+
/// When processing a node as the next best-score candidate, we want to quickly check if it is
1776+
/// a direct counterparty of ours, using our local channel information immediately if we can.
1777+
///
1778+
/// In order to do so efficiently, we cache whether a node is a direct counterparty here at the
1779+
/// start of a route-finding pass. Unlike all other fields in this struct, this field is never
1780+
/// updated after being initialized - it is set at the start of a route-finding pass and only
1781+
/// read thereafter.
1782+
is_first_hop_target: bool,
17751783
/// Used to compare channels when choosing the for routing.
17761784
/// Includes paying for the use of a hop and the following hops, as well as
17771785
/// an estimated cost of reaching this hop.
@@ -1810,6 +1818,7 @@ impl<'a> core::fmt::Debug for PathBuildingHop<'a> {
18101818
.field("source_node_id", &self.candidate.source())
18111819
.field("target_node_id", &self.candidate.target())
18121820
.field("short_channel_id", &self.candidate.short_channel_id())
1821+
.field("is_first_hop_target", &self.is_first_hop_target)
18131822
.field("total_fee_msat", &self.total_fee_msat)
18141823
.field("next_hops_fee_msat", &self.next_hops_fee_msat)
18151824
.field("hop_use_fee_msat", &self.hop_use_fee_msat)
@@ -2516,6 +2525,7 @@ where L::Target: Logger {
25162525
path_htlc_minimum_msat,
25172526
path_penalty_msat: u64::max_value(),
25182527
was_processed: false,
2528+
is_first_hop_target: false,
25192529
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
25202530
value_contribution_msat,
25212531
});
@@ -2679,12 +2689,14 @@ where L::Target: Logger {
26792689
let fee_to_target_msat;
26802690
let next_hops_path_htlc_minimum_msat;
26812691
let next_hops_path_penalty_msat;
2692+
let is_first_hop_target;
26822693
let skip_node = if let Some(elem) = &mut dist[$node.node_counter as usize] {
26832694
let was_processed = elem.was_processed;
26842695
elem.was_processed = true;
26852696
fee_to_target_msat = elem.total_fee_msat;
26862697
next_hops_path_htlc_minimum_msat = elem.path_htlc_minimum_msat;
26872698
next_hops_path_penalty_msat = elem.path_penalty_msat;
2699+
is_first_hop_target = elem.is_first_hop_target;
26882700
was_processed
26892701
} else {
26902702
// Entries are added to dist in add_entry!() when there is a channel from a node.
@@ -2695,21 +2707,24 @@ where L::Target: Logger {
26952707
fee_to_target_msat = 0;
26962708
next_hops_path_htlc_minimum_msat = 0;
26972709
next_hops_path_penalty_msat = 0;
2710+
is_first_hop_target = false;
26982711
false
26992712
};
27002713

27012714
if !skip_node {
2702-
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
2703-
for details in first_channels {
2704-
debug_assert_eq!(*peer_node_counter, $node.node_counter);
2705-
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
2706-
details, payer_node_id: &our_node_id, payer_node_counter,
2707-
target_node_counter: $node.node_counter,
2708-
});
2709-
add_entry!(&candidate, fee_to_target_msat,
2710-
$next_hops_value_contribution,
2711-
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2712-
$next_hops_cltv_delta, $next_hops_path_length);
2715+
if is_first_hop_target {
2716+
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
2717+
for details in first_channels {
2718+
debug_assert_eq!(*peer_node_counter, $node.node_counter);
2719+
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
2720+
details, payer_node_id: &our_node_id, payer_node_counter,
2721+
target_node_counter: $node.node_counter,
2722+
});
2723+
add_entry!(&candidate, fee_to_target_msat,
2724+
$next_hops_value_contribution,
2725+
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2726+
$next_hops_cltv_delta, $next_hops_path_length);
2727+
}
27132728
}
27142729
}
27152730

@@ -2756,6 +2771,32 @@ where L::Target: Logger {
27562771
for e in dist.iter_mut() {
27572772
*e = None;
27582773
}
2774+
for (_, (chans, peer_node_counter)) in first_hop_targets.iter() {
2775+
// In order to avoid looking up whether each node is a first-hop target, we store a
2776+
// dummy entry in dist for each first-hop target, allowing us to do this lookup for
2777+
// free since we're already looking at the `was_processed` flag.
2778+
//
2779+
// Note that all the fields (except `is_first_hop_target`) will be overwritten whenever
2780+
// we find a path to the target, so are left as dummies here.
2781+
dist[*peer_node_counter as usize] = Some(PathBuildingHop {
2782+
candidate: CandidateRouteHop::FirstHop(FirstHopCandidate {
2783+
details: &chans[0],
2784+
payer_node_id: &our_node_id,
2785+
target_node_counter: u32::max_value(),
2786+
payer_node_counter: u32::max_value(),
2787+
}),
2788+
fee_msat: 0,
2789+
next_hops_fee_msat: u64::max_value(),
2790+
hop_use_fee_msat: u64::max_value(),
2791+
total_fee_msat: u64::max_value(),
2792+
path_htlc_minimum_msat: u64::max_value(),
2793+
path_penalty_msat: u64::max_value(),
2794+
was_processed: false,
2795+
is_first_hop_target: true,
2796+
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
2797+
value_contribution_msat: 0,
2798+
});
2799+
}
27592800
hit_minimum_limit = false;
27602801

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

0 commit comments

Comments
 (0)