Track historical liquidity update time separately from the bounds
[rust-lightning] / lightning / src / routing / scoring.rs
index 207e1d69bd98d2b66974884ab1226f1b36562a73..9c03ff40de0518c067a8112dfb2e7ffbcbe66fd6 100644 (file)
@@ -26,7 +26,7 @@
 //! #
 //! # struct FakeLogger {};
 //! # impl Logger for FakeLogger {
-//! #     fn log(&self, record: &Record) { unimplemented!() }
+//! #     fn log(&self, record: Record) { unimplemented!() }
 //! # }
 //! # fn find_scored_route(payer: PublicKey, route_params: RouteParameters, network_graph: NetworkGraph<&FakeLogger>) {
 //! # let logger = FakeLogger {};
@@ -58,7 +58,7 @@
 
 use crate::ln::msgs::DecodeError;
 use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId};
-use crate::routing::router::Path;
+use crate::routing::router::{Path, CandidateRouteHop};
 use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer};
 use crate::util::logger::Logger;
 use crate::util::time::Time;
@@ -103,23 +103,29 @@ pub trait ScoreLookUp {
        /// [`u64::max_value`] is given to indicate sufficient capacity for the invoice's full amount.
        /// Thus, implementations should be overflow-safe.
        fn channel_penalty_msat(
-               &self, short_channel_id: u64, source: &NodeId, target: &NodeId, usage: ChannelUsage, score_params: &Self::ScoreParams
+               &self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams
        ) -> u64;
 }
 
 /// `ScoreUpdate` is used to update the scorer's internal state after a payment attempt.
 pub trait ScoreUpdate {
        /// Handles updating channel penalties after failing to route through a channel.
-       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64);
+       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration);
 
        /// Handles updating channel penalties after successfully routing along a path.
-       fn payment_path_successful(&mut self, path: &Path);
+       fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration);
 
        /// Handles updating channel penalties after a probe over the given path failed.
-       fn probe_failed(&mut self, path: &Path, short_channel_id: u64);
+       fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration);
 
        /// Handles updating channel penalties after a probe over the given path succeeded.
-       fn probe_successful(&mut self, path: &Path);
+       fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration);
+
+       /// Scorers may wish to reduce their certainty of channel liquidity information over time.
+       /// Thus, this method is provided to allow scorers to observe the passage of time - the holder
+       /// of this object should call this method regularly (generally via the
+       /// `lightning-background-processor` crate).
+       fn time_passed(&mut self, duration_since_epoch: Duration);
 }
 
 /// A trait which can both lookup and update routing channel penalty scores.
@@ -137,28 +143,32 @@ impl<T: ScoreLookUp + ScoreUpdate $(+ $supertrait)*> Score for T {}
 impl<S: ScoreLookUp, T: Deref<Target=S>> ScoreLookUp for T {
        type ScoreParams = S::ScoreParams;
        fn channel_penalty_msat(
-               &self, short_channel_id: u64, source: &NodeId, target: &NodeId, usage: ChannelUsage, score_params: &Self::ScoreParams
+               &self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams
        ) -> u64 {
-               self.deref().channel_penalty_msat(short_channel_id, source, target, usage, score_params)
+               self.deref().channel_penalty_msat(candidate, usage, score_params)
        }
 }
 
 #[cfg(not(c_bindings))]
 impl<S: ScoreUpdate, T: DerefMut<Target=S>> ScoreUpdate for T {
-       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64) {
-               self.deref_mut().payment_path_failed(path, short_channel_id)
+       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
+               self.deref_mut().payment_path_failed(path, short_channel_id, duration_since_epoch)
+       }
+
+       fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
+               self.deref_mut().payment_path_successful(path, duration_since_epoch)
        }
 
-       fn payment_path_successful(&mut self, path: &Path) {
-               self.deref_mut().payment_path_successful(path)
+       fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
+               self.deref_mut().probe_failed(path, short_channel_id, duration_since_epoch)
        }
 
-       fn probe_failed(&mut self, path: &Path, short_channel_id: u64) {
-               self.deref_mut().probe_failed(path, short_channel_id)
+       fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
+               self.deref_mut().probe_successful(path, duration_since_epoch)
        }
 
-       fn probe_successful(&mut self, path: &Path) {
-               self.deref_mut().probe_successful(path)
+       fn time_passed(&mut self, duration_since_epoch: Duration) {
+               self.deref_mut().time_passed(duration_since_epoch)
        }
 }
 } }
@@ -315,10 +325,9 @@ impl<'a, T: 'a + Score> Deref for MultiThreadedScoreLockRead<'a, T> {
 #[cfg(c_bindings)]
 impl<'a, T: Score> ScoreLookUp for MultiThreadedScoreLockRead<'a, T> {
        type ScoreParams = T::ScoreParams;
-       fn channel_penalty_msat(&self, short_channel_id: u64, source: &NodeId,
-               target: &NodeId, usage: ChannelUsage, score_params: &Self::ScoreParams
+       fn channel_penalty_msat(&self, candidate:&CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams
        ) -> u64 {
-               self.0.channel_penalty_msat(short_channel_id, source, target, usage, score_params)
+               self.0.channel_penalty_msat(candidate, usage, score_params)
        }
 }
 
@@ -347,20 +356,24 @@ impl<'a, T: 'a + Score> DerefMut for MultiThreadedScoreLockWrite<'a, T> {
 
 #[cfg(c_bindings)]
 impl<'a, T: Score> ScoreUpdate for MultiThreadedScoreLockWrite<'a, T> {
-       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64) {
-               self.0.payment_path_failed(path, short_channel_id)
+       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
+               self.0.payment_path_failed(path, short_channel_id, duration_since_epoch)
        }
 
-       fn payment_path_successful(&mut self, path: &Path) {
-               self.0.payment_path_successful(path)
+       fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
+               self.0.payment_path_successful(path, duration_since_epoch)
        }
 
-       fn probe_failed(&mut self, path: &Path, short_channel_id: u64) {
-               self.0.probe_failed(path, short_channel_id)
+       fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
+               self.0.probe_failed(path, short_channel_id, duration_since_epoch)
        }
 
-       fn probe_successful(&mut self, path: &Path) {
-               self.0.probe_successful(path)
+       fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
+               self.0.probe_successful(path, duration_since_epoch)
+       }
+
+       fn time_passed(&mut self, duration_since_epoch: Duration) {
+               self.0.time_passed(duration_since_epoch)
        }
 }
 
