Skip to content

Commit b3b3360

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 087755d commit b3b3360

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,
@@ -2021,22 +2010,20 @@ mod bucketed_history {
20212010
}
20222011

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

20412028
let mut total_valid_points_tracked = 0;
20422029
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
@@ -2048,33 +2035,10 @@ mod bucketed_history {
20482035
// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
20492036
// treat it as if we were fully decayed.
20502037
const FULLY_DECAYED: u16 = BUCKET_FIXED_POINT_ONE * BUCKET_FIXED_POINT_ONE;
2051-
if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < FULLY_DECAYED.into() {
2038+
if total_valid_points_tracked < FULLY_DECAYED.into() {
20522039
return None;
20532040
}
20542041

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

2933-
// No decay
2934-
SinceEpoch::advance(Duration::from_secs(4));
2935-
let usage = ChannelUsage { amount_msat: 128, ..usage };
2936-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
2937-
let usage = ChannelUsage { amount_msat: 256, ..usage };
2938-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 93);
2939-
let usage = ChannelUsage { amount_msat: 768, ..usage };
2940-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1_479);
2941-
let usage = ChannelUsage { amount_msat: 896, ..usage };
2942-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
2943-
29442897
// Half decay (i.e., three-quarter life)
2945-
SinceEpoch::advance(Duration::from_secs(1));
2898+
SinceEpoch::advance(Duration::from_secs(5));
2899+
scorer.decay_liquidity_certainty(Duration::from_secs(5));
29462900
let usage = ChannelUsage { amount_msat: 128, ..usage };
29472901
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 22);
29482902
let usage = ChannelUsage { amount_msat: 256, ..usage };
@@ -2954,6 +2908,7 @@ mod tests {
29542908

29552909
// One decay (i.e., half life)
29562910
SinceEpoch::advance(Duration::from_secs(5));
2911+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
29572912
let usage = ChannelUsage { amount_msat: 64, ..usage };
29582913
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29592914
let usage = ChannelUsage { amount_msat: 128, ..usage };
@@ -2965,6 +2920,7 @@ mod tests {
29652920

29662921
// Fully decay liquidity lower bound.
29672922
SinceEpoch::advance(Duration::from_secs(10 * 7));
2923+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 8));
29682924
let usage = ChannelUsage { amount_msat: 0, ..usage };
29692925
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29702926
let usage = ChannelUsage { amount_msat: 1, ..usage };
@@ -2976,12 +2932,14 @@ mod tests {
29762932

29772933
// Fully decay liquidity upper bound.
29782934
SinceEpoch::advance(Duration::from_secs(10));
2935+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 9));
29792936
let usage = ChannelUsage { amount_msat: 0, ..usage };
29802937
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29812938
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
29822939
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
29832940

29842941
SinceEpoch::advance(Duration::from_secs(10));
2942+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 10));
29852943
let usage = ChannelUsage { amount_msat: 0, ..usage };
29862944
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29872945
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
@@ -3016,9 +2974,11 @@ mod tests {
30162974
// An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
30172975
// would cause an overflow.
30182976
SinceEpoch::advance(Duration::from_secs(10 * 64));
2977+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 64));
30192978
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30202979

30212980
SinceEpoch::advance(Duration::from_secs(10));
2981+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 65));
30222982
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30232983
}
30242984

@@ -3052,6 +3012,7 @@ mod tests {
30523012

30533013
// Decaying knowledge gives less confidence (128, 896), meaning a higher penalty.
30543014
SinceEpoch::advance(Duration::from_secs(10));
3015+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
30553016
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 291);
30563017

30573018
// Reducing the upper bound gives more confidence (128, 832) that the payment amount (512)
@@ -3066,6 +3027,7 @@ mod tests {
30663027

30673028
// Further decaying affects the lower bound more than the upper bound (128, 928).
30683029
SinceEpoch::advance(Duration::from_secs(10));
3030+
scorer.decay_liquidity_certainty(Duration::from_secs(20));
30693031
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 280);
30703032
}
30713033

@@ -3095,6 +3057,7 @@ mod tests {
30953057
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
30963058

30973059
SinceEpoch::advance(Duration::from_secs(10));
3060+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
30983061
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
30993062

31003063
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
@@ -3109,8 +3072,7 @@ mod tests {
31093072
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
31103073
}
31113074

3112-
#[test]
3113-
fn decays_persisted_liquidity_bounds() {
3075+
fn do_decays_persisted_liquidity_bounds(decay_before_reload: bool) {
31143076
let logger = TestLogger::new();
31153077
let network_graph = network_graph(&logger);
31163078
let params = ProbabilisticScoringFeeParameters {
@@ -3134,23 +3096,38 @@ mod tests {
31343096
scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
31353097
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
31363098

3099+
if decay_before_reload {
3100+
SinceEpoch::advance(Duration::from_secs(10));
3101+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3102+
}
3103+
31373104
let mut serialized_scorer = Vec::new();
31383105
scorer.write(&mut serialized_scorer).unwrap();
31393106

3140-
SinceEpoch::advance(Duration::from_secs(10));
3141-
31423107
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
3143-
let deserialized_scorer =
3108+
let mut deserialized_scorer =
31443109
<ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
3110+
if !decay_before_reload {
3111+
SinceEpoch::advance(Duration::from_secs(10));
3112+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3113+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(10));
3114+
}
31453115
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
31463116

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

31503120
SinceEpoch::advance(Duration::from_secs(10));
3121+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(20));
31513122
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 370);
31523123
}
31533124

3125+
#[test]
3126+
fn decays_persisted_liquidity_bounds() {
3127+
do_decays_persisted_liquidity_bounds(false);
3128+
do_decays_persisted_liquidity_bounds(true);
3129+
}
3130+
31543131
#[test]
31553132
fn scores_realistic_payments() {
31563133
// Shows the scores of "realistic" sends of 100k sats over channels of 1-10m sats (with a
@@ -3414,11 +3391,11 @@ mod tests {
34143391
// Advance the time forward 16 half-lives (which the docs claim will ensure all data is
34153392
// gone), and check that we're back to where we started.
34163393
SinceEpoch::advance(Duration::from_secs(10 * 16));
3394+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 16));
34173395
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 168);
3418-
// Once fully decayed we still have data, but its all-0s. In the future we may remove the
3419-
// data entirely instead.
3396+
// Once fully decayed we still have data, so we get 0s for the buckets.
34203397
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
3421-
None);
3398+
Some(([0; 32], [0; 32])));
34223399
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1, &params), None);
34233400

34243401
let mut usage = ChannelUsage {
@@ -3429,7 +3406,7 @@ mod tests {
34293406
scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::from_secs(10 * 16));
34303407
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2050);
34313408
usage.inflight_htlc_msat = 0;
3432-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 866);
3409+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2048);
34333410

34343411
let usage = ChannelUsage {
34353412
amount_msat: 1,
@@ -3440,6 +3417,12 @@ mod tests {
34403417

34413418
// Advance to decay all liquidity offsets to zero.
34423419
SinceEpoch::advance(Duration::from_secs(60 * 60 * 10));
3420+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * (16 + 60 * 60)));
3421+
3422+
// Once even the bounds have decayed information about the channel should be removed
3423+
// entirely.
3424+
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
3425+
None);
34433426

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

0 commit comments

Comments
 (0)