Skip to content

Commit 61eafd1

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 c1b3644 commit 61eafd1

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,
@@ -2019,22 +2008,20 @@ mod bucketed_history {
20192008
}
20202009

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

20392026
let mut total_valid_points_tracked = 0;
20402027
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
@@ -2046,33 +2033,10 @@ mod bucketed_history {
20462033
// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
20472034
// treat it as if we were fully decayed.
20482035
const FULLY_DECAYED: u16 = BUCKET_FIXED_POINT_ONE * BUCKET_FIXED_POINT_ONE;
2049-
if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < FULLY_DECAYED.into() {
2036+
if total_valid_points_tracked < FULLY_DECAYED.into() {
20502037
return None;
20512038
}
20522039

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

2931-
// No decay
2932-
SinceEpoch::advance(Duration::from_secs(4));
2933-
let usage = ChannelUsage { amount_msat: 128, ..usage };
2934-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
2935-
let usage = ChannelUsage { amount_msat: 256, ..usage };
2936-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 93);
2937-
let usage = ChannelUsage { amount_msat: 768, ..usage };
2938-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 1_479);
2939-
let usage = ChannelUsage { amount_msat: 896, ..usage };
2940-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
2941-
29422895
// Half decay (i.e., three-quarter life)
2943-
SinceEpoch::advance(Duration::from_secs(1));
2896+
SinceEpoch::advance(Duration::from_secs(5));
2897+
scorer.decay_liquidity_certainty(Duration::from_secs(5));
29442898
let usage = ChannelUsage { amount_msat: 128, ..usage };
29452899
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 22);
29462900
let usage = ChannelUsage { amount_msat: 256, ..usage };
@@ -2952,6 +2906,7 @@ mod tests {
29522906

29532907
// One decay (i.e., half life)
29542908
SinceEpoch::advance(Duration::from_secs(5));
2909+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
29552910
let usage = ChannelUsage { amount_msat: 64, ..usage };
29562911
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29572912
let usage = ChannelUsage { amount_msat: 128, ..usage };
@@ -2963,6 +2918,7 @@ mod tests {
29632918

29642919
// Fully decay liquidity lower bound.
29652920
SinceEpoch::advance(Duration::from_secs(10 * 7));
2921+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 8));
29662922
let usage = ChannelUsage { amount_msat: 0, ..usage };
29672923
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29682924
let usage = ChannelUsage { amount_msat: 1, ..usage };
@@ -2974,12 +2930,14 @@ mod tests {
29742930

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

29822939
SinceEpoch::advance(Duration::from_secs(10));
2940+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 10));
29832941
let usage = ChannelUsage { amount_msat: 0, ..usage };
29842942
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29852943
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
@@ -3014,9 +2972,11 @@ mod tests {
30142972
// An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
30152973
// would cause an overflow.
30162974
SinceEpoch::advance(Duration::from_secs(10 * 64));
2975+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 64));
30172976
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30182977

30192978
SinceEpoch::advance(Duration::from_secs(10));
2979+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 65));
30202980
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30212981
}
30222982

@@ -3050,6 +3010,7 @@ mod tests {
30503010

30513011
// Decaying knowledge gives less confidence (128, 896), meaning a higher penalty.
30523012
SinceEpoch::advance(Duration::from_secs(10));
3013+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
30533014
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 291);
30543015

30553016
// Reducing the upper bound gives more confidence (128, 832) that the payment amount (512)
@@ -3064,6 +3025,7 @@ mod tests {
30643025

30653026
// Further decaying affects the lower bound more than the upper bound (128, 928).
30663027
SinceEpoch::advance(Duration::from_secs(10));
3028+
scorer.decay_liquidity_certainty(Duration::from_secs(20));
30673029
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 280);
30683030
}
30693031

@@ -3093,6 +3055,7 @@ mod tests {
30933055
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
30943056

30953057
SinceEpoch::advance(Duration::from_secs(10));
3058+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
30963059
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
30973060

30983061
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
@@ -3107,8 +3070,7 @@ mod tests {
31073070
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 300);
31083071
}
31093072

3110-
#[test]
3111-
fn decays_persisted_liquidity_bounds() {
3073+
fn do_decays_persisted_liquidity_bounds(decay_before_reload: bool) {
31123074
let logger = TestLogger::new();
31133075
let network_graph = network_graph(&logger);
31143076
let params = ProbabilisticScoringFeeParameters {
@@ -3132,23 +3094,38 @@ mod tests {
31323094
scorer.payment_path_failed(&payment_path_for_amount(500), 42, Duration::ZERO);
31333095
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), u64::max_value());
31343096

3097+
if decay_before_reload {
3098+
SinceEpoch::advance(Duration::from_secs(10));
3099+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3100+
}
3101+
31353102
let mut serialized_scorer = Vec::new();
31363103
scorer.write(&mut serialized_scorer).unwrap();
31373104

3138-
SinceEpoch::advance(Duration::from_secs(10));
3139-
31403105
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
3141-
let deserialized_scorer =
3106+
let mut deserialized_scorer =
31423107
<ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
3108+
if !decay_before_reload {
3109+
SinceEpoch::advance(Duration::from_secs(10));
3110+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3111+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(10));
3112+
}
31433113
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
31443114

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

31483118
SinceEpoch::advance(Duration::from_secs(10));
3119+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(20));
31493120
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 370);
31503121
}
31513122

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

34223399
let mut usage = ChannelUsage {
@@ -3427,7 +3404,7 @@ mod tests {
34273404
scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::from_secs(10 * 16));
34283405
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2050);
34293406
usage.inflight_htlc_msat = 0;
3430-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 866);
3407+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2048);
34313408

34323409
let usage = ChannelUsage {
34333410
amount_msat: 1,
@@ -3438,6 +3415,12 @@ mod tests {
34383415

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

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

0 commit comments

Comments
 (0)