Skip to content

Commit 35b4964

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 9659c06 commit 35b4964

File tree

1 file changed

+59
-77
lines changed

1 file changed

+59
-77
lines changed

lightning/src/routing/scoring.rs

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

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

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

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

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

993986
return dir_liq.liquidity_history.calculate_success_probability_times_billion(
994-
dir_liq.now, *dir_liq.offset_history_last_updated,
995-
self.decay_params.historical_no_updates_half_life, &params, amount_msat,
996-
capacity_msat
987+
&params, amount_msat, capacity_msat
997988
).map(|p| p as f64 / (1024 * 1024 * 1024) as f64);
998989
}
999990
}
@@ -1214,9 +1205,7 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
12141205
score_params.historical_liquidity_penalty_amount_multiplier_msat != 0 {
12151206
if let Some(cumulative_success_prob_times_billion) = self.liquidity_history
12161207
.calculate_success_probability_times_billion(
1217-
self.now, *self.offset_history_last_updated,
1218-
self.decay_params.historical_no_updates_half_life, score_params, amount_msat,
1219-
self.capacity_msat)
1208+
score_params, amount_msat, self.capacity_msat)
12201209
{
12211210
let historical_negative_log10_times_2048 = approx::negative_log10_times_2048(cumulative_success_prob_times_billion + 1, 1024 * 1024 * 1024);
12221211
res = res.saturating_add(Self::combined_penalty_msat(amount_msat,
@@ -2027,22 +2016,20 @@ mod bucketed_history {
20272016
}
20282017

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

20472034
let mut total_valid_points_tracked = 0;
20482035
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
@@ -2054,33 +2041,10 @@ mod bucketed_history {
20542041
// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
20552042
// treat it as if we were fully decayed.
20562043
const FULLY_DECAYED: u16 = BUCKET_FIXED_POINT_ONE * BUCKET_FIXED_POINT_ONE;
2057-
if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < FULLY_DECAYED.into() {
2044+
if total_valid_points_tracked < FULLY_DECAYED.into() {
20582045
return None;
20592046
}
20602047

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

3015-
// No decay
3016-
SinceEpoch::advance(Duration::from_secs(4));
3017-
let usage = ChannelUsage { amount_msat: 128, ..usage };
3018-
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
3019-
let usage = ChannelUsage { amount_msat: 256, ..usage };
3020-
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 93);
3021-
let usage = ChannelUsage { amount_msat: 768, ..usage };
3022-
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 1_479);
3023-
let usage = ChannelUsage { amount_msat: 896, ..usage };
3024-
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
3025-
30262979
// Half decay (i.e., three-quarter life)
3027-
SinceEpoch::advance(Duration::from_secs(1));
2980+
SinceEpoch::advance(Duration::from_secs(5));
2981+
scorer.time_passed(Duration::from_secs(5));
30282982
let usage = ChannelUsage { amount_msat: 128, ..usage };
30292983
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 22);
30302984
let usage = ChannelUsage { amount_msat: 256, ..usage };
@@ -3036,6 +2990,7 @@ mod tests {
30362990

30372991
// One decay (i.e., half life)
30382992
SinceEpoch::advance(Duration::from_secs(5));
2993+
scorer.time_passed(Duration::from_secs(10));
30392994
let usage = ChannelUsage { amount_msat: 64, ..usage };
30402995
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
30412996
let usage = ChannelUsage { amount_msat: 128, ..usage };
@@ -3047,6 +3002,7 @@ mod tests {
30473002

30483003
// Fully decay liquidity lower bound.
30493004
SinceEpoch::advance(Duration::from_secs(10 * 7));
3005+
scorer.time_passed(Duration::from_secs(10 * 8));
30503006
let usage = ChannelUsage { amount_msat: 0, ..usage };
30513007
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
30523008
let usage = ChannelUsage { amount_msat: 1, ..usage };
@@ -3058,12 +3014,14 @@ mod tests {
30583014

30593015
// Fully decay liquidity upper bound.
30603016
SinceEpoch::advance(Duration::from_secs(10));
3017+
scorer.time_passed(Duration::from_secs(10 * 9));
30613018
let usage = ChannelUsage { amount_msat: 0, ..usage };
30623019
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
30633020
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
30643021
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
30653022

30663023
SinceEpoch::advance(Duration::from_secs(10));
3024+
scorer.time_passed(Duration::from_secs(10 * 10));
30673025
let usage = ChannelUsage { amount_msat: 0, ..usage };
30683026
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 0);
30693027
let usage = ChannelUsage { amount_msat: 1_024, ..usage };
@@ -3103,9 +3061,11 @@ mod tests {
31033061
// An unchecked right shift 64 bits or more in DirectedChannelLiquidity::decayed_offset_msat
31043062
// would cause an overflow.
31053063
SinceEpoch::advance(Duration::from_secs(10 * 64));
3064+
scorer.time_passed(Duration::from_secs(10 * 64));
31063065
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 125);
31073066

31083067
SinceEpoch::advance(Duration::from_secs(10));
3068+
scorer.time_passed(Duration::from_secs(10 * 65));
31093069
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 125);
31103070
}
31113071

@@ -3144,6 +3104,7 @@ mod tests {
31443104

31453105
// Decaying knowledge gives less confidence (128, 896), meaning a higher penalty.
31463106
SinceEpoch::advance(Duration::from_secs(10));
3107+
scorer.time_passed(Duration::from_secs(10));
31473108
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 291);
31483109

31493110
// Reducing the upper bound gives more confidence (128, 832) that the payment amount (512)
@@ -3158,6 +3119,7 @@ mod tests {
31583119

31593120
// Further decaying affects the lower bound more than the upper bound (128, 928).
31603121
SinceEpoch::advance(Duration::from_secs(10));
3122+
scorer.time_passed(Duration::from_secs(20));
31613123
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 280);
31623124
}
31633125

@@ -3192,6 +3154,7 @@ mod tests {
31923154
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
31933155

31943156
SinceEpoch::advance(Duration::from_secs(10));
3157+
scorer.time_passed(Duration::from_secs(10));
31953158
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 473);
31963159

31973160
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
@@ -3206,8 +3169,7 @@ mod tests {
32063169
assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, &params), 300);
32073170
}
32083171

