Skip to content

Misc routing optimization #2803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 52 additions & 11 deletions lightning/src/routing/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1772,6 +1772,14 @@ struct PathBuildingHop<'a> {
/// decrease as well. Thus, we have to explicitly track which nodes have been processed and
/// avoid processing them again.
was_processed: bool,
/// When processing a node as the next best-score candidate, we want to quickly check if it is
/// a direct counterparty of ours, using our local channel information immediately if we can.
///
/// In order to do so efficiently, we cache whether a node is a direct counterparty here at the
/// start of a route-finding pass. Unlike all other fields in this struct, this field is never
/// updated after being initialized - it is set at the start of a route-finding pass and only
/// read thereafter.
is_first_hop_target: bool,
/// Used to compare channels when choosing the for routing.
/// Includes paying for the use of a hop and the following hops, as well as
/// an estimated cost of reaching this hop.
Expand Down Expand Up @@ -1810,6 +1818,7 @@ impl<'a> core::fmt::Debug for PathBuildingHop<'a> {
.field("source_node_id", &self.candidate.source())
.field("target_node_id", &self.candidate.target())
.field("short_channel_id", &self.candidate.short_channel_id())
.field("is_first_hop_target", &self.is_first_hop_target)
.field("total_fee_msat", &self.total_fee_msat)
.field("next_hops_fee_msat", &self.next_hops_fee_msat)
.field("hop_use_fee_msat", &self.hop_use_fee_msat)
Expand Down Expand Up @@ -2516,6 +2525,7 @@ where L::Target: Logger {
path_htlc_minimum_msat,
path_penalty_msat: u64::max_value(),
was_processed: false,
is_first_hop_target: false,
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
value_contribution_msat,
});
Expand Down Expand Up @@ -2679,12 +2689,14 @@ where L::Target: Logger {
let fee_to_target_msat;
let next_hops_path_htlc_minimum_msat;
let next_hops_path_penalty_msat;
let is_first_hop_target;
let skip_node = if let Some(elem) = &mut dist[$node.node_counter as usize] {
let was_processed = elem.was_processed;
elem.was_processed = true;
fee_to_target_msat = elem.total_fee_msat;
next_hops_path_htlc_minimum_msat = elem.path_htlc_minimum_msat;
next_hops_path_penalty_msat = elem.path_penalty_msat;
is_first_hop_target = elem.is_first_hop_target;
was_processed
} else {
// Entries are added to dist in add_entry!() when there is a channel from a node.
Expand All @@ -2695,21 +2707,24 @@ where L::Target: Logger {
fee_to_target_msat = 0;
next_hops_path_htlc_minimum_msat = 0;
next_hops_path_penalty_msat = 0;
is_first_hop_target = false;
false
};

if !skip_node {
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
for details in first_channels {
debug_assert_eq!(*peer_node_counter, $node.node_counter);
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
details, payer_node_id: &our_node_id, payer_node_counter,
target_node_counter: $node.node_counter,
});
add_entry!(&candidate, fee_to_target_msat,
$next_hops_value_contribution,
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
$next_hops_cltv_delta, $next_hops_path_length);
if is_first_hop_target {
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get(&$node_id) {
for details in first_channels {
debug_assert_eq!(*peer_node_counter, $node.node_counter);
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
details, payer_node_id: &our_node_id, payer_node_counter,
target_node_counter: $node.node_counter,
});
add_entry!(&candidate, fee_to_target_msat,
$next_hops_value_contribution,
next_hops_path_htlc_minimum_msat, next_hops_path_penalty_msat,
$next_hops_cltv_delta, $next_hops_path_length);
}
Comment on lines +2719 to +2731
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handling of first hop targets within the routing algorithm is complex and should be carefully reviewed to ensure correctness.

Review the logic for handling first hop targets to ensure correctness. Consider adding more comments to explain how first hop targets are processed in the routing algorithm.

}
}

Expand Down Expand Up @@ -2756,6 +2771,32 @@ where L::Target: Logger {
for e in dist.iter_mut() {
*e = None;
}
for (_, (chans, peer_node_counter)) in first_hop_targets.iter() {
// In order to avoid looking up whether each node is a first-hop target, we store a
// dummy entry in dist for each first-hop target, allowing us to do this lookup for
// free since we're already looking at the `was_processed` flag.
//
// Note that all the fields (except `is_first_hop_target`) will be overwritten whenever
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth adding debug_asserts or similar checks to make sure we don't deviate from this assumption?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it would be, but I'm not sure I know how to write such an assertion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could check that if one field is updated, all others are too? But maybe not worth it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not sure how to check on a per-field basis. The likely failure case is we add another field and fail to update it when relevant, but I'm not aware of a way to iterate over the fields of a struct?

// we find a path to the target, so are left as dummies here.
dist[*peer_node_counter as usize] = Some(PathBuildingHop {
candidate: CandidateRouteHop::FirstHop(FirstHopCandidate {
details: &chans[0],
payer_node_id: &our_node_id,
target_node_counter: u32::max_value(),
payer_node_counter: u32::max_value(),
}),
fee_msat: 0,
next_hops_fee_msat: u64::max_value(),
hop_use_fee_msat: u64::max_value(),
total_fee_msat: u64::max_value(),
path_htlc_minimum_msat: u64::max_value(),
path_penalty_msat: u64::max_value(),
was_processed: false,
is_first_hop_target: true,
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
value_contribution_msat: 0,
});
}
hit_minimum_limit = false;

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