@@ -394,19 +407,21 @@ impl FixedPenaltyScorer {
 
 impl ScoreLookUp for FixedPenaltyScorer {
        type ScoreParams = ();
-       fn channel_penalty_msat(&self, _: u64, _: &NodeId, _: &NodeId, _: ChannelUsage, _score_params: &Self::ScoreParams) -> u64 {
+       fn channel_penalty_msat(&self, _: &CandidateRouteHop, _: ChannelUsage, _score_params: &Self::ScoreParams) -> u64 {
                self.penalty_msat
        }
 }
 
 impl ScoreUpdate for FixedPenaltyScorer {
-       fn payment_path_failed(&mut self, _path: &Path, _short_channel_id: u64) {}
+       fn payment_path_failed(&mut self, _path: &Path, _short_channel_id: u64, _duration_since_epoch: Duration) {}
+
+       fn payment_path_successful(&mut self, _path: &Path, _duration_since_epoch: Duration) {}
 
-       fn payment_path_successful(&mut self, _path: &Path) {}
+       fn probe_failed(&mut self, _path: &Path, _short_channel_id: u64, _duration_since_epoch: Duration) {}
 
-       fn probe_failed(&mut self, _path: &Path, _short_channel_id: u64) {}
+       fn probe_successful(&mut self, _path: &Path, _duration_since_epoch: Duration) {}
 
-       fn probe_successful(&mut self, _path: &Path) {}
+       fn time_passed(&mut self, _duration_since_epoch: Duration) {}
 }
 
 impl Writeable for FixedPenaltyScorer {
@@ -790,11 +805,14 @@ struct ChannelLiquidity<T: Time> {
        /// Upper channel liquidity bound in terms of an offset from the effective capacity.
        max_liquidity_offset_msat: u64,
 
+       min_liquidity_offset_history: HistoricalBucketRangeTracker,
+       max_liquidity_offset_history: HistoricalBucketRangeTracker,
+
        /// Time when the liquidity bounds were last modified.
        last_updated: T,
 
-       min_liquidity_offset_history: HistoricalBucketRangeTracker,
-       max_liquidity_offset_history: HistoricalBucketRangeTracker,
+       /// Time when the historical liquidity bounds were last modified.
+       offset_history_last_updated: T,
 }
 
 /// A snapshot of [`ChannelLiquidity`] in one direction assuming a certain channel capacity and
@@ -805,6 +823,7 @@ struct DirectedChannelLiquidity<L: Deref<Target = u64>, BRT: Deref<Target = Hist
        liquidity_history: HistoricalMinMaxBuckets<BRT>,
        capacity_msat: u64,
        last_updated: U,
+       offset_history_last_updated: U,
        now: T,
        decay_params: ProbabilisticScoringDecayParameters,
 }
@@ -843,7 +862,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
                                                let dir_liq = liq.as_directed(source, target, amt, self.decay_params);
 
                                                let (min_buckets, max_buckets) = dir_liq.liquidity_history
-                                                       .get_decayed_buckets(now, *dir_liq.last_updated,
+                                                       .get_decayed_buckets(now, *dir_liq.offset_history_last_updated,
                                                                self.decay_params.historical_no_updates_half_life)
                                                        .unwrap_or(([0; 32], [0; 32]));
 
@@ -940,7 +959,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
 
                                        let (min_buckets, mut max_buckets) =
                                                dir_liq.liquidity_history.get_decayed_buckets(
-                                                       dir_liq.now, *dir_liq.last_updated,
+                                                       dir_liq.now, *dir_liq.offset_history_last_updated,
                                                        self.decay_params.historical_no_updates_half_life
                                                )?;
 
@@ -973,7 +992,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
                                        let dir_liq = liq.as_directed(source, target, capacity_msat, self.decay_params);
 
                                        return dir_liq.liquidity_history.calculate_success_probability_times_billion(
-                                               dir_liq.now, *dir_liq.last_updated,
+                                               dir_liq.now, *dir_liq.offset_history_last_updated,
                                                self.decay_params.historical_no_updates_half_life, &params, amount_msat,
                                                capacity_msat
                                        ).map(|p| p as f64 / (1024 * 1024 * 1024) as f64);
@@ -993,6 +1012,7 @@ impl<T: Time> ChannelLiquidity<T> {
                        min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                        max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                        last_updated: T::now(),
+                       offset_history_last_updated: T::now(),
                }
        }
 
@@ -1019,6 +1039,7 @@ impl<T: Time> ChannelLiquidity<T> {
                        },
                        capacity_msat,
                        last_updated: &self.last_updated,
+                       offset_history_last_updated: &self.offset_history_last_updated,
                        now: T::now(),
                        decay_params: decay_params,
                }
@@ -1047,6 +1068,7 @@ impl<T: Time> ChannelLiquidity<T> {
                        },
                        capacity_msat,
                        last_updated: &mut self.last_updated,
+                       offset_history_last_updated: &mut self.offset_history_last_updated,
                        now: T::now(),
                        decay_params: decay_params,
                }