3209-
#[test]
3210-
fn decays_persisted_liquidity_bounds() {
3172+
fn do_decays_persisted_liquidity_bounds(decay_before_reload: bool) {
32113173
let logger = TestLogger::new();
32123174
let network_graph = network_graph(&logger);
32133175
let params = ProbabilisticScoringFeeParameters {
@@ -3236,23 +3198,38 @@ mod tests {
32363198
};
32373199
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), u64::max_value());
32383200

3201+
if decay_before_reload {
3202+
SinceEpoch::advance(Duration::from_secs(10));
3203+
scorer.time_passed(Duration::from_secs(10));
3204+
}
3205+
32393206
let mut serialized_scorer = Vec::new();
32403207
scorer.write(&mut serialized_scorer).unwrap();
32413208

3242-
SinceEpoch::advance(Duration::from_secs(10));
3243-
32443209
let mut serialized_scorer = io::Cursor::new(&serialized_scorer);
3245-
let deserialized_scorer =
3210+
let mut deserialized_scorer =
32463211
<ProbabilisticScorer>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap();
3212+
if !decay_before_reload {
3213+
SinceEpoch::advance(Duration::from_secs(10));
3214+
scorer.time_passed(Duration::from_secs(10));
3215+
deserialized_scorer.time_passed(Duration::from_secs(10));
3216+
}
32473217
assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, &params), 473);
32483218

32493219
scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10));
32503220
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 300);
32513221

32523222
SinceEpoch::advance(Duration::from_secs(10));
3223+
deserialized_scorer.time_passed(Duration::from_secs(20));
32533224
assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, &params), 370);
32543225
}
32553226

3227+
#[test]
3228+
fn decays_persisted_liquidity_bounds() {
3229+
do_decays_persisted_liquidity_bounds(false);
3230+
do_decays_persisted_liquidity_bounds(true);
3231+
}
3232+
32563233
#[test]
32573234
fn scores_realistic_payments() {
32583235
// Shows the scores of "realistic" sends of 100k sats over channels of 1-10m sats (with a
@@ -3577,6 +3554,7 @@ mod tests {
35773554
// Advance the time forward 16 half-lives (which the docs claim will ensure all data is
35783555
// gone), and check that we're back to where we started.
35793556
SinceEpoch::advance(Duration::from_secs(10 * 16));
3557+
scorer.time_passed(Duration::from_secs(10 * 16));
35803558
{
35813559
let network_graph = network_graph.read_only();
35823560
let channel = network_graph.channel(42).unwrap();
@@ -3591,7 +3569,7 @@ mod tests {
35913569
// Once fully decayed we still have data, but its all-0s. In the future we may remove the
35923570
// data entirely instead.
35933571
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
3594-
None);
3572+
Some(([0; 32], [0; 32])));
35953573
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1, &params), None);
35963574

35973575
let mut usage = ChannelUsage {
@@ -3610,8 +3588,6 @@ mod tests {
36103588
};
36113589

36123590
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 2050);
3613-
usage.inflight_htlc_msat = 0;
3614-
assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 866);
36153591

36163592
let usage = ChannelUsage {
36173593
amount_msat: 1,
@@ -3623,6 +3599,12 @@ mod tests {
36233599

36243600
// Advance to decay all liquidity offsets to zero.
36253601
SinceEpoch::advance(Duration::from_secs(60 * 60 * 10));
3602+
scorer.time_passed(Duration::from_secs(10 * (16 + 60 * 60)));
3603+
3604+
// Once even the bounds have decayed information about the channel should be removed
3605+
// entirely.
3606+
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
3607+
None);
36263608

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

0 commit comments

Comments
 (0)