Add direct hops to intros after all blinded paths in pathfinding

When we do pathfinding with blinded paths, we start each
pathfinding iteration by inserting all the blinded paths into our
nodes map as last-hops to the destination. As we do that, we check
if any of the introduction points happen to be nodes we have direct
chanels with, as we want to use the local info for such channels
and support finding a path even if that channel is not publicly
announced.

However, as we iterate the blinded paths, we may find a second
blinded path from the same introduction point which we prefer over
the first. If this happens, we would already have added info from
us over the local channel to that intro point and end up with
calculations for the first hop to a blinded path that we no longer
prefer.

This is ultimately fixed here in two ways:
(a) we process the first-hop channels to blinded path introduction
    points in a separate loop after we've processed all blinded
    paths, ensuring we only ever consider a channel to the blinded
    path we will ultimately prefer.
(b) In the next commit, we add we add a new tracking bool in
    `PathBuildingHop` called `best_path_from_hop_selected` which we
    set when we process a channel backwards from a node, indicating
    that we've committed to the best path to the node and check when
    we add a new path to a node. This would have resulted in a much
    earlier debug-assertion in fuzzing or several tests.
This commit is contained in:
Matt Corallo 2025-02-02 23:52:49 +00:00
parent 0351a24722
commit b2c635b8e0

View file

@ -2937,6 +2937,7 @@ where L::Target: Logger {
introduction_node_id_cache.len(),
"introduction_node_id_cache was built by iterating the blinded_route_hints, so they should be the same len"
);
let mut blind_intros_added = hash_map_with_capacity(payment_params.payee.blinded_route_hints().len());
for (hint_idx, hint) in payment_params.payee.blinded_route_hints().iter().enumerate() {
// Only add the hops in this route to our candidate set if either
// we have a direct channel to the first hop or the first hop is
@ -2951,12 +2952,21 @@ where L::Target: Logger {
} else {
CandidateRouteHop::Blinded(BlindedPathCandidate { source_node_counter, source_node_id, hint, hint_idx })
};
let mut path_contribution_msat = path_value_msat;
if let Some(hop_used_msat) = add_entry!(&candidate,
0, path_contribution_msat, 0, 0_u64, 0, 0)
0, path_value_msat, 0, 0_u64, 0, 0)
{
path_contribution_msat = hop_used_msat;
blind_intros_added.insert(source_node_id, (hop_used_msat, candidate));
} else { continue }
}
// If we added a blinded path from an introduction node to the destination, where the
// introduction node is one of our direct peers, we need to scan our `first_channels`
// to detect this. However, doing so immediately after calling `add_entry`, above, could
// result in incorrect behavior if we, in a later loop iteration, update the fee from the
// same introduction point to the destination (due to a different blinded path with the
// same introduction point having a lower score).
// Thus, we track the nodes that we added paths from in `blind_intros_added` and scan for
// introduction points we have a channel with after processing all blinded paths.
for (source_node_id, (path_contribution_msat, candidate)) in blind_intros_added {
if let Some((first_channels, peer_node_counter)) = first_hop_targets.get_mut(source_node_id) {
sort_first_hop_channels(
first_channels, &used_liquidities, recommended_value_msat, our_node_pubkey