Skip to content

Commit 3bd07d6

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 4774c35 commit 3bd07d6

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,
@@ -2018,22 +2007,20 @@ mod bucketed_history {
20182007
}
20192008

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

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

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

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

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

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

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

29812938
SinceEpoch::advance(Duration::from_secs(10));
2939+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 10));
29822940
let usage = ChannelUsage { amount_msat: 0, ..usage };
29832941
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 0);
29842942
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
@@ -3013,9 +2971,11 @@ mod tests {
30132971
// An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
30142972
// would cause an overflow.
30152973
SinceEpoch::advance(Duration::from_secs(10 * 64));
2974+
scorer.decay_liquidity_certainty(Duration::from_secs(10 * 64));
30162975
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 125);
30172976

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

@@ -3049,6 +3009,7 @@ mod tests {
30493009

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

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

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

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

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

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

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

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

3137-
SinceEpoch::advance(Duration::from_secs(10));
3138-
31393104
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
3140-
let deserialized_scorer =
3105+
let mut deserialized_scorer =
31413106
<ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
3107+
if !decay_before_reload {
3108+
SinceEpoch::advance(Duration::from_secs(10));
3109+
scorer.decay_liquidity_certainty(Duration::from_secs(10));
3110+
deserialized_scorer.decay_liquidity_certainty(Duration::from_secs(10));
3111+
}
31423112
assert_eq!(deserialized_scorer.channel_penalty_msat(42, &source, &target, usage, &params), 473);
31433113

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

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

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

34213398
let mut usage = ChannelUsage {
@@ -3426,7 +3403,7 @@ mod tests {
34263403
scorer.payment_path_failed(&payment_path_for_amount(1), 42, Duration::from_secs(10 * 16));
34273404
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2050);
34283405
usage.inflight_htlc_msat = 0;
3429-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 866);
3406+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2048);
34303407

34313408
let usage = ChannelUsage {
34323409
amount_msat: 1,
@@ -3437,6 +3414,12 @@ mod tests {
34373414

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

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

0 commit comments

Comments
 (0)