Skip to content

Commit 2ec5837

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 2face52 commit 2ec5837

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
@@ -1542,6 +1542,14 @@ struct PathBuildingHop<'a> {
15421542
/// decrease as well. Thus, we have to explicitly track which nodes have been processed and
15431543
/// avoid processing them again.
15441544
was_processed: bool,
1545+
/// When processing a node as the next best-score candidate, we want to quickly check if it is
1546+
/// a direct counterparty of ours, using our local channel information immediately if we can.
1547+
///
1548+
/// In order to do so efficiently, we cache whether a node is a direct counterparty here at the
1549+
/// start of a route-finding pass. Unlike all other fields in this struct, this field is never
1550+
/// updated after being initialized - it is set at the start of a route-finding pass and only
1551+
/// read thereafter.
1552+
is_first_hop_target: bool,
15451553
/// Used to compare channels when choosing the for routing.
15461554
/// Includes paying for the use of a hop and the following hops, as well as
15471555
/// an estimated cost of reaching this hop.
@@ -2318,7 +2326,7 @@ where L::Target: Logger {
23182326
.saturating_add(curr_min);
23192327

23202328
let dist_entry = &mut dist[$candidate.src_node_counter() as usize];
2321-
let mut old_entry = if let Some(hop) = dist_entry {
2329+
let old_entry = if let Some(hop) = dist_entry {
23222330
hop
23232331
} else {
23242332
// If there was previously no known way to access the source node
@@ -2336,6 +2344,7 @@ where L::Target: Logger {
23362344
path_htlc_minimum_msat,
23372345
path_penalty_msat: u64::max_value(),
23382346
was_processed: false,
2347+
is_first_hop_target: false,
23392348
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
23402349
value_contribution_msat,
23412350
});
@@ -2500,12 +2509,14 @@ where L::Target: Logger {
25002509
let fee_to_target_msat;
25012510
let next_hops_path_htlc_minimum_msat;
25022511
let next_hops_path_penalty_msat;
2512+
let is_first_hop_target;
25032513
let skip_node = if let Some(elem) = &mut dist[$node.node_counter as usize] {
25042514
let was_processed = elem.was_processed;
25052515
elem.was_processed = true;
25062516
fee_to_target_msat = elem.total_fee_msat;
25072517
next_hops_path_htlc_minimum_msat = elem.path_htlc_minimum_msat;
25082518
next_hops_path_penalty_msat = elem.path_penalty_msat;
2519+
is_first_hop_target = elem.is_first_hop_target;
25092520
was_processed
25102521
} else {
25112522
// Entries are added to dist in add_entry!() when there is a channel from a node.
@@ -2516,21 +2527,24 @@ where L::Target: Logger {
25162527
fee_to_target_msat = 0;
25172528
next_hops_path_htlc_minimum_msat = 0;
25182529
next_hops_path_penalty_msat = 0;
2530+
is_first_hop_target = false;
25192531
false
25202532
};
25212533

25222534
if !skip_node {
2523-
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
2524-
for details in first_channels {
2525-
debug_assert_eq!(*peer_node_counter, $node.node_counter);
2526-
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
2527-
details, payer_node_id: &our_node_id, payer_node_counter,
2528-
target_node_counter: $node.node_counter,
2529-
});
2530-
add_entry!(&candidate, fee_to_target_msat,
2531-
$next_hops_value_contribution,
2532-
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2533-
$next_hops_cltv_delta, $next_hops_path_length);
2535+
if is_first_hop_target {
2536+
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
2537+
for details in first_channels {
2538+
debug_assert_eq!(*peer_node_counter, $node.node_counter);
2539+
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
2540+
details, payer_node_id: &our_node_id, payer_node_counter,
2541+
target_node_counter: $node.node_counter,
2542+
});
2543+
add_entry!(&candidate, fee_to_target_msat,
2544+
$next_hops_value_contribution,
2545+
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
2546+
$next_hops_cltv_delta, $next_hops_path_length);
2547+
}
25342548
}
25352549
}
25362550

@@ -2577,6 +2591,33 @@ where L::Target: Logger {
25772591
for e in dist.iter_mut() {
25782592
*e = None;
25792593
}
2594+
for (_, (chans, peer_node_counter)) in first_hop_targets.iter() {
2595+
// In order to avoid looking up whether each node is a first-hop target, we store a
2596+
// dummy entry in dist for each first-hop target, allowing us to do this lookup for
2597+
// free since we're already looking at the `was_processed` flag.
2598+
//
2599+
// Note that all the fields (except `is_first_hop_target`) will be overwritten whenever
2600+
// we find a path to the target, so are left as dummies here.
2601+
dist[*peer_node_counter as usize] = Some(PathBuildingHop {
2602+
candidate: CandidateRouteHop::FirstHop(FirstHopCandidate {
2603+
details: &chans[0],
2604+
payer_node_id: &our_node_id,
2605+
target_node_counter: u32::max_value(),
2606+
payer_node_counter: u32::max_value(),
2607+
}),
2608+
target_node_counter: None,
2609+
fee_msat: 0,
2610+
next_hops_fee_msat: u64::max_value(),
2611+
hop_use_fee_msat: u64::max_value(),
2612+
total_fee_msat: u64::max_value(),
2613+
path_htlc_minimum_msat: u64::max_value(),
2614+
path_penalty_msat: u64::max_value(),
2615+
was_processed: false,
2616+
is_first_hop_target: true,
2617+
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
2618+
value_contribution_msat: 0,
2619+
});
2620+
}
25802621
hit_minimum_limit = false;
25812622

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

0 commit comments

Comments
 (0)