///
/// This is not exported to bindings users as lifetimes are not expressible in most languages.
pub payer_node_id: &'a NodeId,
+ /// A unique ID which describes the payer.
+ ///
+ /// It will not conflict with any [`NodeInfo::node_counter`]s, but may be equal to one if the
+ /// payer is a public node.
+ ///
+ /// [`NodeInfo::node_counter`]: super::gossip::NodeInfo::node_counter
+ pub(crate) payer_node_counter: u32,
+ /// A unique ID which describes the first hop counterparty.
+ ///
+ /// It will not conflict with any [`NodeInfo::node_counter`]s, but may be equal to one if the
+ /// counterparty is a public node.
+ ///
+ /// [`NodeInfo::node_counter`]: super::gossip::NodeInfo::node_counter
+ pub(crate) target_node_counter: u32,
}
/// A [`CandidateRouteHop::PublicHop`] entry.
/// Node id of the next hop in BOLT 11 route hint.
///
/// This is not exported to bindings users as lifetimes are not expressible in most languages.
- pub target_node_id: &'a NodeId
+ pub target_node_id: &'a NodeId,
+ /// A unique ID which describes the source node of the hop (further from the payment target).
+ ///
+ /// It will not conflict with any [`NodeInfo::node_counter`]s, but may be equal to one if the
+ /// node is a public node.
+ ///
+ /// [`NodeInfo::node_counter`]: super::gossip::NodeInfo::node_counter
+ pub(crate) source_node_counter: u32,
+ /// A unique ID which describes the destination node of the hop (towards the payment target).
+ ///
+ /// It will not conflict with any [`NodeInfo::node_counter`]s, but may be equal to one if the
+ /// node is a public node.
+ ///
+ /// [`NodeInfo::node_counter`]: super::gossip::NodeInfo::node_counter
+ pub(crate) target_node_counter: u32,
}
/// A [`CandidateRouteHop::Blinded`] entry.
/// This is used to cheaply uniquely identify this blinded path, even though we don't have
/// a short channel ID for this hop.
hint_idx: usize,
+ /// A unique ID which describes the introduction point of the blinded path.
+ ///
+ /// It will not conflict with any [`NodeInfo::node_counter`]s, but will generally be equal to
+ /// one from the public network graph (assuming the introduction point is a public node).
+ ///
+ /// [`NodeInfo::node_counter`]: super::gossip::NodeInfo::node_counter
+ source_node_counter: u32,
}
/// A [`CandidateRouteHop::OneHopBlinded`] entry.
/// This is used to cheaply uniquely identify this blinded path, even though we don't have
/// a short channel ID for this hop.
hint_idx: usize,
+ /// A unique ID which describes the introduction point of the blinded path.
+ ///
+ /// It will not conflict with any [`NodeInfo::node_counter`]s, but will generally be equal to
+ /// one from the public network graph (assuming the introduction point is a public node).
+ ///
+ /// [`NodeInfo::node_counter`]: super::gossip::NodeInfo::node_counter
+ source_node_counter: u32,
}
/// A wrapper around the various hop representations.
}
}
+ #[inline]
+ fn src_node_counter(&self) -> u32 {
+ match self {
+ CandidateRouteHop::FirstHop(hop) => hop.payer_node_counter,
+ CandidateRouteHop::PublicHop(hop) => hop.info.source_counter(),
+ CandidateRouteHop::PrivateHop(hop) => hop.source_node_counter,
+ CandidateRouteHop::Blinded(hop) => hop.source_node_counter,
+ CandidateRouteHop::OneHopBlinded(hop) => hop.source_node_counter,
+ }
+ }
+
+ #[inline]
+ fn target_node_counter(&self) -> Option<u32> {
+ match self {
+ CandidateRouteHop::FirstHop(hop) => Some(hop.target_node_counter),
+ CandidateRouteHop::PublicHop(hop) => Some(hop.info.target_counter()),
+ CandidateRouteHop::PrivateHop(hop) => Some(hop.target_node_counter),
+ CandidateRouteHop::Blinded(_) => None,
+ CandidateRouteHop::OneHopBlinded(_) => None,
+ }
+ }
+
/// Returns the fees that must be paid to route an HTLC over this channel.
#[inline]
pub fn fees(&self) -> RoutingFees {
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
let mut debug_struct = f.debug_struct("PathBuildingHop");
debug_struct
- .field("node_id", &self.candidate.target())
+ .field("source_node_id", &self.candidate.source())
+ .field("target_node_id", &self.candidate.target())
.field("short_channel_id", &self.candidate.short_channel_id())
.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)
- .field("total_fee_msat - (next_hops_fee_msat + hop_use_fee_msat)", &(&self.total_fee_msat - (&self.next_hops_fee_msat + &self.hop_use_fee_msat)))
+ .field("total_fee_msat - (next_hops_fee_msat + hop_use_fee_msat)", &(&self.total_fee_msat.saturating_sub(self.next_hops_fee_msat).saturating_sub(self.hop_use_fee_msat)))
.field("path_penalty_msat", &self.path_penalty_msat)
.field("path_htlc_minimum_msat", &self.path_htlc_minimum_msat)
.field("cltv_expiry_delta", &self.candidate.cltv_expiry_delta());
//
// First cache all our direct channels so that we can insert them in the heap at startup.
// Then process any blinded routes, resolving their introduction node and caching it.
- let mut first_hop_targets: HashMap<_, Vec<&ChannelDetails>> =
+ let mut first_hop_targets: HashMap<_, (Vec<&ChannelDetails>, u32)> =
hash_map_with_capacity(if first_hops.is_some() { first_hops.as_ref().unwrap().len() } else { 0 });
if let Some(hops) = first_hops {
for chan in hops {
.entry(counterparty_id)
.or_insert_with(|| {
// Make sure there's a counter assigned for the counterparty
- node_counter_builder.select_node_counter_for_id(counterparty_id);
- Vec::new()
+ let node_counter = node_counter_builder.select_node_counter_for_id(counterparty_id);
+ (Vec::new(), node_counter)
})
- .push(chan);
+ .0.push(chan);
}
if first_hop_targets.is_empty() {
return Err(LightningError{err: "Cannot route when there are no outbound routes away from us".to_owned(), action: ErrorAction::IgnoreError});
path.public_introduction_node_id(network_graph)
.map(|node_id_ref| *node_id_ref)
.or_else(|| {
- first_hop_targets.iter().find(|(_, channels)|
+ first_hop_targets.iter().find(|(_, (channels, _))|
channels
.iter()
.any(|details| Some(*scid) == details.get_outbound_payment_scid())
// Map from node_id to information about the best current path to that node, including feerate
// information.
- let mut dist: HashMap<NodeId, PathBuildingHop> = hash_map_with_capacity(network_nodes.len());
+ let dist_len = node_counters.max_counter() + 1;
+ let mut dist: Vec<Option<PathBuildingHop>> = vec![None; dist_len as usize];
// During routing, if we ignore a path due to an htlc_minimum_msat limit, we set this,
// indicating that we may wish to try again with a higher value, potentially paying to meet an
// when we want to stop looking for new paths.
let mut already_collected_value_msat = 0;
- for (_, channels) in first_hop_targets.iter_mut() {
+ for (_, (channels, _)) in first_hop_targets.iter_mut() {
sort_first_hop_channels(channels, &used_liquidities, recommended_value_msat,
our_node_pubkey);
}
);
let path_htlc_minimum_msat = compute_fees_saturating(curr_min, $candidate.fees())
.saturating_add(curr_min);
- let hm_entry = dist.entry(src_node_id);
- let old_entry = hm_entry.or_insert_with(|| {
+
+ let dist_entry = &mut dist[$candidate.src_node_counter() as usize];
+ let old_entry = if let Some(hop) = dist_entry {
+ hop
+ } else {
// If there was previously no known way to access the source node
// (recall it goes payee-to-payer) of short_channel_id, first add a
// semi-dummy record just to compute the fees to reach the source node.
// This will affect our decision on selecting short_channel_id
// as a way to reach the $candidate.target() node.
- PathBuildingHop {
+ *dist_entry = Some(PathBuildingHop {
candidate: $candidate.clone(),
fee_msat: 0,
next_hops_fee_msat: u64::max_value(),
was_processed: false,
#[cfg(all(not(ldk_bench), any(test, fuzzing)))]
value_contribution_msat,
- }
- });
+ });
+ dist_entry.as_mut().unwrap()
+ };
#[allow(unused_mut)] // We only use the mut in cfg(test)
let mut should_process = !old_entry.was_processed;
let fee_to_target_msat;
let next_hops_path_htlc_minimum_msat;
let next_hops_path_penalty_msat;
- let skip_node = if let Some(elem) = dist.get_mut(&$node_id) {
+ 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;
// Because there are no channels from payee, it will not have a dist entry at this point.
// If we're processing any other node, it is always be the result of a channel from it.
debug_assert_eq!($node_id, maybe_dummy_payee_node_id);
+
fee_to_target_msat = 0;
next_hops_path_htlc_minimum_msat = 0;
next_hops_path_penalty_msat = 0;
};
if !skip_node {
- if let Some(first_channels) = first_hop_targets.get(&$node_id) {
+ 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,
+ 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,
// For every new path, start from scratch, except for used_liquidities, which
// helps to avoid reusing previously selected paths in future iterations.
targets.clear();
- dist.clear();
+ for e in dist.iter_mut() {
+ *e = None;
+ }
hit_minimum_limit = false;
// If first hop is a private channel and the only way to reach the payee, this is the only
// place where it could be added.
- payee_node_id_opt.map(|payee| first_hop_targets.get(&payee).map(|first_channels| {
+ payee_node_id_opt.map(|payee| first_hop_targets.get(&payee).map(|(first_channels, peer_node_counter)| {
+ debug_assert_eq!(*peer_node_counter, payee_node_counter);
for details in first_channels {
let candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
- details, payer_node_id: &our_node_id,
+ details, payer_node_id: &our_node_id, payer_node_counter,
+ target_node_counter: payee_node_counter,
});
let added = add_entry!(&candidate, 0, path_value_msat,
0, 0u64, 0, 0).is_some();
// we have a direct channel to the first hop or the first hop is
// in the regular network graph.
let source_node_opt = introduction_node_id_cache[hint_idx];
- let (source_node_id, _source_node_counter) = if let Some(v) = source_node_opt { v } else { continue };
+ let (source_node_id, source_node_counter) = if let Some(v) = source_node_opt { v } else { continue };
if our_node_id == *source_node_id { continue }
let candidate = if hint.1.blinded_hops.len() == 1 {
CandidateRouteHop::OneHopBlinded(
- OneHopBlindedPathCandidate { source_node_id, hint, hint_idx }
+ OneHopBlindedPathCandidate { source_node_counter, source_node_id, hint, hint_idx }
)
} else {
- CandidateRouteHop::Blinded(BlindedPathCandidate { source_node_id, hint, hint_idx })
+ 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,
{
path_contribution_msat = hop_used_msat;
} else { continue }
- if let Some(first_channels) = first_hop_targets.get_mut(source_node_id) {
+ 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
);
for details in first_channels {
let first_hop_candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
- details, payer_node_id: &our_node_id,
+ details, payer_node_id: &our_node_id, payer_node_counter,
+ target_node_counter: *peer_node_counter,
});
let blinded_path_fee = match compute_fees(path_contribution_msat, candidate.fees()) {
Some(fee) => fee,
let mut aggregate_path_contribution_msat = path_value_msat;
for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() {
- let (target, _private_target_node_counter) =
+ let (target, private_target_node_counter) =
node_counters.private_node_counter_from_pubkey(&prev_hop_id)
+ .expect("node_counter_from_pubkey is called on all unblinded_route_hints keys during setup, so is always Some here");
+ let (_src_id, private_source_node_counter) =
+ node_counters.private_node_counter_from_pubkey(&hop.src_node_id)
.expect("node_counter_from_pubkey is called on all unblinded_route_hints keys during setup, so is always Some here");
- if let Some(first_channels) = first_hop_targets.get(target) {
+ if let Some((first_channels, _)) = first_hop_targets.get(target) {
if first_channels.iter().any(|d| d.outbound_scid_alias == Some(hop.short_channel_id)) {
log_trace!(logger, "Ignoring route hint with SCID {} (and any previous) due to it being a direct channel of ours.",
hop.short_channel_id);
info,
short_channel_id: hop.short_channel_id,
}))
- .unwrap_or_else(|| CandidateRouteHop::PrivateHop(PrivateHopCandidate { hint: hop, target_node_id: target }));
+ .unwrap_or_else(|| CandidateRouteHop::PrivateHop(PrivateHopCandidate {
+ hint: hop, target_node_id: target,
+ source_node_counter: *private_source_node_counter,
+ target_node_counter: *private_target_node_counter,
+ }));
if let Some(hop_used_msat) = add_entry!(&candidate,
aggregate_next_hops_fee_msat, aggregate_path_contribution_msat,
.saturating_add(1);
// Searching for a direct channel between last checked hop and first_hop_targets
- if let Some(first_channels) = first_hop_targets.get_mut(target) {
+ if let Some((first_channels, peer_node_counter)) = first_hop_targets.get_mut(target) {
sort_first_hop_channels(
first_channels, &used_liquidities, recommended_value_msat, our_node_pubkey
);
for details in first_channels {
let first_hop_candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
- details, payer_node_id: &our_node_id,
+ details, payer_node_id: &our_node_id, payer_node_counter,
+ target_node_counter: *peer_node_counter,
});
add_entry!(&first_hop_candidate,
aggregate_next_hops_fee_msat, aggregate_path_contribution_msat,
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
- if let Some(first_channels) = first_hop_targets.get_mut(&NodeId::from_pubkey(&hop.src_node_id)) {
+ if let Some((first_channels, peer_node_counter)) = first_hop_targets.get_mut(&NodeId::from_pubkey(&hop.src_node_id)) {
sort_first_hop_channels(
first_channels, &used_liquidities, recommended_value_msat, our_node_pubkey
);
for details in first_channels {
let first_hop_candidate = CandidateRouteHop::FirstHop(FirstHopCandidate {
- details, payer_node_id: &our_node_id,
+ details, payer_node_id: &our_node_id, payer_node_counter,
+ target_node_counter: *peer_node_counter,
});
add_entry!(&first_hop_candidate,
aggregate_next_hops_fee_msat,
// Since we're going payee-to-payer, hitting our node as a target means we should stop
// traversing the graph and arrange the path out of what we found.
if node_id == our_node_id {
- let mut new_entry = dist.remove(&our_node_id).unwrap();
+ let mut new_entry = dist[payer_node_counter as usize].take().unwrap();
let mut ordered_hops: Vec<(PathBuildingHop, NodeFeatures)> = vec!((new_entry.clone(), default_node_features.clone()));
'path_walk: loop {
let mut features_set = false;
- let target = ordered_hops.last().unwrap().0.candidate.target().unwrap_or(maybe_dummy_payee_node_id);
- if let Some(first_channels) = first_hop_targets.get(&target) {
+ let candidate = &ordered_hops.last().unwrap().0.candidate;
+ let target = candidate.target().unwrap_or(maybe_dummy_payee_node_id);
+ let target_node_counter = candidate.target_node_counter();
+ if let Some((first_channels, _)) = first_hop_targets.get(&target) {
for details in first_channels {
if let CandidateRouteHop::FirstHop(FirstHopCandidate { details: last_hop_details, .. })
- = ordered_hops.last().unwrap().0.candidate
+ = candidate
{
if details.get_outbound_payment_scid() == last_hop_details.get_outbound_payment_scid() {
ordered_hops.last_mut().unwrap().1 = details.counterparty.features.to_context();
// save this path for the payment route. Also, update the liquidity
// remaining on the used hops, so that we take them into account
// while looking for more paths.
- if target == maybe_dummy_payee_node_id {
+ if target_node_counter.is_none() {
break 'path_walk;
}
+ if target_node_counter == Some(payee_node_counter) { break 'path_walk; }
- new_entry = match dist.remove(&target) {
+ new_entry = match dist[target_node_counter.unwrap() as usize].take() {
Some(payment_hop) => payment_hop,
// We can't arrive at None because, if we ever add an entry to targets,
// we also fill in the entry in dist (see add_entry!).