Make the historical bucket data private to `bucketed_history`
[rust-lightning] / lightning / src / routing / router.rs
index 6300763e2a20e34c90850e12c9965524a82bda95..7f11f31983c5636774dbed527275294b88482703 100644 (file)
@@ -968,33 +968,24 @@ impl_writeable_tlv_based!(RouteHintHop, {
 });
 
 #[derive(Eq, PartialEq)]
+#[repr(align(64))] // Force the size to 64 bytes
 struct RouteGraphNode {
        node_id: NodeId,
-       lowest_fee_to_node: u64,
-       total_cltv_delta: u32,
+       score: u64,
        // The maximum value a yet-to-be-constructed payment path might flow through this node.
        // This value is upper-bounded by us by:
        // - how much is needed for a path being constructed
        // - how much value can channels following this node (up to the destination) can contribute,
        //   considering their capacity and fees
        value_contribution_msat: u64,
-       /// The effective htlc_minimum_msat at this hop. If a later hop on the path had a higher HTLC
-       /// minimum, we use it, plus the fees required at each earlier hop to meet it.
-       path_htlc_minimum_msat: u64,
-       /// All penalties incurred from this hop on the way to the destination, as calculated using
-       /// channel scoring.
-       path_penalty_msat: u64,
+       total_cltv_delta: u32,
        /// The number of hops walked up to this node.
        path_length_to_node: u8,
 }
 
 impl cmp::Ord for RouteGraphNode {
        fn cmp(&self, other: &RouteGraphNode) -> cmp::Ordering {
-               let other_score = cmp::max(other.lowest_fee_to_node, other.path_htlc_minimum_msat)
-                       .saturating_add(other.path_penalty_msat);
-               let self_score = cmp::max(self.lowest_fee_to_node, self.path_htlc_minimum_msat)
-                       .saturating_add(self.path_penalty_msat);
-               other_score.cmp(&self_score).then_with(|| other.node_id.cmp(&self.node_id))
+               other.score.cmp(&self.score).then_with(|| other.node_id.cmp(&self.node_id))
        }
 }
 
@@ -1004,6 +995,11 @@ impl cmp::PartialOrd for RouteGraphNode {
        }
 }
 
+// While RouteGraphNode can be laid out with fewer bytes, performance appears to be improved
+// substantially when it is laid out at exactly 64 bytes.
+const _GRAPH_NODE_SMALL: usize = 64 - core::mem::size_of::<RouteGraphNode>();
+const _GRAPH_NODE_FIXED_SIZE: usize = core::mem::size_of::<RouteGraphNode>() - 64;
+
 /// A wrapper around the various hop representations.
 ///
 /// Can be used to examine the properties of a hop,
@@ -1020,7 +1016,11 @@ pub enum CandidateRouteHop<'a> {
                /// [`find_route`] validates this prior to constructing a [`CandidateRouteHop`].
                details: &'a ChannelDetails,
                /// The node id of the payer, which is also the source side of this candidate route hop.
-               node_id: NodeId,
+               payer_node_id: &'a NodeId,
+               /// XXX
+               payer_node_counter: u32,
+               /// XXX
+               target_node_counter: u32,
        },
        /// A hop found in the [`ReadOnlyNetworkGraph`].
        PublicHop {
@@ -1039,7 +1039,11 @@ pub enum CandidateRouteHop<'a> {
                /// Information about the private hop communicated via BOLT 11.
                hint: &'a RouteHintHop,
                /// Node id of the next hop in BOLT 11 route hint.
-               target_node_id: NodeId
+               target_node_id: &'a NodeId,
+               /// XXX
+               source_node_counter: u32,
+               /// XXX
+               target_node_counter: u32,
        },
        /// A blinded path which starts with an introduction point and ultimately terminates with the
        /// payee.
@@ -1055,9 +1059,11 @@ pub enum CandidateRouteHop<'a> {
                hint: &'a (BlindedPayInfo, BlindedPath),
                /// Index of the hint in the original list of blinded hints.
                ///
-               /// This is used to build a [`CandidateHopId`] that uniquely identifies this blinded path,
-               /// even though we don't have a short channel ID for this hop.
+               /// 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,
+               /// XXX
+               source_node_counter: u32,
        },
        /// Similar to [`Self::Blinded`], but the path here only has one hop.
        ///
@@ -1078,18 +1084,32 @@ pub enum CandidateRouteHop<'a> {
                hint: &'a (BlindedPayInfo, BlindedPath),
                /// Index of the hint in the original list of blinded hints.
                ///
