Skip to content

Commit 5ef158d

Browse files
committed
Stop decaying historical liquidity information during scoring
Because scoring is an incredibly performance-sensitive operation, doing liquidity information decay (and especially fetching the current time!) during scoring isn't really a great idea. Now that we decay liquidity information in the background, we don't have any reason to decay during scoring, and we remove the historical bucket liquidity decaying here.
1 parent 19b443f commit 5ef158d

File tree

1 file changed

+61
-78
lines changed

1 file changed

+61
-78
lines changed

lightning/src/routing/scoring.rs

Lines changed: 61 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -851,8 +851,6 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
851851
/// Note that this writes roughly one line per channel for which we have a liquidity estimate,
852852
/// which may be a substantial amount of log output.
853853
pub fn debug_log_liquidity_stats(&self) {
854-
let now = T::now();
855-
856854
let graph = self.network_graph.read_only();
857855
for (scid, liq) in self.channel_liquidities.iter() {
858856
if let Some(chan_debug) = graph.channels().get(scid) {
@@ -861,10 +859,8 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
861859
let amt = directed_info.effective_capacity().as_msat();
862860
let dir_liq = liq.as_directed(source, target, amt, self.decay_params);
863861

864-
let (min_buckets, max_buckets) = dir_liq.liquidity_history
865-
.get_decayed_buckets(now, *dir_liq.offset_history_last_updated,
866-
self.decay_params.historical_no_updates_half_life)
867-
.unwrap_or(([0; 32], [0; 32]));
862+
let min_buckets = &dir_liq.liquidity_history.min_liquidity_offset_history.buckets;
863+
let max_buckets = &dir_liq.liquidity_history.max_liquidity_offset_history.buckets;
868864

869865
log_debug!(self.logger, core::concat!(
870866
"Liquidity from {} to {} via {} is in the range ({}, {}).\n",
@@ -943,7 +939,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
943939
/// in the top and bottom bucket, and roughly with similar (recent) frequency.
944940
///
945941
/// Because the datapoints are decayed slowly over time, values will eventually return to
946-
/// `Some(([1; 32], [1; 32]))` and then to `None` once no datapoints remain.
942+
/// `Some(([0; 32], [0; 32]))` or `None` if no data remains for a channel.
947943
///
948944
/// In order to fetch a single success probability from the buckets provided here, as used in
949945
/// the scoring model, see [`Self::historical_estimated_payment_success_probability`].
@@ -957,11 +953,8 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
957953
let amt = directed_info.effective_capacity().as_msat();
958954
let dir_liq = liq.as_directed(source, target, amt, self.decay_params);
959955

960-
let (min_buckets, mut max_buckets) =
961-
dir_liq.liquidity_history.get_decayed_buckets(
962-
dir_liq.now, *dir_liq.offset_history_last_updated,
963-
self.decay_params.historical_no_updates_half_life
964-
)?;
956+
let min_buckets = dir_liq.liquidity_history.min_liquidity_offset_history.buckets;
957+
let mut max_buckets = dir_liq.liquidity_history.max_liquidity_offset_history.buckets;
965958

966959
// Note that the liquidity buckets are an offset from the edge, so we inverse
967960
// the max order to get the probabilities from zero.
@@ -992,9 +985,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
992985
let dir_liq = liq.as_directed(source, target, capacity_msat, self.decay_params);
993986

994987
return dir_liq.liquidity_history.calculate_success_probability_times_billion(
995-
dir_liq.now, *dir_liq.offset_history_last_updated,
996-
self.decay_params.historical_no_updates_half_life, &params, amount_msat,
997-
capacity_msat
988+
&params, amount_msat, capacity_msat
998989
).map(|p| p as f64 / (1024 * 1024 * 1024) as f64);
999990
}
1000991
}
@@ -1215,9 +1206,7 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
12151206
score_params.historical_liquidity_penalty_amount_multiplier_msat != 0 {
12161207
if let Some(cumulative_success_prob_times_billion) = self.liquidity_history
12171208
.calculate_success_probability_times_billion(
1218-
self.now, *self.offset_history_last_updated,
1219-
self.decay_params.historical_no_updates_half_life, score_params, amount_msat,
1220-
self.capacity_msat)
1209+
score_params, amount_msat, self.capacity_msat)
12211210
{
12221211
let historical_negative_log10_times_2048 = approx::negative_log10_times_2048(cumulative_success_prob_times_billion + 1, 1024 * 1024 * 1024);
12231212
res = res.saturating_add(Self::combined_penalty_msat(amount_msat,
@@ -2009,22 +1998,20 @@ mod bucketed_history {
20091998
}
20101999

20112000
impl<D: Deref<Target = HistoricalBucketRangeTracker>> HistoricalMinMaxBuckets<D> {
2012-
pub(super) fn get_decayed_buckets<T: Time>(&self, now: T, offset_history_last_updated: T, half_life: Duration)
2013-
-> Option<([u16; 32], [u16; 32])> {
2014-
let (_, required_decays) = self.get_total_valid_points(now, offset_history_last_updated, half_life)?;
2015-
2016-
let mut min_buckets = *self.min_liquidity_offset_history;
2017-
min_buckets.time_decay_data(required_decays);
2018-
let mut max_buckets = *self.max_liquidity_offset_history;
2019-
max_buckets.time_decay_data(required_decays);
2020-
Some((min_buckets.buckets, max_buckets.buckets))
2021-
}
20222001
#[inline]
2023-
pub(super) fn get_total_valid_points<T: Time>(&self, now: T, offset_history_last_updated: T, half_life: Duration)
2024-
-> Option<(u64, u32)> {
2025-
let required_decays = now.duration_since(offset_history_last_updated).as_secs()
2026-
.checked_div(half_life.as_secs())
2027-
.map_or(u32::max_value(), |decays| cmp::min(decays, u32::max_value() as u64) as u32);
2002+
pub(super) fn calculate_success_probability_times_billion(
2003+
&self, params: &ProbabilisticScoringFeeParameters, amount_msat: u64,
2004+
capacity_msat: u64
2005+
) -> Option<u64> {
2006+
// If historical penalties are enabled, we try to calculate a probability of success
2007+
// given our historical distribution of min- and max-liquidity bounds in a channel.
2008+
// To do so, we walk the set of historical liquidity bucket (min, max) combinations
2009+
// (where min_idx < max_idx, as having a minimum above our maximum is an invalid
2010+
// state). For each pair, we calculate the probability as if the bucket's corresponding
2011+
// min- and max- liquidity bounds were our current liquidity bounds and then multiply
2012+
// that probability by the weight of the selected buckets.
2013+
let payment_pos = amount_to_pos(amount_msat, capacity_msat);
2014+
if payment_pos >= POSITION_TICKS { return None; }
20282015

20292016
let mut total_valid_points_tracked = 0;
20302017
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
@@ -2036,33 +2023,10 @@ mod bucketed_history {
20362023
// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
20372024
// treat it as if we were fully decayed.
20382025
const FULLY_DECAYED: u16 = BUCKET_FIXED_POINT_ONE * BUCKET_FIXED_POINT_ONE;
2039-
if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < FULLY_DECAYED.into() {
2026+
if total_valid_points_tracked < FULLY_DECAYED.into() {
20402027
return None;
20412028
}
20422029

2043-
Some((total_valid_points_tracked, required_decays))
2044-
}
2045-
2046-
#[inline]
2047-
pub(super) fn calculate_success_probability_times_billion<T: Time>(
2048-
&self, now: T, offset_history_last_updated: T, half_life: Duration,
2049-
params: &ProbabilisticScoringFeeParameters, amount_msat: u64, capacity_msat: u64
2050-
) -> Option<u64> {
2051-
// If historical penalties are enabled, we try to calculate a probability of success
2052-
// given our historical distribution of min- and max-liquidity bounds in a channel.
2053-
// To do so, we walk the set of historical liquidity bucket (min, max) combinations
2054-
// (where min_idx < max_idx, as having a minimum above our maximum is an invalid
2055-
// state). For each pair, we calculate the probability as if the bucket's corresponding
2056-
// min- and max- liquidity bounds were our current liquidity bounds and then multiply
2057-
// that probability by the weight of the selected buckets.
2058-
let payment_pos = amount_to_pos(amount_msat, capacity_msat);
2059-
if payment_pos >= POSITION_TICKS { return None; }
2060-
2061-
// Check if all our buckets are zero, once decayed and treat it as if we had no data. We
2062-
// don't actually use the decayed buckets, though, as that would lose precision.
2063-
let (total_valid_points_tracked, _)
2064-
= self.get_total_valid_points(now, offset_history_last_updated, half_life)?;
2065-
20662030
let mut cumulative_success_prob_times_billion = 0;
20672031
// Special-case the 0th min bucket - it generally means we failed a payment, so only
20682032
// consider the highest (i.e. largest-offset-from-max-capacity) max bucket for all
@@ -2918,19 +2882,9 @@ mod tests {
29182882
let usage = ChannelUsage { amount_msat: 896, ..usage };
29192883
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
29202884

2921-
// No decay
2922-
SinceEpoch::advance(Duration::from_secs(4));
2923-
let usage = ChannelUsage { amount_msat: 128, ..usage };
2924-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
2925-
let usage = ChannelUsage { amount_msat: 256, ..usage };
2926-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 93);
2927-
let usage = ChannelUsage { amount_msat: 768, ..usage };
2928-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1_479);
2929-
let usage = ChannelUsage { amount_msat: 896, ..usage };
2930-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
2931-
29322885
// Half decay (i.e., three-quarter life)
2933-
SinceEpoch::advance(Duration::from_secs(1));
2886+
SinceEpoch::advance(Duration::from_secs(5));
2887+
scorer.decay_liquidity_certainty(Duration::from_secs(5));
29342888
let usage = ChannelUsage { amount_msat: 128, ..usage };
29352889
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 22);
29362890
let usage = ChannelUsage { amount_msat: 256, ..usage };
@@ -2942,6 +2896,7 @@ mod tests {
29422896

29432897
// One decay (i.e., half life)
29442898
SinceEpoch::advance(Duration::from_secs(5));
2899+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
29452900
let usage = ChannelUsage { amount_msat: 64, ..usage };
29462901
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29472902
let usage = ChannelUsage { amount_msat: 128, ..usage };
@@ -2953,6 +2908,7 @@ mod tests {
29532908

29542909
// Fully decay liquidity lower bound.
29552910
SinceEpoch::advance(Duration::from_secs(10 * 7));
2911+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 8));
29562912
let usage = ChannelUsage { amount_msat: 0, ..usage };
29572913
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29582914
let usage = ChannelUsage { amount_msat: 1, ..usage };
@@ -2964,12 +2920,14 @@ mod tests {
29642920

29652921
// Fully decay liquidity upper bound.
29662922
SinceEpoch::advance(Duration::from_secs(10));
2923+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 9));
29672924
let usage = ChannelUsage { amount_msat: 0, ..usage };
29682925
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29692926
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
29702927
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
29712928

29722929
SinceEpoch::advance(Duration::from_secs(10));
2930+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 10));
29732931
let usage = ChannelUsage { amount_msat: 0, ..usage };
29742932
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29752933
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
@@ -3004,9 +2962,11 @@ mod tests {
30042962
// An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
30052963
// would cause an overflow.
30062964
SinceEpoch::advance(Duration::from_secs(10 * 64));
2965+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 64));
30072966
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30082967

30092968
SinceEpoch::advance(Duration::from_secs(10));
2969+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 65));
30102970
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30112971
}
30122972

@@ -3040,6 +3000,7 @@ mod tests {
30403000

30413001
// Decaying knowledge gives less confidence (128, 896), meaning a higher penalty.
30423002
SinceEpoch::advance(Duration::from_secs(10));
3003+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
30433004
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 291);
30443005

30453006
// Reducing the upper bound gives more confidence (128, 832) that the payment amount (512)
@@ -3054,6 +3015,7 @@ mod tests {
30543015

30553016
// Further decaying affects the lower bound more than the upper bound (128, 928).
30563017
SinceEpoch::advance(Duration::from_secs(10));
3018+
scorer.decay_liquidity_certainty(Duration::from_secs(20));
30573019
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 280);
30583020
}
30593021

@@ -3083,6 +3045,7 @@ mod tests {
30833045
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
30843046

30853047
SinceEpoch::advance(Duration::from_secs(10));
3048+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
30863049
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
30873050

30883051
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
@@ -3097,8 +3060,7 @@ mod tests {
30973060
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
30983061
}
30993062

3100-
#[test]
3101-
fn decays_persisted_liquidity_bounds() {
3063+
fn do_decays_persisted_liquidity_bounds(decay_before_reload: bool) {
31023064
let logger = TestLogger::new();
31033065
let network_graph = network_graph(&logger);
31043066
let params = ProbabilisticScoringFeeParameters {
@@ -3122,23 +3084,38 @@ mod tests {
31223084
scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
31233085
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
31243086

3087+
if decay_before_reload {
3088+
SinceEpoch::advance(Duration::from_secs(10));
3089+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3090+
}
3091+
31253092
let mut serialized_scorer = Vec::new();
31263093
scorer.write(&mut serialized_scorer).unwrap();
31273094

3128-
SinceEpoch::advance(Duration::from_secs(10));
3129-
31303095
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
3131-
let deserialized_scorer =
3096+
let mut deserialized_scorer =
31323097
<ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
3098+
if !decay_before_reload {
3099+
SinceEpoch::advance(Duration::from_secs(10));
3100+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3101+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(10));
3102+
}
31333103
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
31343104

31353105
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
31363106
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
31373107

31383108
SinceEpoch::advance(Duration::from_secs(10));
3109+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(20));
31393110
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 370);
31403111
}
31413112

3113+
#[test]
3114+
fn decays_persisted_liquidity_bounds() {
3115+
do_decays_persisted_liquidity_bounds(false);
3116+
do_decays_persisted_liquidity_bounds(true);
3117+
}
3118+
31423119
#[test]
31433120
fn scores_realistic_payments() {
31443121
// Shows the scores of "realistic" sends of 100k sats over channels of 1-10m sats (with a
@@ -3402,11 +3379,11 @@ mod tests {
34023379
// Advance the time forward 16 half-lives (which the docs claim will ensure all data is
34033380
// gone), and check that we're back to where we started.
34043381
SinceEpoch::advance(Duration::from_secs(10 * 16));
3382+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 16));
34053383
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 168);
3406-
// Once fully decayed we still have data, but its all-0s. In the future we may remove the
3407-
// data entirely instead.
3384+
// Once fully decayed we still have data, so we get 0s for the buckets.
34083385
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
3409-
None);
3386+
Some(([0; 32], [0; 32])));
34103387
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1, &params), None);
34113388

34123389
let mut usage = ChannelUsage {
@@ -3417,7 +3394,7 @@ mod tests {
34173394
scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::from_secs(10 * 16));
34183395
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2050);
34193396
usage.inflight_htlc_msat = 0;
3420-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 866);
3397+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2048);
34213398

34223399
let usage = ChannelUsage {
34233400
amount_msat: 1,
@@ -3428,6 +3405,12 @@ mod tests {
34283405

34293406
// Advance to decay all liquidity offsets to zero.
34303407
SinceEpoch::advance(Duration::from_secs(60 * 60 * 10));
3408+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * (16 + 60 * 60)));
3409+
3410+
// Once even the bounds have decayed information about the channel should be removed
3411+
// entirely.
3412+
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
3413+
None);
34313414

34323415
// Use a path in the opposite direction, which have zero for htlc_maximum_msat. This will
34333416
// ensure that the effective capacity is zero to test division-by-zero edge cases.

0 commit comments

Comments
 (0)