@@ -1182,7 +1204,8 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
                if score_params.historical_liquidity_penalty_multiplier_msat != 0 ||
                   score_params.historical_liquidity_penalty_amount_multiplier_msat != 0 {
                        if let Some(cumulative_success_prob_times_billion) = self.liquidity_history
-                               .calculate_success_probability_times_billion(self.now, *self.last_updated,
+                               .calculate_success_probability_times_billion(
+                                       self.now, *self.offset_history_last_updated,
                                        self.decay_params.historical_no_updates_half_life, score_params, amount_msat,
                                        self.capacity_msat)
                        {
@@ -1301,7 +1324,7 @@ impl<L: DerefMut<Target = u64>, BRT: DerefMut<Target = HistoricalBucketRangeTrac
        /// state"), we allow the caller to set an offset applied to our liquidity bounds which
        /// represents the amount of the successful payment we just made.
        fn update_history_buckets(&mut self, bucket_offset_msat: u64) {
-               let half_lives = self.now.duration_since(*self.last_updated).as_secs()
+               let half_lives = self.now.duration_since(*self.offset_history_last_updated).as_secs()
                        .checked_div(self.decay_params.historical_no_updates_half_life.as_secs())
                        .map(|v| v.try_into().unwrap_or(u32::max_value())).unwrap_or(u32::max_value());
                self.liquidity_history.min_liquidity_offset_history.time_decay_data(half_lives);
@@ -1326,6 +1349,7 @@ impl<L: DerefMut<Target = u64>, BRT: DerefMut<Target = HistoricalBucketRangeTrac
                        self.decayed_offset_msat(*self.max_liquidity_offset_msat)
                };
                *self.last_updated = self.now;
+               *self.offset_history_last_updated = self.now;
        }
 
        /// Adjusts the upper bound of the channel liquidity balance in this direction.
@@ -1337,15 +1361,23 @@ impl<L: DerefMut<Target = u64>, BRT: DerefMut<Target = HistoricalBucketRangeTrac
                        self.decayed_offset_msat(*self.min_liquidity_offset_msat)
                };
                *self.last_updated = self.now;
+               *self.offset_history_last_updated = self.now;
        }
 }
 
 impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreLookUp for ProbabilisticScorerUsingTime<G, L, T> where L::Target: Logger {
        type ScoreParams = ProbabilisticScoringFeeParameters;
        fn channel_penalty_msat(
-               &self, short_channel_id: u64, source: &NodeId, target: &NodeId, usage: ChannelUsage, score_params: &ProbabilisticScoringFeeParameters
+               &self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &ProbabilisticScoringFeeParameters
        ) -> u64 {
-               if let Some(penalty) = score_params.manual_node_penalties.get(target) {
+               let (scid, target) = match candidate {
+                       CandidateRouteHop::PublicHop { info, short_channel_id } => {
+                               (short_channel_id, info.target())
+                       },
+                       _ => return 0,
+               };
+               let source = candidate.source();
+               if let Some(penalty) = score_params.manual_node_penalties.get(&target) {
                        return *penalty;
                }
 
@@ -1375,9 +1407,9 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreLookUp for Prob
                let amount_msat = usage.amount_msat.saturating_add(usage.inflight_htlc_msat);
                let capacity_msat = usage.effective_capacity.as_msat();
                self.channel_liquidities
-                       .get(&short_channel_id)
+                       .get(&scid)
                        .unwrap_or(&ChannelLiquidity::new())
-                       .as_directed(source, target, capacity_msat, self.decay_params)
+                       .as_directed(&source, &target, capacity_msat, self.decay_params)
                        .penalty_msat(amount_msat, score_params)
                        .saturating_add(anti_probing_penalty_msat)
                        .saturating_add(base_penalty_msat)
@@ -1385,7 +1417,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreLookUp for Prob
 }
 
 impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreUpdate for ProbabilisticScorerUsingTime<G, L, T> where L::Target: Logger {
-       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64) {
+       fn payment_path_failed(&mut self, path: &Path, short_channel_id: u64, _duration_since_epoch: Duration) {
                let amount_msat = path.final_value_msat();
                log_trace!(self.logger, "Scoring path through to SCID {} as having failed at {} msat", short_channel_id, amount_msat);
                let network_graph = self.network_graph.read_only();
@@ -1424,7 +1456,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreUpdate for Prob
                }
        }
 
-       fn payment_path_successful(&mut self, path: &Path) {
+       fn payment_path_successful(&mut self, path: &Path, _duration_since_epoch: Duration) {
                let amount_msat = path.final_value_msat();
                log_trace!(self.logger, "Scoring path through SCID {} as having succeeded at {} msat.",
                        path.hops.split_last().map(|(hop, _)| hop.short_channel_id).unwrap_or(0), amount_msat);
@@ -1450,19 +1482,31 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ScoreUpdate for Prob
                }
        }
 
-       fn probe_failed(&mut self, path: &Path, short_channel_id: u64) {
-               self.payment_path_failed(path, short_channel_id)
+       fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
+               self.payment_path_failed(path, short_channel_id, duration_since_epoch)
        }
 
-       fn probe_successful(&mut self, path: &Path) {
-               self.payment_path_failed(path, u64::max_value())
+       fn probe_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
+               self.payment_path_failed(path, u64::max_value(), duration_since_epoch)
        }
+
+       fn time_passed(&mut self, _duration_since_epoch: Duration) {}
 }
 
 #[cfg(c_bindings)]
 impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> Score for ProbabilisticScorerUsingTime<G, L, T>
 where L::Target: Logger {}
 
+#[cfg(feature = "std")]
+#[inline]
+fn powf64(n: f64, exp: f64) -> f64 {
+       n.powf(exp)
+}
+#[cfg(not(feature = "std"))]
+fn powf64(n: f64, exp: f64) -> f64 {
+       libm::powf(n as f32, exp as f32) as f64
+}
+
 mod approx {
        const BITS: u32 = 64;
        const HIGHEST_BIT: u32 = BITS - 1;
@@ -1949,9 +1993,9 @@ mod bucketed_history {
        }
 
        impl<D: Deref<Target = HistoricalBucketRangeTracker>> HistoricalMinMaxBuckets<D> {
-               pub(super) fn get_decayed_buckets<T: Time>(&self, now: T, last_updated: T, half_life: Duration)
+               pub(super) fn get_decayed_buckets<T: Time>(&self, now: T, offset_history_last_updated: T, half_life: Duration)
                -> Option<([u16; 32], [u16; 32])> {
-                       let (_, required_decays) = self.get_total_valid_points(now, last_updated, half_life)?;
+                       let (_, required_decays) = self.get_total_valid_points(now, offset_history_last_updated, half_life)?;
 
                        let mut min_buckets = *self.min_liquidity_offset_history;
                        min_buckets.time_decay_data(required_decays);
@@ -1960,9 +2004,9 @@ mod bucketed_history {
                        Some((min_buckets.buckets, max_buckets.buckets))
                }
                #[inline]
-               pub(super) fn get_total_valid_points<T: Time>(&self, now: T, last_updated: T, half_life: Duration)
+               pub(super) fn get_total_valid_points<T: Time>(&self, now: T, offset_history_last_updated: T, half_life: Duration)
                -> Option<(u64, u32)> {
-                       let required_decays = now.duration_since(last_updated).as_secs()
+                       let required_decays = now.duration_since(offset_history_last_updated).as_secs()
                                .checked_div(half_life.as_secs())
                                .map_or(u32::max_value(), |decays| cmp::min(decays, u32::max_value() as u64) as u32);
 
@@ -1985,7 +2029,7 @@ mod bucketed_history {
 
                #[inline]
                pub(super) fn calculate_success_probability_times_billion<T: Time>(
-                       &self, now: T, last_updated: T, half_life: Duration,
+                       &self, now: T, offset_history_last_updated: T, half_life: Duration,
                        params: &ProbabilisticScoringFeeParameters, amount_msat: u64, capacity_msat: u64
                ) -> Option<u64> {
                        // If historical penalties are enabled, we try to calculate a probability of success
@@ -2001,7 +2045,7 @@ mod bucketed_history {
                        // Check if all our buckets are zero, once decayed and treat it as if we had no data. We
                        // don't actually use the decayed buckets, though, as that would lose precision.
                        let (total_valid_points_tracked, _)
-                               = self.get_total_valid_points(now, last_updated, half_life)?;
+                               = self.get_total_valid_points(now, offset_history_last_updated, half_life)?;
 
                        let mut cumulative_success_prob_times_billion = 0;
                        // Special-case the 0th min bucket - it generally means we failed a payment, so only
@@ -2094,6 +2138,8 @@ ReadableArgs<(ProbabilisticScoringDecayParameters, G, L)> for ProbabilisticScore
 impl<T: Time> Writeable for ChannelLiquidity<T> {
        #[inline]
        fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+               let offset_history_duration_since_epoch =
+                       T::duration_since_epoch() - self.offset_history_last_updated.elapsed();
                let duration_since_epoch = T::duration_since_epoch() - self.last_updated.elapsed();
                write_tlv_fields!(w, {
                        (0, self.min_liquidity_offset_msat, required),
@@ -2103,6 +2149,7 @@ impl<T: Time> Writeable for ChannelLiquidity<T> {
                        (4, duration_since_epoch, required),
                        (5, Some(self.min_liquidity_offset_history), option),
                        (7, Some(self.max_liquidity_offset_history), option),
+                       (9, offset_history_duration_since_epoch, required),
                });
                Ok(())
        }
@@ -2118,6 +2165,7 @@ impl<T: Time> Readable for ChannelLiquidity<T> {
                let mut min_liquidity_offset_history: Option<HistoricalBucketRangeTracker> = None;
                let mut max_liquidity_offset_history: Option<HistoricalBucketRangeTracker> = None;
                let mut duration_since_epoch = Duration::from_secs(0);
+               let mut offset_history_duration_since_epoch = None;
                read_tlv_fields!(r, {
                        (0, min_liquidity_offset_msat, required),
                        (1, legacy_min_liq_offset_history, option),
@@ -2126,6 +2174,7 @@ impl<T: Time> Readable for ChannelLiquidity<T> {
                        (4, duration_since_epoch, required),
                        (5, min_liquidity_offset_history, option),
                        (7, max_liquidity_offset_history, option),
+                       (9, offset_history_duration_since_epoch, option),
                });
                // On rust prior to 1.60 `Instant::duration_since` will panic if time goes backwards.
                // We write `last_updated` as wallclock time even though its ultimately an `Instant` (which
@@ -2139,6 +2188,13 @@ impl<T: Time> Readable for ChannelLiquidity<T> {
                let last_updated = if wall_clock_now > duration_since_epoch {
                        now - (wall_clock_now - duration_since_epoch)
                } else { now };
+
+               let offset_history_duration_since_epoch =
+                       offset_history_duration_since_epoch.unwrap_or(duration_since_epoch);
+               let offset_history_last_updated = if wall_clock_now > offset_history_duration_since_epoch {
+                       now - (wall_clock_now - offset_history_duration_since_epoch)
+               } else { now };
+
                if min_liquidity_offset_history.is_none() {
                        if let Some(legacy_buckets) = legacy_min_liq_offset_history {
                                min_liquidity_offset_history = Some(legacy_buckets.into_current());
@@ -2159,6 +2215,7 @@ impl<T: Time> Readable for ChannelLiquidity<T> {
                        min_liquidity_offset_history: min_liquidity_offset_history.unwrap(),
                        max_liquidity_offset_history: max_liquidity_offset_history.unwrap(),
                        last_updated,
+                       offset_history_last_updated,
                })
        }
 }
@@ -2174,12 +2231,12 @@ mod tests {
        use crate::ln::channelmanager;
        use crate::ln::msgs::{ChannelAnnouncement, ChannelUpdate, UnsignedChannelAnnouncement, UnsignedChannelUpdate};
        use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId};
-       use crate::routing::router::{BlindedTail, Path, RouteHop};
+       use crate::routing::router::{BlindedTail, Path, RouteHop, CandidateRouteHop};
        use crate::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate};
        use crate::util::ser::{ReadableArgs, Writeable};
        use crate::util::test_utils::{self, TestLogger};
 
-       use bitcoin::blockdata::constants::genesis_block;
+       use bitcoin::blockdata::constants::ChainHash;
        use bitcoin::hashes::Hash;
        use bitcoin::hashes::sha256d::Hash as Sha256dHash;
        use bitcoin::network::constants::Network;
@@ -2236,10 +2293,6 @@ mod tests {
                PublicKey::from_secret_key(&secp_ctx, &recipient_privkey())
        }
 
-       fn sender_node_id() -> NodeId {
-               NodeId::from_pubkey(&sender_pubkey())
-       }
-
        fn recipient_node_id() -> NodeId {
                NodeId::from_pubkey(&recipient_pubkey())
        }
@@ -2256,7 +2309,7 @@ mod tests {
                network_graph: &mut NetworkGraph<&TestLogger>, short_channel_id: u64, node_1_key: SecretKey,
                node_2_key: SecretKey
        ) {
-               let genesis_hash = genesis_block(Network::Testnet).header.block_hash();
+               let genesis_hash = ChainHash::using_genesis_block(Network::Testnet);
                let node_1_secret = &SecretKey::from_slice(&[39; 32]).unwrap();
                let node_2_secret = &SecretKey::from_slice(&[40; 32]).unwrap();
                let secp_ctx = Secp256k1::new();
@@ -2289,7 +2342,7 @@ mod tests {
                network_graph: &mut NetworkGraph<&TestLogger>, short_channel_id: u64, node_key: SecretKey,
                flags: u8, htlc_maximum_msat: u64, timestamp: u32,
        ) {
-               let genesis_hash = genesis_block(Network::Testnet).header.block_hash();
+               let genesis_hash = ChainHash::using_genesis_block(Network::Testnet);
                let secp_ctx = Secp256k1::new();
                let unsigned_update = UnsignedChannelUpdate {
                        chain_hash: genesis_hash,
@@ -2338,18 +2391,21 @@ mod tests {
        fn liquidity_bounds_directed_from_lowest_node_id() {
                let logger = TestLogger::new();
                let last_updated = SinceEpoch::now();
+               let offset_history_last_updated = SinceEpoch::now();
                let network_graph = network_graph(&logger);
                let decay_params = ProbabilisticScoringDecayParameters::default();
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
                        .with_channel(42,
                                ChannelLiquidity {
-                                       min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100, last_updated,
+                                       min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100,
+                                       last_updated, offset_history_last_updated,
                                        min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                        max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                })
                        .with_channel(43,
                                ChannelLiquidity {
-                                       min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100, last_updated,
+                                       min_liquidity_offset_msat: 700, max_liquidity_offset_msat: 100,
+                                       last_updated, offset_history_last_updated,
                                        min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                        max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                });
@@ -2416,12 +2472,14 @@ mod tests {
        fn resets_liquidity_upper_bound_when_crossed_by_lower_bound() {
                let logger = TestLogger::new();
                let last_updated = SinceEpoch::now();
+               let offset_history_last_updated = SinceEpoch::now();
                let network_graph = network_graph(&logger);
                let decay_params = ProbabilisticScoringDecayParameters::default();
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
                        .with_channel(42,
                                ChannelLiquidity {
-                                       min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400, last_updated,
+                                       min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400,
+                                       last_updated, offset_history_last_updated,
                                        min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                        max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                });
@@ -2475,12 +2533,14 @@ mod tests {
        fn resets_liquidity_lower_bound_when_crossed_by_upper_bound() {
                let logger = TestLogger::new();
                let last_updated = SinceEpoch::now();
+               let offset_history_last_updated = SinceEpoch::now();
                let network_graph = network_graph(&logger);
                let decay_params = ProbabilisticScoringDecayParameters::default();
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
                        .with_channel(42,
                                ChannelLiquidity {
-                                       min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400, last_updated,
+                                       min_liquidity_offset_msat: 200, max_liquidity_offset_msat: 400,
+                                       last_updated, offset_history_last_updated,
                                        min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                        max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                });
@@ -2541,45 +2601,52 @@ mod tests {
                let decay_params = ProbabilisticScoringDecayParameters::default();
                let scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
 
                let usage = ChannelUsage {
                        amount_msat: 1_024,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               let network_graph = network_graph.read_only();
+               let channel = network_graph.channel(42).unwrap();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 10_240, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 102_400, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 47);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 47);
                let usage = ChannelUsage { amount_msat: 1_023_999, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2_000);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2_000);
 
                let usage = ChannelUsage {
                        amount_msat: 128,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 58);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 58);
                let usage = ChannelUsage { amount_msat: 256, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 125);
                let usage = ChannelUsage { amount_msat: 374, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 198);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 198);
                let usage = ChannelUsage { amount_msat: 512, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
                let usage = ChannelUsage { amount_msat: 640, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 425);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 425);
                let usage = ChannelUsage { amount_msat: 768, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 602);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 602);
                let usage = ChannelUsage { amount_msat: 896, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 902);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 902);
        }
 
        #[test]
        fn constant_penalty_outside_liquidity_bounds() {
                let logger = TestLogger::new();
                let last_updated = SinceEpoch::now();
+               let offset_history_last_updated = SinceEpoch::now();
                let network_graph = network_graph(&logger);
                let params = ProbabilisticScoringFeeParameters {
                        liquidity_penalty_multiplier_msat: 1_000,
@@ -2592,24 +2659,30 @@ mod tests {
                let scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger)
                        .with_channel(42,
                                ChannelLiquidity {
-                                       min_liquidity_offset_msat: 40, max_liquidity_offset_msat: 40, last_updated,
+                                       min_liquidity_offset_msat: 40, max_liquidity_offset_msat: 40,
+                                       last_updated, offset_history_last_updated,
                                        min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                        max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
                                });
                let source = source_node_id();
-               let target = target_node_id();
 
                let usage = ChannelUsage {
                        amount_msat: 39,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 100, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 50, ..usage };
-               assert_ne!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
-               assert_ne!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_ne!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
+               assert_ne!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
                let usage = ChannelUsage { amount_msat: 61, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
        }
 
        #[test]
@@ -2621,7 +2694,6 @@ mod tests {
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               let sender = sender_node_id();
                let source = source_node_id();
                let usage = ChannelUsage {
                        amount_msat: 500,
@@ -2630,14 +2702,20 @@ mod tests {
                };
                let failed_path = payment_path_for_amount(500);
                let successful_path = payment_path_for_amount(200);
+               let channel = &network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 41,
+               };
 
-               assert_eq!(scorer.channel_penalty_msat(41, &sender, &source, usage, &params), 301);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 301);
 
-               scorer.payment_path_failed(&failed_path, 41);
-               assert_eq!(scorer.channel_penalty_msat(41, &sender, &source, usage, &params), 301);
+               scorer.payment_path_failed(&failed_path, 41, Duration::ZERO);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 301);
 
-               scorer.payment_path_successful(&successful_path);
-               assert_eq!(scorer.channel_penalty_msat(41, &sender, &source, usage, &params), 301);
+               scorer.payment_path_successful(&successful_path, Duration::ZERO);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 301);
        }
 
        #[test]
@@ -2650,7 +2728,6 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let path = payment_path_for_amount(500);
 
                let usage = ChannelUsage {
@@ -2658,20 +2735,26 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 128);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
                let usage = ChannelUsage { amount_msat: 500, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 301);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 301);
                let usage = ChannelUsage { amount_msat: 750, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 602);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 602);
 
-               scorer.payment_path_failed(&path, 43);
+               scorer.payment_path_failed(&path, 43, Duration::ZERO);
 
                let usage = ChannelUsage { amount_msat: 250, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 500, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 750, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
        }
 
        #[test]
@@ -2685,7 +2768,6 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let path = payment_path_for_amount(500);
 
                let usage = ChannelUsage {
@@ -2693,20 +2775,26 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 128);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
                let usage = ChannelUsage { amount_msat: 500, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 301);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 301);
                let usage = ChannelUsage { amount_msat: 750, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 602);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 602);
 
-               scorer.payment_path_failed(&path, 42);
+               scorer.payment_path_failed(&path, 42, Duration::ZERO);
 
                let usage = ChannelUsage { amount_msat: 250, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
                let usage = ChannelUsage { amount_msat: 500, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
                let usage = ChannelUsage { amount_msat: 750, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
        }
 
        #[test]
@@ -2741,7 +2829,6 @@ mod tests {
                let node_a = NodeId::from_pubkey(&pub_a);
                let node_b = NodeId::from_pubkey(&pub_b);
                let node_c = NodeId::from_pubkey(&pub_c);
-               let node_d = NodeId::from_pubkey(&pub_d);
 
                let params = ProbabilisticScoringFeeParameters {
                        liquidity_penalty_multiplier_msat: 1_000,
@@ -2754,17 +2841,53 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &node_a, &node_b, usage, &params), 128);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&node_a).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
                // Note that a default liquidity bound is used for B -> C as no channel exists
-               assert_eq!(scorer.channel_penalty_msat(43, &node_b, &node_c, usage, &params), 128);
-               assert_eq!(scorer.channel_penalty_msat(44, &node_c, &node_d, usage, &params), 128);
-
-               scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 43);
-
-               assert_eq!(scorer.channel_penalty_msat(42, &node_a, &node_b, usage, &params), 80);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&node_b).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 43,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
+               let channel = network_graph.read_only().channel(44).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&node_c).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 44,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
+
+               scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 43, Duration::ZERO);
+
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&node_a).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 80);
                // Note that a default liquidity bound is used for B -> C as no channel exists
-               assert_eq!(scorer.channel_penalty_msat(43, &node_b, &node_c, usage, &params), 128);
-               assert_eq!(scorer.channel_penalty_msat(44, &node_c, &node_d, usage, &params), 128);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&node_b).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 43,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
+               let channel = network_graph.read_only().channel(44).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&node_c).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 44,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 128);
        }
 
        #[test]