-               /// This is used to build a [`CandidateHopId`] that uniquely identifies this blinded path,
-               /// even though we don't have a short channel ID for this hop.
+               /// 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,
+               /// XXX
+               source_node_counter: u32,
        },
 }
 
 impl<'a> CandidateRouteHop<'a> {
-       /// Returns short_channel_id if known.
-       /// For `FirstHop` we assume [`ChannelDetails::get_outbound_payment_scid`] is always set, this assumption is checked in
-       /// [`find_route`] method.
-       /// For `Blinded` and `OneHopBlinded` we return `None` because next hop is not known.
-       pub fn short_channel_id(&self) -> Option<u64> {
+       /// Returns the short channel ID for this hop, if one is known.
+       ///
+       /// This SCID could be an alias or a globally unique SCID, and thus is only expected to
+       /// uniquely identify this channel in conjunction with the [`CandidateRouteHop::source`].
+       ///
+       /// Returns `Some` as long as the candidate is a [`CandidateRouteHop::PublicHop`], a
+       /// [`CandidateRouteHop::PrivateHop`] from a BOLT 11 route hint, or a
+       /// [`CandidateRouteHop::FirstHop`] with a known [`ChannelDetails::get_outbound_payment_scid`]
+       /// (which is always true for channels which are funded and ready for use).
+       ///
+       /// In other words, this should always return `Some` as long as the candidate hop is not a
+       /// [`CandidateRouteHop::Blinded`] or a [`CandidateRouteHop::OneHopBlinded`].
+       ///
+       /// Note that this is deliberately not public as it is somewhat of a footgun because it doesn't
+       /// define a global namespace.
+       #[inline]
+       fn short_channel_id(&self) -> Option<u64> {
                match self {
                        CandidateRouteHop::FirstHop { details, .. } => details.get_outbound_payment_scid(),
                        CandidateRouteHop::PublicHop { short_channel_id, .. } => Some(*short_channel_id),
@@ -1099,6 +1119,22 @@ impl<'a> CandidateRouteHop<'a> {
                }
        }
 
+       /// Returns the globally unique short channel ID for this hop, if one is known.
+       ///
+       /// This only returns `Some` if the channel is public (either our own, or one we've learned
+       /// from the public network graph), and thus the short channel ID we have for this channel is
+       /// globally unique and identifies this channel in a global namespace.
+       #[inline]
+       pub fn globally_unique_short_channel_id(&self) -> Option<u64> {
+               match self {
+                       CandidateRouteHop::FirstHop { details, .. } => if details.is_public { details.short_channel_id } else { None },
+                       CandidateRouteHop::PublicHop { short_channel_id, .. } => Some(*short_channel_id),
+                       CandidateRouteHop::PrivateHop { .. } => None,
+                       CandidateRouteHop::Blinded { .. } => None,
+                       CandidateRouteHop::OneHopBlinded { .. } => None,
+               }
+       }
+
        // NOTE: This may alloc memory so avoid calling it in a hot code path.
        fn features(&self) -> ChannelFeatures {
                match self {
@@ -1110,7 +1146,12 @@ impl<'a> CandidateRouteHop<'a> {
                }
        }
 
-       /// Returns cltv_expiry_delta for this hop.
+       /// Returns the required difference in HTLC CLTV expiry between the [`Self::source`] and the
+       /// next-hop for an HTLC taking this hop.
+       ///
+       /// This is the time that the node(s) in this hop have to claim the HTLC on-chain if the
+       /// next-hop goes on chain with a payment preimage.
+       #[inline]
        pub fn cltv_expiry_delta(&self) -> u32 {
                match self {
                        CandidateRouteHop::FirstHop { .. } => 0,
@@ -1121,7 +1162,8 @@ impl<'a> CandidateRouteHop<'a> {
                }
        }
 
-       /// Returns the htlc_minimum_msat for this hop.
+       /// Returns the minimum amount that can be sent over this hop, in millisatoshis.
+       #[inline]
        pub fn htlc_minimum_msat(&self) -> u64 {
                match self {
                        CandidateRouteHop::FirstHop { details, .. } => details.next_outbound_htlc_minimum_msat,
@@ -1132,7 +1174,30 @@ impl<'a> CandidateRouteHop<'a> {
                }
        }
 
-       /// Returns the fees for this hop.
+       #[inline(always)]
+       fn src_node_counter(&self) -> u32 {
+               match self {
+                       CandidateRouteHop::FirstHop { payer_node_counter, .. } => *payer_node_counter,
+                       CandidateRouteHop::PublicHop { info, .. } => info.source_counter(),
+                       CandidateRouteHop::PrivateHop { source_node_counter, .. } => *source_node_counter,
+                       CandidateRouteHop::Blinded { source_node_counter, .. } => *source_node_counter,
+                       CandidateRouteHop::OneHopBlinded { source_node_counter, .. } => *source_node_counter,
+               }
+       }
+
+       #[inline]
+       fn target_node_counter(&self) -> Option<u32> {
+               match self {
+                       CandidateRouteHop::FirstHop { target_node_counter, .. } => Some(*target_node_counter),
+                       CandidateRouteHop::PublicHop { info, .. } => Some(info.target_counter()),
+                       CandidateRouteHop::PrivateHop { target_node_counter, .. } => Some(*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 {
                match self {
                        CandidateRouteHop::FirstHop { .. } => RoutingFees {
@@ -1151,6 +1216,10 @@ impl<'a> CandidateRouteHop<'a> {
                }
        }
 
+       /// Fetch the effective capacity of this hop.
+       ///
+       /// Note that this may be somewhat expensive, so calls to this should be limited and results
+       /// cached!
        fn effective_capacity(&self) -> EffectiveCapacity {
                match self {
                        CandidateRouteHop::FirstHop { details, .. } => EffectiveCapacity::ExactLiquidity {
@@ -1167,10 +1236,11 @@ impl<'a> CandidateRouteHop<'a> {
                }
        }
 
-       ///  Returns the id of this hop.
-       ///  For `Blinded` and `OneHopBlinded` we return `CandidateHopId::Blinded` with `hint_idx` because we don't know the channel id.
-       ///  For any other option we return `CandidateHopId::Clear` because we know the channel id and the direction.
-       pub fn id(&self) -> CandidateHopId {
+       /// Returns an ID describing the given hop.
+       ///
+       /// See the docs on [`CandidateHopId`] for when this is, or is not, unique.
+       #[inline]
+       fn id(&self) -> CandidateHopId {
                match self {
                        CandidateRouteHop::Blinded { hint_idx, .. } => CandidateHopId::Blinded(*hint_idx),
                        CandidateRouteHop::OneHopBlinded { hint_idx, .. } => CandidateHopId::Blinded(*hint_idx),
@@ -1187,12 +1257,13 @@ impl<'a> CandidateRouteHop<'a> {
        }
        /// Returns the source node id of current hop.
        ///
-       /// Source node id refers to the hop forwarding the payment.
+       /// Source node id refers to the node forwarding the HTLC through this hop.
        ///
-       /// For `FirstHop` we return payer's node id.
+       /// For [`Self::FirstHop`] we return payer's node id.
+       #[inline(always)]
        pub fn source(&self) -> NodeId {
                match self {
-                       CandidateRouteHop::FirstHop { node_id, .. } => *node_id,
+                       CandidateRouteHop::FirstHop { payer_node_id, .. } => **payer_node_id,
                        CandidateRouteHop::PublicHop { info, .. } => *info.source(),
                        CandidateRouteHop::PrivateHop { hint, .. } => hint.src_node_id.into(),
                        CandidateRouteHop::Blinded { hint, .. } => hint.1.introduction_node_id.into(),
@@ -1201,26 +1272,36 @@ impl<'a> CandidateRouteHop<'a> {
        }
        /// Returns the target node id of this hop, if known.
        ///
-       /// Target node id refers to the hop receiving the payment.
+       /// Target node id refers to the node receiving the HTLC after this hop.
+       ///
+       /// For [`Self::Blinded`] we return `None` because the ultimate destination after the blinded
+       /// path is unknown.
        ///
-       /// For `Blinded` and `OneHopBlinded` we return `None` because next hop is blinded.
-       pub fn target(&self) -> Option<NodeId> {
+       /// For [`Self::OneHopBlinded`] we return `None` because the target is the same as the source,
+       /// and such a return value would be somewhat nonsensical.
+       #[inline(always)]
+       pub fn target(&self) -> Option<NodeId> {
                match self {
                        CandidateRouteHop::FirstHop { details, .. } => Some(details.counterparty.node_id.into()),
                        CandidateRouteHop::PublicHop { info, .. } => Some(*info.target()),
-                       CandidateRouteHop::PrivateHop { target_node_id, .. } => Some(*target_node_id),
+                       CandidateRouteHop::PrivateHop { target_node_id, .. } => Some(**target_node_id),
                        CandidateRouteHop::Blinded { .. } => None,
                        CandidateRouteHop::OneHopBlinded { .. } => None,
                }
        }
 }
 
-/// A wrapper around the various hop id representations.
+/// A unique(ish) identifier for a specific [`CandidateRouteHop`].
 ///
-/// `CandidateHopId::Clear` is used to identify a hop with a known short channel id and direction.
-/// `CandidateHopId::Blinded` is used to identify a blinded hop `hint_idx`.
+/// For blinded paths, this ID is unique only within a given [`find_route`] call.
+///
+/// For other hops, because SCIDs between private channels and public channels can conflict, this
+/// isn't guaranteed to be unique at all.
+///
+/// For our uses, this is generally fine, but it is not public as it is otherwise a rather
+/// difficult-to-use API.
 #[derive(Clone, Copy, Eq, Hash, Ord, PartialOrd, PartialEq)]
-pub enum CandidateHopId {
+enum CandidateHopId {
        /// Contains (scid, src_node_id < target_node_id)
        Clear((u64, bool)),
        /// Index of the blinded route hint in [`Payee::Blinded::route_hints`].
@@ -1255,20 +1336,51 @@ fn iter_equal<I1: Iterator, I2: Iterator>(mut iter_a: I1, mut iter_b: I2)
        }
 }
 
+#[cfg(target_feature = "sse")]
+#[inline(always)]
+unsafe fn do_prefetch<T>(ptr: *const T) {
+       #[cfg(target_arch = "x86_64")]
+       use core::arch::x86_64::*;
+       #[cfg(target_arch = "x86")]
+       use core::arch::x86::*;
+       _mm_prefetch(ptr as *const i8, _MM_HINT_T0);
+}
+
+#[cfg(not(target_feature = "sse"))]
+#[inline(always)]
+unsafe fn do_prefetch<T>(_: *const T) {}
+
+#[inline(always)]
+fn prefetch_first_byte<T>(t: &T) {
+       // While X86's prefetch should be safe even on an invalid memory address (the ISA says
+       // "PREFETCHh instruction is merely a hint and does not affect program behavior"), we take
+       // an extra step towards safety here by requiring the pointer be valid (as Rust references
+       // are always valid when accessed).
+       //
+       // Note that a pointer in Rust could be to a zero sized type, in which case the pointer could
+       // be NULL (or some other bogus value), so we explicitly check for that here.
+       if ::core::mem::size_of::<T>() != 0 {
+               unsafe { do_prefetch(t) }
+       }
+}
+
 /// It's useful to keep track of the hops associated with the fees required to use them,
 /// so that we can choose cheaper paths (as per Dijkstra's algorithm).
 /// Fee values should be updated only in the context of the whole path, see update_value_and_recompute_fees.
 /// These fee values are useful to choose hops as we traverse the graph "payee-to-payer".
 #[derive(Clone)]
+#[repr(align(128))]
 struct PathBuildingHop<'a> {
        candidate: CandidateRouteHop<'a>,
-       fee_msat: u64,
-
-       /// All the fees paid *after* this channel on the way to the destination
-       next_hops_fee_msat: u64,
-       /// Fee paid for the use of the current channel (see candidate.fees()).
-       /// The value will be actually deducted from the counterparty balance on the previous link.
-       hop_use_fee_msat: u64,
+       target_node_counter: Option<u32>,
+       /// If we've already processed a node as the best node, we shouldn't process it again. Normally
+       /// we'd just ignore it if we did as all channels would have a higher new fee, but because we
+       /// may decrease the amounts in use as we walk the graph, the actual calculated fee may
+       /// decrease as well. Thus, we have to explicitly track which nodes have been processed and
+       /// avoid processing them again.
+       was_processed: bool,
+       /// XXX
+       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.
@@ -1280,12 +1392,15 @@ struct PathBuildingHop<'a> {
        /// All penalties incurred from this channel on the way to the destination, as calculated using
        /// channel scoring.
        path_penalty_msat: u64,
-       /// If we've already processed a node as the best node, we shouldn't process it again. Normally
-       /// we'd just ignore it if we did as all channels would have a higher new fee, but because we
-       /// may decrease the amounts in use as we walk the graph, the actual calculated fee may
-       /// decrease as well. Thus, we have to explicitly track which nodes have been processed and
-       /// avoid processing them again.
-       was_processed: bool,
+
+       fee_msat: u64,
+
+       /// All the fees paid *after* this channel on the way to the destination
+       next_hops_fee_msat: u64,
+       /// Fee paid for the use of the current channel (see candidate.fees()).
+       /// The value will be actually deducted from the counterparty balance on the previous link.
+       hop_use_fee_msat: u64,
+
        #[cfg(all(not(ldk_bench), any(test, fuzzing)))]
        // In tests, we apply further sanity checks on cases where we skip nodes we already processed
        // to ensure it is specifically in cases where the fee has gone down because of a decrease in
@@ -1294,16 +1409,20 @@ struct PathBuildingHop<'a> {
        value_contribution_msat: u64,
 }
 
+const _NODE_MAP_SIZE_TWO_CACHE_LINES: usize = 128 - core::mem::size_of::<Option<PathBuildingHop>>();
+const _NODE_MAP_SIZE_EXACTLY_TWO_CACHE_LINES: usize = core::mem::size_of::<Option<PathBuildingHop>>() - 128;
+
 impl<'a> core::fmt::Debug for PathBuildingHop<'a> {
        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());
@@ -1724,11 +1843,42 @@ where L::Target: Logger {
                first_hops.map(|hops| hops.len()).unwrap_or(0), if first_hops.is_some() { "" } else { "not " },
                max_total_routing_fee_msat);
 
+       let private_hop_targets_node_counter_offset = network_graph.max_node_counter() + 2;
+       let mut private_node_id_to_node_counter = HashMap::new();
+
+       let mut private_hop_key_cache = HashMap::with_capacity(
+               payment_params.payee.unblinded_route_hints().iter().map(|path| path.0.len()).sum()
+       );
+
+       // Because we store references to private hop node_ids in `dist`, below, we need them to exist
+       // (as `NodeId`, not `PublicKey`) for the lifetime of `dist`. Thus, we calculate all the keys
+       // we'll need here and simply fetch them when routing.
+       let payee_node_counter = payee_node_id_opt
+               .and_then(|payee| network_nodes.get(&payee))
+               .map(|node| node.node_counter)
+               .unwrap_or(private_hop_targets_node_counter_offset);
+       private_node_id_to_node_counter.insert(maybe_dummy_payee_node_id, payee_node_counter);
+       private_hop_key_cache.insert(maybe_dummy_payee_pk, (NodeId::from_pubkey(&maybe_dummy_payee_pk), payee_node_counter));
+
+       for route in payment_params.payee.unblinded_route_hints().iter() {
+               for hop in route.0.iter() {
+                       let hop_node_id = NodeId::from_pubkey(&hop.src_node_id);
+                       let node_counter = if let Some(node) = network_nodes.get(&hop_node_id) {
+                               node.node_counter
+                       } else {
+                               let next_node_counter = private_hop_targets_node_counter_offset + private_node_id_to_node_counter.len() as u32;
+                               *private_node_id_to_node_counter.entry(hop_node_id)
+                                       .or_insert(next_node_counter)
+                       };
+                       private_hop_key_cache.insert(hop.src_node_id, (hop_node_id, node_counter));
+               }
+       }
+
        // Step (1).
        // Prepare the data we'll use for payee-to-payer search by
        // inserting first hops suggested by the caller as targets.
        // Our search will then attempt to reach them while traversing from the payee node.
-       let mut first_hop_targets: HashMap<_, Vec<&ChannelDetails>> =
+       let mut first_hop_targets: HashMap<_, (Vec<&ChannelDetails>, u32)> =
                HashMap::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 {
@@ -1738,10 +1888,20 @@ where L::Target: Logger {
                        if chan.counterparty.node_id == *our_node_pubkey {
                                return Err(LightningError{err: "First hop cannot have our_node_pubkey as a destination.".to_owned(), action: ErrorAction::IgnoreError});
                        }
+                       let counterparty_node_id = NodeId::from_pubkey(&chan.counterparty.node_id);
                        first_hop_targets
-                               .entry(NodeId::from_pubkey(&chan.counterparty.node_id))
-                               .or_insert(Vec::new())
-                               .push(chan);
+                               .entry(counterparty_node_id)
+                               .or_insert_with(|| {
+                                       let node_counter = if let Some(node) = network_nodes.get(&counterparty_node_id) {
+                                               node.node_counter
+                                       } else {
+                                               let next_node_counter = private_hop_targets_node_counter_offset + private_node_id_to_node_counter.len() as u32;
+                                               *private_node_id_to_node_counter.entry(counterparty_node_id)
+                                                       .or_insert(next_node_counter)
+                                       };
+                                       (Vec::new(), node_counter)
+                               })
+                               .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});
@@ -1755,7 +1915,15 @@ where L::Target: Logger {
 
        // Map from node_id to information about the best current path to that node, including feerate
        // information.
-       let mut dist: HashMap<NodeId, PathBuildingHop> = HashMap::with_capacity(network_nodes.len());
+       let dist_len = private_hop_targets_node_counter_offset + private_hop_key_cache.len() as u32 + 1;
+       let mut dist: Vec<Option<PathBuildingHop>> = vec![None; dist_len as usize];
+       let payer_node_counter = network_nodes
+               .get(&our_node_id)
+               .map(|node| node.node_counter)
+               .or_else(|| {
+                       private_node_id_to_node_counter.get(&our_node_id).map(|counter| *counter)
+               })
+               .unwrap_or(network_graph.max_node_counter() + 1);
 
        // 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
@@ -1802,7 +1970,7 @@ where L::Target: Logger {
        // 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);
        }
@@ -1848,6 +2016,8 @@ where L::Target: Logger {
                                // if the amount being transferred over this path is lower.
                                // We do this for now, but this is a subject for removal.
                                if let Some(mut available_value_contribution_msat) = htlc_maximum_msat.checked_sub($next_hops_fee_msat) {
+                                       let cltv_expiry_delta = $candidate.cltv_expiry_delta();
+                                       let htlc_minimum_msat = $candidate.htlc_minimum_msat();
                                        let used_liquidity_msat = used_liquidities
                                                .get(&$candidate.id())
                                                .map_or(0, |used_liquidity_msat| {
@@ -1870,7 +2040,7 @@ where L::Target: Logger {
                                                .checked_sub(2*MEDIAN_HOP_CLTV_EXPIRY_DELTA)
                                                .unwrap_or(payment_params.max_total_cltv_expiry_delta - final_cltv_expiry_delta);
                                        let hop_total_cltv_delta = ($next_hops_cltv_delta as u32)
-                                               .saturating_add($candidate.cltv_expiry_delta());
+                                               .saturating_add(cltv_expiry_delta);
                                        let exceeds_cltv_delta_limit = hop_total_cltv_delta > max_total_cltv_expiry_delta;
 
                                        let value_contribution_msat = cmp::min(available_value_contribution_msat, $next_hops_value_contribution);
@@ -1881,13 +2051,13 @@ where L::Target: Logger {
                                                None => unreachable!(),
                                        };
                                        #[allow(unused_comparisons)] // $next_hops_path_htlc_minimum_msat is 0 in some calls so rustc complains
-                                       let over_path_minimum_msat = amount_to_transfer_over_msat >= $candidate.htlc_minimum_msat() &&
+                                       let over_path_minimum_msat = amount_to_transfer_over_msat >= htlc_minimum_msat &&
                                                amount_to_transfer_over_msat >= $next_hops_path_htlc_minimum_msat;
 
                                        #[allow(unused_comparisons)] // $next_hops_path_htlc_minimum_msat is 0 in some calls so rustc complains
                                        let may_overpay_to_meet_path_minimum_msat =
-                                               ((amount_to_transfer_over_msat < $candidate.htlc_minimum_msat() &&
-                                                 recommended_value_msat >= $candidate.htlc_minimum_msat()) ||
+                                               ((amount_to_transfer_over_msat < htlc_minimum_msat &&
+                                                 recommended_value_msat >= htlc_minimum_msat) ||
                                                 (amount_to_transfer_over_msat < $next_hops_path_htlc_minimum_msat &&
                                                  recommended_value_msat >= $next_hops_path_htlc_minimum_msat));
 
@@ -1962,19 +2132,25 @@ where L::Target: Logger {
                                                // payment path (upstream to the payee). To avoid that, we recompute
                                                // path fees knowing the final path contribution after constructing it.
                                                let curr_min = cmp::max(
-                                                       $next_hops_path_htlc_minimum_msat, $candidate.htlc_minimum_msat()
+                                                       $next_hops_path_htlc_minimum_msat, htlc_minimum_msat
                                                );
-                                               let path_htlc_minimum_msat = compute_fees_saturating(curr_min, $candidate.fees())
+                                               let candidate_fees = $candidate.fees();
+                                               let src_node_counter = $candidate.src_node_counter();
+                                               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[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(),
+                                                               target_node_counter: None,
                                                                fee_msat: 0,
                                                                next_hops_fee_msat: u64::max_value(),
                                                                hop_use_fee_msat: u64::max_value(),
@@ -1982,10 +2158,12 @@ 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,
-                                                       }
-                                               });
+                                                       });
+                                                       dist_entry.as_mut().unwrap()
+                                               };
 
                                                #[allow(unused_mut)] // We only use the mut in cfg(test)
                                                let mut should_process = !old_entry.was_processed;
@@ -2005,7 +2183,7 @@ where L::Target: Logger {
                                                        if src_node_id != our_node_id {
                                                                // Note that `u64::max_value` means we'll always fail the
                                                                // `old_entry.total_fee_msat > total_fee_msat` check below
-                                                               hop_use_fee_msat = compute_fees_saturating(amount_to_transfer_over_msat, $candidate.fees());
+                                                               hop_use_fee_msat = compute_fees_saturating(amount_to_transfer_over_msat, candidate_fees);
                                                                total_fee_msat = total_fee_msat.saturating_add(hop_use_fee_msat);
                                                        }
 
@@ -2035,15 +2213,6 @@ where L::Target: Logger {
                                                                                score_params);
                                                                let path_penalty_msat = $next_hops_path_penalty_msat
                                                                        .saturating_add(channel_penalty_msat);
-                                                               let new_graph_node = RouteGraphNode {
-                                                                       node_id: src_node_id,
-                                                                       lowest_fee_to_node: total_fee_msat,
-                                                                       total_cltv_delta: hop_total_cltv_delta,
-                                                                       value_contribution_msat,
-                                                                       path_htlc_minimum_msat,
-                                                                       path_penalty_msat,
-                                                                       path_length_to_node,
-                                                               };
 
                                                                // Update the way of reaching $candidate.source()
                                                                // with the given short_channel_id (from $candidate.target()),
@@ -2068,7 +2237,15 @@ where L::Target: Logger {
                                                                        .saturating_add(path_penalty_msat);
 
                                                                if !old_entry.was_processed && new_cost < old_cost {
+                                                                       let new_graph_node = RouteGraphNode {
+                                                                               node_id: src_node_id,
+                                                                               score: cmp::max(total_fee_msat, path_htlc_minimum_msat).saturating_add(path_penalty_msat),
+                                                                               total_cltv_delta: hop_total_cltv_delta,
+                                                                               value_contribution_msat,
+                                                                               path_length_to_node,
+                                                                       };
                                                                        targets.push(new_graph_node);
+                                                                       old_entry.target_node_counter = $candidate.target_node_counter();
                                                                        old_entry.next_hops_fee_msat = $next_hops_fee_msat;
                                                                        old_entry.hop_use_fee_msat = hop_use_fee_msat;
                                                                        old_entry.total_fee_msat = total_fee_msat;
@@ -2141,29 +2318,47 @@ where L::Target: Logger {
        // meaning how much will be paid in fees after this node (to the best of our knowledge).
        // This data can later be helpful to optimize routing (pay lower fees).
        macro_rules! add_entries_to_cheapest_to_target_node {
-               ( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr,
-                 $next_hops_path_htlc_minimum_msat: expr, $next_hops_path_penalty_msat: expr,
+               ( $node: expr, $node_id: expr, $next_hops_value_contribution: expr,
                  $next_hops_cltv_delta: expr, $next_hops_path_length: expr ) => {
-                       let skip_node = if let Some(elem) = dist.get_mut(&$node_id) {
+                       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.
                                // 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;
+                               is_first_hop_target = false;
                                false
                        };
 
                        if !skip_node {
-                               if let Some(first_channels) = first_hop_targets.get(&$node_id) {
-                                       for details in first_channels {
-                                               let candidate = CandidateRouteHop::FirstHop { details, node_id: our_node_id };
-                                               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 {
+                                                               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);
+                                               }
                                        }
                                }
 
@@ -2176,6 +2371,13 @@ where L::Target: Logger {
                                if !features.requires_unknown_bits() {
                                        for chan_id in $node.channels.iter() {
                                                let chan = network_channels.get(chan_id).unwrap();
+                                               // Calling chan.as_directed_to, below, will require access to memory two
+                                               // cache lines away from chan.features (in the form of `one_to_two` or
+                                               // `two_to_one`, depending on our direction). Thus, while we're looking at
+                                               // feature flags, go ahead and prefetch that memory, reducing the price we
+                                               // pay for it later.
+                                               prefetch_first_byte(&chan.one_to_two);
+                                               prefetch_first_byte(&chan.two_to_one);
                                                if !chan.features.requires_unknown_bits() {
                                                        if let Some((directed_channel, source)) = chan.as_directed_to(&$node_id) {
                                                                if first_hops.is_none() || *source != our_node_id {
@@ -2185,10 +2387,10 @@ where L::Target: Logger {
                                                                                        short_channel_id: *chan_id,
                                                                                };
                                                                                add_entry!(&candidate,
-                                                                                       $fee_to_target_msat,
+                                                                                       fee_to_target_msat,
                                                                                        $next_hops_value_contribution,
-                                                                                       $next_hops_path_htlc_minimum_msat,
-                                                                                       $next_hops_path_penalty_msat,
+                                                                                       next_hops_path_htlc_minimum_msat,
+                                                                                       next_hops_path_penalty_msat,
                                                                                        $next_hops_cltv_delta, $next_hops_path_length);
                                                                        }
                                                                }
@@ -2207,14 +2409,47 @@ where L::Target: Logger {
                // 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;
+               }
+               for (node_id, (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
+                       // we find a path to the target, so are left as dummies here.
+                       dist[*peer_node_counter as usize] = Some(PathBuildingHop {
+                               candidate: CandidateRouteHop::FirstHop {
+                                       details: &chans[0],
+                                       payer_node_id: &our_node_id,
+                                       target_node_counter: u32::max_value(),
+                                       payer_node_counter: u32::max_value(),
+                               },
+                               target_node_counter: None,
+                               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
                // 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 { details, node_id: our_node_id };
+                               let candidate = CandidateRouteHop::FirstHop {
+                                       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();
                                log_trace!(logger, "{} direct route to payee via {}",
@@ -2231,7 +2466,7 @@ where L::Target: Logger {
                        // If not, targets.pop() will not even let us enter the loop in step 2.
                        None => {},
                        Some(node) => {
-                               add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0, 0u64, 0, 0);
+                               add_entries_to_cheapest_to_target_node!(node, payee, path_value_msat, 0, 0);
                        },
                });
 
@@ -2241,27 +2476,42 @@ where L::Target: Logger {
                // it matters only if the fees are exactly the same.
                for (hint_idx, hint) in payment_params.payee.blinded_route_hints().iter().enumerate() {
                        let intro_node_id = NodeId::from_pubkey(&hint.1.introduction_node_id);
-                       let have_intro_node_in_graph =
+                       let intro_node_counter_opt =
                                // 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
                                // in the regular network graph.
-                               first_hop_targets.get(&intro_node_id).is_some() ||
-                               network_nodes.get(&intro_node_id).is_some();
-                       if !have_intro_node_in_graph || our_node_id == intro_node_id { continue }
+                               network_nodes.get(&intro_node_id).map(|node| node.node_counter)
+                               .or(
+                                       first_hop_targets.get(&intro_node_id).map(|(_, node_counter)| *node_counter)
+                               );
+                       if intro_node_counter_opt.is_none() || our_node_id == intro_node_id { continue }
                        let candidate = if hint.1.blinded_hops.len() == 1 {
-                               CandidateRouteHop::OneHopBlinded { hint, hint_idx }
-                       } else { CandidateRouteHop::Blinded { hint, hint_idx } };
+                               CandidateRouteHop::OneHopBlinded {
+                                       hint,
+                                       hint_idx,
+                                       source_node_counter: intro_node_counter_opt.unwrap(),
+                               }
+                       } else {
+                               CandidateRouteHop::Blinded {
+                                       hint,
+                                       hint_idx,
+                                       source_node_counter: intro_node_counter_opt.unwrap(),
+                               }
+                       };
                        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)
                        {
                                path_contribution_msat = hop_used_msat;
                        } else { continue }
-                       if let Some(first_channels) = first_hop_targets.get_mut(&NodeId::from_pubkey(&hint.1.introduction_node_id)) {
+                       if let Some((first_channels, peer_node_counter)) = first_hop_targets.get_mut(&NodeId::from_pubkey(&hint.1.introduction_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 { details, node_id: our_node_id};
+                                       let first_hop_candidate = CandidateRouteHop::FirstHop {
+                                               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,
                                                None => continue
@@ -2300,10 +2550,10 @@ where L::Target: Logger {
                                let mut aggregate_path_contribution_msat = path_value_msat;
 
                                for (idx, (hop, prev_hop_id)) in hop_iter.zip(prev_hop_iter).enumerate() {
-                                       let source = NodeId::from_pubkey(&hop.src_node_id);
-                                       let target = NodeId::from_pubkey(&prev_hop_id);
+                                       let (target, private_target_node_counter) = private_hop_key_cache.get(&prev_hop_id).unwrap();
+                                       let (_src_id, private_source_node_counter) = private_hop_key_cache.get(&hop.src_node_id).unwrap();
 
-                                       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);
@@ -2318,7 +2568,11 @@ where L::Target: Logger {
                                                        info,
                                                        short_channel_id: hop.short_channel_id,
                                                })
-                                               .unwrap_or_else(|| CandidateRouteHop::PrivateHop { hint: hop, target_node_id: target });
+                                               .unwrap_or_else(|| CandidateRouteHop::PrivateHop {
+                                                       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,
@@ -2354,11 +2608,14 @@ where L::Target: Logger {
                                                .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 { details, node_id: our_node_id};
+                                                       let first_hop_candidate = CandidateRouteHop::FirstHop {
+                                                               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,
                                                                aggregate_next_hops_path_htlc_minimum_msat, aggregate_next_hops_path_penalty_msat,
@@ -2399,11 +2656,14 @@ where L::Target: Logger {
                                                // 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 { details, node_id: our_node_id};
+                                                               let first_hop_candidate = CandidateRouteHop::FirstHop {
+                                                                       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,
@@ -2433,21 +2693,25 @@ where L::Target: Logger {
                // Both these cases (and other cases except reaching recommended_value_msat) mean that
                // paths_collection will be stopped because found_new_path==false.
                // This is not necessarily a routing failure.
-               'path_construction: while let Some(RouteGraphNode { node_id, lowest_fee_to_node, total_cltv_delta, mut value_contribution_msat, path_htlc_minimum_msat, path_penalty_msat, path_length_to_node, .. }) = targets.pop() {
+               'path_construction: while let Some(RouteGraphNode { node_id, total_cltv_delta, mut value_contribution_msat, path_length_to_node, .. }) = targets.pop() {
 
                        // 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 Some(scid) = ordered_hops.last().unwrap().0.candidate.short_channel_id() {
-                                                               if details.get_outbound_payment_scid().unwrap() == scid {
+                                                       if let CandidateRouteHop::FirstHop { details: last_hop_details, .. }
+                                                               = ordered_hops.last().unwrap().0.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();
                                                                        features_set = true;
                                                                        break;
@@ -2473,11 +2737,12 @@ where L::Target: Logger {
                                        // 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!).
@@ -2566,8 +2831,8 @@ where L::Target: Logger {
                        match network_nodes.get(&node_id) {
                                None => {},
                                Some(node) => {
-                                       add_entries_to_cheapest_to_target_node!(node, node_id, lowest_fee_to_node,
-                                               value_contribution_msat, path_htlc_minimum_msat, path_penalty_msat,
+                                       add_entries_to_cheapest_to_target_node!(node, node_id,
+                                               value_contribution_msat,
                                                total_cltv_delta, path_length_to_node);
                                },
                        }
@@ -2696,8 +2961,8 @@ where L::Target: Logger {
        });
        for idx in 0..(selected_route.len() - 1) {
                if idx + 1 >= selected_route.len() { break; }
-               if iter_equal(selected_route[idx].hops.iter().map(|h| (h.0.candidate.id(), h.0.candidate.target())),
-                                                                       selected_route[idx + 1].hops.iter().map(|h| (h.0.candidate.id(), h.0.candidate.target()))) {
+               if iter_equal(selected_route[idx    ].hops.iter().map(|h| (h.0.candidate.id(), h.0.candidate.target())),
+                             selected_route[idx + 1].hops.iter().map(|h| (h.0.candidate.id(), h.0.candidate.target()))) {
                        let new_value = selected_route[idx].get_value_msat() + selected_route[idx + 1].get_value_msat();
                        selected_route[idx].update_value_and_recompute_fees(new_value);
                        selected_route.remove(idx + 1);
@@ -6677,11 +6942,11 @@ mod tests {
        #[test]
        #[cfg(not(feature = "no-std"))]
        fn generate_routes() {
-               use crate::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters};
+               use crate::routing::scoring::ProbabilisticScoringFeeParameters;
 
                let logger = ln_test_utils::TestLogger::new();
-               let graph = match super::bench_utils::read_network_graph(&logger) {
-                       Ok(f) => f,
+               let (graph, mut scorer) = match super::bench_utils::read_graph_scorer(&logger) {
+                       Ok(res) => res,
                        Err(e) => {
                                eprintln!("{}", e);
                                return;
@@ -6689,7 +6954,6 @@ mod tests {
                };
 
                let params = ProbabilisticScoringFeeParameters::default();
-               let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &graph, &logger);
                let features = super::Bolt11InvoiceFeatures::empty();
 
                super::bench_utils::generate_test_routes(&graph, &mut scorer, &params, features, random_init_seed(), 0, 2);
@@ -6698,11 +6962,11 @@ mod tests {
        #[test]
        #[cfg(not(feature = "no-std"))]
        fn generate_routes_mpp() {
-               use crate::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters};
+               use crate::routing::scoring::ProbabilisticScoringFeeParameters;
 
                let logger = ln_test_utils::TestLogger::new();
-               let graph = match super::bench_utils::read_network_graph(&logger) {
-                       Ok(f) => f,
+               let (graph, mut scorer) = match super::bench_utils::read_graph_scorer(&logger) {
+                       Ok(res) => res,
                        Err(e) => {
                                eprintln!("{}", e);
                                return;
@@ -6710,7 +6974,6 @@ mod tests {
                };
 
                let params = ProbabilisticScoringFeeParameters::default();
-               let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &graph, &logger);
                let features = channelmanager::provided_bolt11_invoice_features(&UserConfig::default());
 
                super::bench_utils::generate_test_routes(&graph, &mut scorer, &params, features, random_init_seed(), 0, 2);
@@ -6722,8 +6985,8 @@ mod tests {
                use crate::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters};
 
                let logger = ln_test_utils::TestLogger::new();
-               let graph = match super::bench_utils::read_network_graph(&logger) {
-                       Ok(f) => f,
+               let (graph, mut scorer) = match super::bench_utils::read_graph_scorer(&logger) {
+                       Ok(res) => res,
                        Err(e) => {
                                eprintln!("{}", e);
                                return;
@@ -6731,7 +6994,7 @@ mod tests {
                };
 
                let params = ProbabilisticScoringFeeParameters::default();
-               let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &graph, &logger);
+               let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &*graph, &logger);
                let features = channelmanager::provided_bolt11_invoice_features(&UserConfig::default());
 
                super::bench_utils::generate_test_routes(&graph, &mut scorer, &params, features, random_init_seed(), 1_000_000, 2);
@@ -8068,46 +8331,63 @@ pub(crate) mod bench_utils {
        use crate::ln::channelmanager::{self, ChannelCounterparty, ChannelDetails};
        use crate::ln::features::Bolt11InvoiceFeatures;
        use crate::routing::gossip::NetworkGraph;
+       use crate::routing::scoring::ProbabilisticScorer;
        use crate::util::config::UserConfig;
        use crate::util::ser::ReadableArgs;
        use crate::util::test_utils::TestLogger;
+       use crate::sync::Arc;
 
        /// Tries to open a network graph file, or panics with a URL to fetch it.
-       pub(crate) fn get_route_file() -> Result<std::fs::File, &'static str> {
-               let res = File::open("net_graph-2023-01-18.bin") // By default we're run in RL/lightning
-                       .or_else(|_| File::open("lightning/net_graph-2023-01-18.bin")) // We may be run manually in RL/
-                       .or_else(|_| { // Fall back to guessing based on the binary location
-                               // path is likely something like .../rust-lightning/target/debug/deps/lightning-...
-                               let mut path = std::env::current_exe().unwrap();
-                               path.pop(); // lightning-...
-                               path.pop(); // deps
-                               path.pop(); // debug
-                               path.pop(); // target
-                               path.push("lightning");
-                               path.push("net_graph-2023-01-18.bin");
-                               File::open(path)
-                       })
-                       .or_else(|_| { // Fall back to guessing based on the binary location for a subcrate
-                               // path is likely something like .../rust-lightning/bench/target/debug/deps/bench..
-                               let mut path = std::env::current_exe().unwrap();
-                               path.pop(); // bench...
-                               path.pop(); // deps
-                               path.pop(); // debug
-                               path.pop(); // target
-                               path.pop(); // bench
-                               path.push("lightning");
-                               path.push("net_graph-2023-01-18.bin");
-                               File::open(path)
-                       })
-               .map_err(|_| "Please fetch https://bitcoin.ninja/ldk-net_graph-v0.0.113-2023-01-18.bin and place it at lightning/net_graph-2023-01-18.bin");
+       pub(crate) fn get_graph_scorer_file() -> Result<(std::fs::File, std::fs::File), &'static str> {
+               let load_file = |fname, err_str| {
+                       File::open(fname) // By default we're run in RL/lightning
+                               .or_else(|_| File::open(&format!("lightning/{}", fname))) // We may be run manually in RL/
+                               .or_else(|_| { // Fall back to guessing based on the binary location
+                                       // path is likely something like .../rust-lightning/target/debug/deps/lightning-...
+                                       let mut path = std::env::current_exe().unwrap();
+                                       path.pop(); // lightning-...
+                                       path.pop(); // deps
+                                       path.pop(); // debug
+                                       path.pop(); // target
+                                       path.push("lightning");
+                                       path.push(fname);
+                                       File::open(path)
+                               })
+                               .or_else(|_| { // Fall back to guessing based on the binary location for a subcrate
+                                       // path is likely something like .../rust-lightning/bench/target/debug/deps/bench..
+                                       let mut path = std::env::current_exe().unwrap();
+                                       path.pop(); // bench...
+                                       path.pop(); // deps
+                                       path.pop(); // debug
+                                       path.pop(); // target
+                                       path.pop(); // bench
+                                       path.push("lightning");
+                                       path.push(fname);
+                                       File::open(path)
+                               })
+                       .map_err(|_| err_str)
+               };
+               let graph_res = load_file(
+                       "net_graph-2023-12-10.bin",
+                       "Please fetch https://bitcoin.ninja/ldk-net_graph-v0.0.118-2023-12-10.bin and place it at lightning/net_graph-2023-12-10.bin"
+               );
+               let scorer_res = load_file(
+                       "scorer-2023-12-10.bin",
+                       "Please fetch https://bitcoin.ninja/ldk-scorer-v0.0.118-2023-12-10.bin and place it at scorer-2023-12-10.bin"
+               );
                #[cfg(require_route_graph_test)]
-               return Ok(res.unwrap());
+               return Ok((graph_res.unwrap(), scorer_res.unwrap()));
                #[cfg(not(require_route_graph_test))]
-               return res;
+               return Ok((graph_res?, scorer_res?));
        }
 
-       pub(crate) fn read_network_graph(logger: &TestLogger) -> Result<NetworkGraph<&TestLogger>, &'static str> {
-               get_route_file().map(|mut f| NetworkGraph::read(&mut f, logger).unwrap())
+       pub(crate) fn read_graph_scorer(logger: &TestLogger)
+       -> Result<(Arc<NetworkGraph<&TestLogger>>, ProbabilisticScorer<Arc<NetworkGraph<&TestLogger>>, &TestLogger>), &'static str> {
+               let (mut graph_file, mut scorer_file) = get_graph_scorer_file()?;
+               let graph = Arc::new(NetworkGraph::read(&mut graph_file, logger).unwrap());
+               let scorer_args = (Default::default(), Arc::clone(&graph), logger);
+               let scorer = ProbabilisticScorer::read(&mut scorer_file, scorer_args).unwrap();
+               Ok((graph, scorer))
        }
 
        pub(crate) fn payer_pubkey() -> PublicKey {
@@ -8167,9 +8447,7 @@ pub(crate) mod bench_utils {
 
                let nodes = graph.read_only().nodes().clone();
                let mut route_endpoints = Vec::new();
-               // Fetch 1.5x more routes than we need as after we do some scorer updates we may end up
-               // with some routes we picked being un-routable.
-               for _ in 0..route_count * 3 / 2 {
+               for _ in 0..route_count {
                        loop {
                                seed = seed.overflowing_mul(6364136223846793005).0.overflowing_add(1).0;
                                let src = PublicKey::from_slice(nodes.unordered_keys()
@@ -8187,37 +8465,6 @@ pub(crate) mod bench_utils {
                                        get_route(&payer, &route_params, &graph.read_only(), Some(&[&first_hop]),
                                                &TestLogger::new(), scorer, score_params, &random_seed_bytes).is_ok();
                                if path_exists {
-                                       // ...and seed the scorer with success and failure data...
-                                       seed = seed.overflowing_mul(6364136223846793005).0.overflowing_add(1).0;
-                                       let mut score_amt = seed % 1_000_000_000;
-                                       loop {
-                                               // Generate fail/success paths for a wider range of potential amounts with
-                                               // MPP enabled to give us a chance to apply penalties for more potential
-                                               // routes.
-                                               let mpp_features = channelmanager::provided_bolt11_invoice_features(&UserConfig::default());
-                                               let params = PaymentParameters::from_node_id(dst, 42)
-                                                       .with_bolt11_features(mpp_features).unwrap();
-                                               let route_params = RouteParameters::from_payment_params_and_value(
-                                                       params.clone(), score_amt);
-                                               let route_res = get_route(&payer, &route_params, &graph.read_only(),
-                                                       Some(&[&first_hop]), &TestLogger::new(), scorer, score_params,
-                                                       &random_seed_bytes);
-                                               if let Ok(route) = route_res {
-                                                       for path in route.paths {
-                                                               if seed & 0x80 == 0 {
-                                                                       scorer.payment_path_successful(&path);
-                                                               } else {
-                                                                       let short_channel_id = path.hops[path.hops.len() / 2].short_channel_id;
-                                                                       scorer.payment_path_failed(&path, short_channel_id);
-                                                               }
-                                                               seed = seed.overflowing_mul(6364136223846793005).0.overflowing_add(1).0;
-                                                       }
-                                                       break;
-                                               }
-                                               // If we couldn't find a path with a higer amount, reduce and try again.
-                                               score_amt /= 100;
-                                       }
-
                                        route_endpoints.push((first_hop, params, amt_msat));
                                        break;
                                }
@@ -8247,7 +8494,7 @@ pub mod benches {
        use crate::ln::channelmanager;
        use crate::ln::features::Bolt11InvoiceFeatures;
        use crate::routing::gossip::NetworkGraph;
-       use crate::routing::scoring::{FixedPenaltyScorer, ProbabilisticScorer, ProbabilisticScoringFeeParameters, ProbabilisticScoringDecayParameters};
+       use crate::routing::scoring::{FixedPenaltyScorer, ProbabilisticScoringFeeParameters};
        use crate::util::config::UserConfig;
        use crate::util::logger::{Logger, Record};
        use crate::util::test_utils::TestLogger;
@@ -8261,7 +8508,7 @@ pub mod benches {
 
        pub fn generate_routes_with_zero_penalty_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, _) = bench_utils::read_graph_scorer(&logger).unwrap();
                let scorer = FixedPenaltyScorer::with_penalty(0);
                generate_routes(bench, &network_graph, scorer, &Default::default(),
                        Bolt11InvoiceFeatures::empty(), 0, "generate_routes_with_zero_penalty_scorer");
@@ -8269,7 +8516,7 @@ pub mod benches {
 
        pub fn generate_mpp_routes_with_zero_penalty_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, _) = bench_utils::read_graph_scorer(&logger).unwrap();
                let scorer = FixedPenaltyScorer::with_penalty(0);
                generate_routes(bench, &network_graph, scorer, &Default::default(),
                        channelmanager::provided_bolt11_invoice_features(&UserConfig::default()), 0,
@@ -8278,18 +8525,16 @@ pub mod benches {
 
        pub fn generate_routes_with_probabilistic_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
                let params = ProbabilisticScoringFeeParameters::default();
-               let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                generate_routes(bench, &network_graph, scorer, &params, Bolt11InvoiceFeatures::empty(), 0,
                        "generate_routes_with_probabilistic_scorer");
        }
 
        pub fn generate_mpp_routes_with_probabilistic_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
                let params = ProbabilisticScoringFeeParameters::default();
-               let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                generate_routes(bench, &network_graph, scorer, &params,
                        channelmanager::provided_bolt11_invoice_features(&UserConfig::default()), 0,
                        "generate_mpp_routes_with_probabilistic_scorer");
@@ -8297,9 +8542,8 @@ pub mod benches {
 
        pub fn generate_large_mpp_routes_with_probabilistic_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
                let params = ProbabilisticScoringFeeParameters::default();
-               let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                generate_routes(bench, &network_graph, scorer, &params,
                        channelmanager::provided_bolt11_invoice_features(&UserConfig::default()), 100_000_000,
                        "generate_large_mpp_routes_with_probabilistic_scorer");
@@ -8307,11 +8551,9 @@ pub mod benches {
 
        pub fn generate_routes_with_nonlinear_probabilistic_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
                let mut params = ProbabilisticScoringFeeParameters::default();
                params.linear_success_probability = false;
-               let scorer = ProbabilisticScorer::new(
-                       ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                generate_routes(bench, &network_graph, scorer, &params,
                        channelmanager::provided_bolt11_invoice_features(&UserConfig::default()), 0,
                        "generate_routes_with_nonlinear_probabilistic_scorer");
@@ -8319,11 +8561,9 @@ pub mod benches {
 
        pub fn generate_mpp_routes_with_nonlinear_probabilistic_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
                let mut params = ProbabilisticScoringFeeParameters::default();
                params.linear_success_probability = false;
-               let scorer = ProbabilisticScorer::new(
-                       ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                generate_routes(bench, &network_graph, scorer, &params,
                        channelmanager::provided_bolt11_invoice_features(&UserConfig::default()), 0,
                        "generate_mpp_routes_with_nonlinear_probabilistic_scorer");
@@ -8331,11 +8571,9 @@ pub mod benches {
 
        pub fn generate_large_mpp_routes_with_nonlinear_probabilistic_scorer(bench: &mut Criterion) {
                let logger = TestLogger::new();
-               let network_graph = bench_utils::read_network_graph(&logger).unwrap();
+               let (network_graph, scorer) = bench_utils::read_graph_scorer(&logger).unwrap();
                let mut params = ProbabilisticScoringFeeParameters::default();
                params.linear_success_probability = false;
-               let scorer = ProbabilisticScorer::new(
-                       ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                generate_routes(bench, &network_graph, scorer, &params,
                        channelmanager::provided_bolt11_invoice_features(&UserConfig::default()), 100_000_000,
                        "generate_large_mpp_routes_with_nonlinear_probabilistic_scorer");
@@ -8346,14 +8584,23 @@ pub mod benches {
                score_params: &S::ScoreParams, features: Bolt11InvoiceFeatures, starting_amount: u64,
                bench_name: &'static str,
        ) {
-               let payer = bench_utils::payer_pubkey();
-               let keys_manager = KeysManager::new(&[0u8; 32], 42, 42);
-               let random_seed_bytes = keys_manager.get_secure_random_bytes();
-
                // First, get 100 (source, destination) pairs for which route-getting actually succeeds...
                let route_endpoints = bench_utils::generate_test_routes(graph, &mut scorer, score_params, features, 0xdeadbeef, starting_amount, 50);
 
                // ...then benchmark finding paths between the nodes we learned.
+               do_route_bench(bench, graph, scorer, score_params, bench_name, route_endpoints);
+       }
+
+       #[inline(never)]
+       fn do_route_bench<S: ScoreLookUp + ScoreUpdate>(
+               bench: &mut Criterion, graph: &NetworkGraph<&TestLogger>, scorer: S,
+               score_params: &S::ScoreParams, bench_name: &'static str,
+               route_endpoints: Vec<(ChannelDetails, PaymentParameters, u64)>,
+       ) {
+               let payer = bench_utils::payer_pubkey();
+               let keys_manager = KeysManager::new(&[0u8; 32], 42, 42);
+               let random_seed_bytes = keys_manager.get_secure_random_bytes();
+
                let mut idx = 0;
                bench.bench_function(bench_name, |b| b.iter(|| {
                        let (first_hop, params, amt) = &route_endpoints[idx % route_endpoints.len()];