@@ -2776,25 +2899,39 @@ mod tests {
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let mut scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               let sender = sender_node_id();
                let source = source_node_id();
-               let target = target_node_id();
-               let recipient = recipient_node_id();
                let usage = ChannelUsage {
                        amount_msat: 250,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
+               let network_graph = network_graph.read_only().channels().clone();
+               let channel_42 = network_graph.get(&42).unwrap();
+               let channel_43 = network_graph.get(&43).unwrap();
+               let (info, _) = channel_42.as_directed_from(&source).unwrap();
+               let candidate_41 = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 41,
+               };
+               let (info, target) = channel_42.as_directed_from(&source).unwrap();
+               let candidate_42 = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               let (info, _) = channel_43.as_directed_from(&target).unwrap();
+               let candidate_43 = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 43,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate_41, usage, &params), 128);
+               assert_eq!(scorer.channel_penalty_msat(&candidate_42, usage, &params), 128);
+               assert_eq!(scorer.channel_penalty_msat(&candidate_43, usage, &params), 128);
 
-               assert_eq!(scorer.channel_penalty_msat(41, &sender, &source, usage, &params), 128);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 128);
-               assert_eq!(scorer.channel_penalty_msat(43, &target, &recipient, usage, &params), 128);
-
-               scorer.payment_path_successful(&payment_path_for_amount(500));
+               scorer.payment_path_successful(&payment_path_for_amount(500), Duration::ZERO);
 
-               assert_eq!(scorer.channel_penalty_msat(41, &sender, &source, usage, &params), 128);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
-               assert_eq!(scorer.channel_penalty_msat(43, &target, &recipient, usage, &params), 300);
+               assert_eq!(scorer.channel_penalty_msat(&candidate_41, usage, &params), 128);
+               assert_eq!(scorer.channel_penalty_msat(&candidate_42, usage, &params), 300);
+               assert_eq!(scorer.channel_penalty_msat(&candidate_43, usage, &params), 300);
        }
 
        #[test]
@@ -2812,86 +2949,91 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
 
                let usage = ChannelUsage {
                        amount_msat: 0,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 1_023, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2_000);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2_000);
 
-               scorer.payment_path_failed(&payment_path_for_amount(768), 42);
-               scorer.payment_path_failed(&payment_path_for_amount(128), 43);
+               scorer.payment_path_failed(&payment_path_for_amount(768), 42, Duration::ZERO);
+               scorer.payment_path_failed(&payment_path_for_amount(128), 43, Duration::ZERO);
 
                // Initial penalties
                let usage = ChannelUsage { amount_msat: 128, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 256, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 93);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 93);
                let usage = ChannelUsage { amount_msat: 768, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1_479);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 1_479);
                let usage = ChannelUsage { amount_msat: 896, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                // No decay
                SinceEpoch::advance(Duration::from_secs(4));
                let usage = ChannelUsage { amount_msat: 128, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 256, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 93);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 93);
                let usage = ChannelUsage { amount_msat: 768, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1_479);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 1_479);
                let usage = ChannelUsage { amount_msat: 896, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                // Half decay (i.e., three-quarter life)
                SinceEpoch::advance(Duration::from_secs(1));
                let usage = ChannelUsage { amount_msat: 128, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 22);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 22);
                let usage = ChannelUsage { amount_msat: 256, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 106);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 106);
                let usage = ChannelUsage { amount_msat: 768, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 921);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 921);
                let usage = ChannelUsage { amount_msat: 896, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                // One decay (i.e., half life)
                SinceEpoch::advance(Duration::from_secs(5));
                let usage = ChannelUsage { amount_msat: 64, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 128, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 34);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 34);
                let usage = ChannelUsage { amount_msat: 896, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1_970);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 1_970);
                let usage = ChannelUsage { amount_msat: 960, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                // Fully decay liquidity lower bound.
                SinceEpoch::advance(Duration::from_secs(10 * 7));
                let usage = ChannelUsage { amount_msat: 0, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 1, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 1_023, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2_000);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2_000);
                let usage = ChannelUsage { amount_msat: 1_024, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                // Fully decay liquidity upper bound.
                SinceEpoch::advance(Duration::from_secs(10));
                let usage = ChannelUsage { amount_msat: 0, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 1_024, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                SinceEpoch::advance(Duration::from_secs(10));
                let usage = ChannelUsage { amount_msat: 0, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
                let usage = ChannelUsage { amount_msat: 1_024, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
        }
 
        #[test]
@@ -2908,24 +3050,29 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 256,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 125);
 
-               scorer.payment_path_failed(&payment_path_for_amount(512), 42);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 281);
+               scorer.payment_path_failed(&payment_path_for_amount(512), 42, Duration::ZERO);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 281);
 
                // An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
                // would cause an overflow.
                SinceEpoch::advance(Duration::from_secs(10 * 64));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 125);
 
                SinceEpoch::advance(Duration::from_secs(10));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 125);
        }
 
        #[test]
@@ -2942,37 +3089,42 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 512,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
                };
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
 
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
 
                // More knowledge gives higher confidence (256, 768), meaning a lower penalty.
-               scorer.payment_path_failed(&payment_path_for_amount(768), 42);
-               scorer.payment_path_failed(&payment_path_for_amount(256), 43);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 281);
+               scorer.payment_path_failed(&payment_path_for_amount(768), 42, Duration::ZERO);
+               scorer.payment_path_failed(&payment_path_for_amount(256), 43, Duration::ZERO);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 281);
 
                // Decaying knowledge gives less confidence (128, 896), meaning a higher penalty.
                SinceEpoch::advance(Duration::from_secs(10));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 291);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 291);
 
                // Reducing the upper bound gives more confidence (128, 832) that the payment amount (512)
                // is closer to the upper bound, meaning a higher penalty.
-               scorer.payment_path_successful(&payment_path_for_amount(64));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 331);
+               scorer.payment_path_successful(&payment_path_for_amount(64), Duration::from_secs(10));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 331);
 
                // Increasing the lower bound gives more confidence (256, 832) that the payment amount (512)
                // is closer to the lower bound, meaning a lower penalty.
-               scorer.payment_path_failed(&payment_path_for_amount(256), 43);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 245);
+               scorer.payment_path_failed(&payment_path_for_amount(256), 43, Duration::from_secs(10));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 245);
 
                // Further decaying affects the lower bound more than the upper bound (128, 928).
                SinceEpoch::advance(Duration::from_secs(10));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 280);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 280);
        }
 
        #[test]
@@ -2990,21 +3142,26 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 500,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
 
-               scorer.payment_path_failed(&payment_path_for_amount(500), 42);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                SinceEpoch::advance(Duration::from_secs(10));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 473);
 
-               scorer.payment_path_failed(&payment_path_for_amount(250), 43);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
 
                let mut serialized_scorer = Vec::new();
                scorer.write(&mut serialized_scorer).unwrap();
@@ -3012,7 +3169,7 @@ mod tests {
                let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
                let deserialized_scorer =
                        <ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
-               assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, &params), 300);
        }
 
        #[test]
@@ -3030,15 +3187,20 @@ mod tests {
                };
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 500,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
 
-               scorer.payment_path_failed(&payment_path_for_amount(500), 42);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                let mut serialized_scorer = Vec::new();
                scorer.write(&mut serialized_scorer).unwrap();
@@ -3048,13 +3210,13 @@ mod tests {
                let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
                let deserialized_scorer =
                        <ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
-               assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
+               assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, &params), 473);
 
-               scorer.payment_path_failed(&payment_path_for_amount(250), 43);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
 
                SinceEpoch::advance(Duration::from_secs(10));
-               assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 370);
+               assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, &params), 370);
        }
 
        #[test]
@@ -3066,54 +3228,59 @@ mod tests {
                let params = ProbabilisticScoringFeeParameters::default();
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
 
                let usage = ChannelUsage {
                        amount_msat: 100_000_000,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 950_000_000, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 11497);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 11497);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 7408);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 7408);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 2_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 6151);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 6151);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 3_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 5427);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 5427);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 4_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4955);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4955);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 5_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4736);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4736);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 6_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4484);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4484);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_450_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4484);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4484);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4263);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4263);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 8_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4263);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4263);
                let usage = ChannelUsage {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 9_950_000_000, htlc_maximum_msat: 1_000 }, ..usage
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 4044);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 4044);
        }
 
        #[test]
@@ -3121,7 +3288,6 @@ mod tests {
                let logger = TestLogger::new();
                let network_graph = network_graph(&logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 128,
                        inflight_htlc_msat: 0,
@@ -3133,14 +3299,20 @@ mod tests {
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 58);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 58);
 
                let params = ProbabilisticScoringFeeParameters {
                        base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
                        anti_probing_penalty_msat: 0, ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 558);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 558);
 
                let params = ProbabilisticScoringFeeParameters {
                        base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
@@ -3149,7 +3321,7 @@ mod tests {
                };
 
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 558 + 128);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 558 + 128);
        }
 
        #[test]
@@ -3157,7 +3329,6 @@ mod tests {
                let logger = TestLogger::new();
                let network_graph = network_graph(&logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 512_000,
                        inflight_htlc_msat: 0,
@@ -3170,7 +3341,13 @@ mod tests {
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
 
                let params = ProbabilisticScoringFeeParameters {
                        liquidity_penalty_multiplier_msat: 1_000,
@@ -3178,7 +3355,7 @@ mod tests {
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 337);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 337);
        }
 
        #[test]
@@ -3186,20 +3363,24 @@ mod tests {
                let logger = TestLogger::new();
                let network_graph = network_graph(&logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: u64::max_value(),
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Infinite,
                };
-
                let params = ProbabilisticScoringFeeParameters {
                        liquidity_penalty_multiplier_msat: 40_000,
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
                };
                let decay_params = ProbabilisticScoringDecayParameters::zero_penalty();
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
                let scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 80_000);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 80_000);
        }
 
        #[test]
@@ -3212,17 +3393,23 @@ mod tests {
                };
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
 
                let usage = ChannelUsage {
                        amount_msat: 750,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 },
                };
-               assert_ne!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               let network_graph = network_graph.read_only();
+               let channel = network_graph.channel(42).unwrap();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_ne!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
 
                let usage = ChannelUsage { inflight_htlc_msat: 251, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
        }
 
        #[test]
@@ -3232,7 +3419,6 @@ mod tests {
                let params = ProbabilisticScoringFeeParameters::default();
                let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
 
                let base_penalty_msat = params.base_penalty_msat;
                let usage = ChannelUsage {
@@ -3240,13 +3426,20 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::ExactLiquidity { liquidity_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), base_penalty_msat);
+               let network_graph = network_graph.read_only();
+               let channel = network_graph.channel(42).unwrap();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), base_penalty_msat);
 
                let usage = ChannelUsage { amount_msat: 1_000, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), base_penalty_msat);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), base_penalty_msat);
 
                let usage = ChannelUsage { amount_msat: 1_001, ..usage };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
        }
 
        #[test]
@@ -3277,16 +3470,36 @@ mod tests {
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
                };
 
-               // With no historical data the normal liquidity penalty calculation is used.
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 168);
+               {
+                       let network_graph = network_graph.read_only();
+                       let channel = network_graph.channel(42).unwrap();
+                       let (info, _) = channel.as_directed_from(&source).unwrap();
+                       let candidate = CandidateRouteHop::PublicHop {
+                               info,
+                               short_channel_id: 42,
+                       };
+
+                       // With no historical data the normal liquidity penalty calculation is used.
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 168);
+               }
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
-                       None);
+               None);
                assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 42, &params),
-                       None);
+               None);
+
+               scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::ZERO);
+               {
+                       let network_graph = network_graph.read_only();
+                       let channel = network_graph.channel(42).unwrap();
+                       let (info, _) = channel.as_directed_from(&source).unwrap();
+                       let candidate = CandidateRouteHop::PublicHop {
+                               info,
+                               short_channel_id: 42,
+                       };
 
-               scorer.payment_path_failed(&payment_path_for_amount(1), 42);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2048);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage_1, &params), 249);
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2048);
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage_1, &params), 249);
+               }
                // The "it failed" increment is 32, where the probability should lie several buckets into
                // the first octile.
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
@@ -3299,8 +3512,18 @@ mod tests {
 
                // Even after we tell the scorer we definitely have enough available liquidity, it will
                // still remember that there was some failure in the past, and assign a non-0 penalty.
-               scorer.payment_path_failed(&payment_path_for_amount(1000), 43);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 105);
+               scorer.payment_path_failed(&payment_path_for_amount(1000), 43, Duration::ZERO);
+               {
+                       let network_graph = network_graph.read_only();
+                       let channel = network_graph.channel(42).unwrap();
+                       let (info, _) = channel.as_directed_from(&source).unwrap();
+                       let candidate = CandidateRouteHop::PublicHop {
+                               info,
+                               short_channel_id: 42,
+                       };
+
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 105);
+               }
                // The first points should be decayed just slightly and the last bucket has a new point.
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
                        Some(([31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0],
@@ -3320,7 +3543,17 @@ mod tests {
                // Advance the time forward 16 half-lives (which the docs claim will ensure all data is
                // gone), and check that we're back to where we started.
                SinceEpoch::advance(Duration::from_secs(10 * 16));
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 168);
+               {
+                       let network_graph = network_graph.read_only();
+                       let channel = network_graph.channel(42).unwrap();
+                       let (info, _) = channel.as_directed_from(&source).unwrap();
+                       let candidate = CandidateRouteHop::PublicHop {
+                               info,
+                               short_channel_id: 42,
+                       };
+
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 168);
+               }
                // Once fully decayed we still have data, but its all-0s. In the future we may remove the
                // data entirely instead.
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
@@ -3332,17 +3565,27 @@ mod tests {
                        inflight_htlc_msat: 1024,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
                };
-               scorer.payment_path_failed(&payment_path_for_amount(1), 42);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2050);
-               usage.inflight_htlc_msat = 0;
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 866);
+               scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::from_secs(10 * 16));
+               {
+                       let network_graph = network_graph.read_only();
+                       let channel = network_graph.channel(42).unwrap();
+                       let (info, _) = channel.as_directed_from(&source).unwrap();
+                       let candidate = CandidateRouteHop::PublicHop {
+                               info,
+                               short_channel_id: 42,
+                       };
 
-               let usage = ChannelUsage {
-                       amount_msat: 1,
-                       inflight_htlc_msat: 0,
-                       effective_capacity: EffectiveCapacity::AdvertisedMaxHTLC { amount_msat: 0 },
-               };
-               assert_eq!(scorer.channel_penalty_msat(42, &target, &source, usage, &params), 2048);
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2050);
+                       usage.inflight_htlc_msat = 0;
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 866);
+
+                       let usage = ChannelUsage {
+                               amount_msat: 1,
+                               inflight_htlc_msat: 0,
+                               effective_capacity: EffectiveCapacity::AdvertisedMaxHTLC { amount_msat: 0 },
+                       };
+                       assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2048);
+               }
 
                // Advance to decay all liquidity offsets to zero.
                SinceEpoch::advance(Duration::from_secs(60 * 60 * 10));
@@ -3354,7 +3597,7 @@ mod tests {
                        path_hop(source_pubkey(), 42, 1),
                        path_hop(sender_pubkey(), 41, 0),
                ];
-               scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42);
+               scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42, Duration::from_secs(10 * (16 + 60 * 60)));
        }
 
        #[test]
@@ -3362,7 +3605,6 @@ mod tests {
                let logger = TestLogger::new();
                let network_graph = network_graph(&logger);
                let source = source_node_id();
-               let target = target_node_id();
                let params = ProbabilisticScoringFeeParameters {
                        anti_probing_penalty_msat: 500,
                        ..ProbabilisticScoringFeeParameters::zero_penalty()
@@ -3375,7 +3617,14 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               let network_graph = network_graph.read_only();
+               let channel = network_graph.channel(42).unwrap();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
 
                // Check we receive anti-probing penalty for htlc_maximum_msat == channel_capacity.
                let usage = ChannelUsage {
@@ -3383,7 +3632,7 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_024_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 500);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 500);
 
                // Check we receive anti-probing penalty for htlc_maximum_msat == channel_capacity/2.
                let usage = ChannelUsage {
@@ -3391,7 +3640,7 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 512_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 500);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 500);
 
                // Check we receive no anti-probing penalty for htlc_maximum_msat == channel_capacity/2 - 1.
                let usage = ChannelUsage {
@@ -3399,7 +3648,7 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 511_999 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
        }
 
        #[test]
@@ -3414,13 +3663,18 @@ mod tests {
                let decay_params = ProbabilisticScoringDecayParameters::default();
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
                let usage = ChannelUsage {
                        amount_msat: 512,
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
                };
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, target) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
 
                let mut path = payment_path_for_amount(768);
                let recipient_hop = path.hops.pop().unwrap();
@@ -3442,9 +3696,9 @@ mod tests {
                // final value is taken into account.
                assert!(scorer.channel_liquidities.get(&42).is_none());
 
-               scorer.payment_path_failed(&path, 42);
+               scorer.payment_path_failed(&path, 42, Duration::ZERO);
                path.blinded_tail.as_mut().unwrap().final_value_msat = 256;
-               scorer.payment_path_failed(&path, 43);
+               scorer.payment_path_failed(&path, 43, Duration::ZERO);
 
                let liquidity = scorer.channel_liquidities.get(&42).unwrap()
                        .as_directed(&source, &target, 1_000, decay_params);
@@ -3476,7 +3730,6 @@ mod tests {
 
                let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
                let source = source_node_id();
-               let target = target_node_id();
 
                let mut amount_msat = 10_000_000;
                let usage = ChannelUsage {
@@ -3484,19 +3737,25 @@ mod tests {
                        inflight_htlc_msat: 0,
                        effective_capacity: EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat: capacity_msat },
                };
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, target) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop {
+                       info,
+                       short_channel_id: 42,
+               };
                // With no historical data the normal liquidity penalty calculation is used, which results
                // in a success probability of ~75%.
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1269);
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 1269);
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
                        None);
                assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 42, &params),
                        None);
 
                // Fail to pay once, and then check the buckets and penalty.
-               scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42);
+               scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42, Duration::ZERO);
                // The penalty should be the maximum penalty, as the payment we're scoring is now in the
                // same bucket which is the only maximum datapoint.
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params),
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params),
                        2048 + 2048 * amount_msat / super::AMOUNT_PENALTY_DIVISOR);
                // The "it failed" increment is 32, which we should apply to the first upper-bound (between
                // 6k sats and 12k sats).
@@ -3517,7 +3776,7 @@ mod tests {
                // ...but once we see a failure, we consider the payment to be substantially less likely,
                // even though not a probability of zero as we still look at the second max bucket which
                // now shows 31.
-               scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42);
+               scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42, Duration::ZERO);
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
                        Some(([63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                                [32, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));