Skip to content

Commit 6c7f568

Browse files
committed
Decay historical liquidity tracking when no new data is added
To avoid scoring based on incredibly old historical liquidity data, we add a new half-life here which is used to (very slowly) decay historical liquidity tracking buckets.
1 parent f2b9189 commit 6c7f568

File tree

1 file changed

+47
-6
lines changed

1 file changed

+47
-6
lines changed

lightning/src/routing/scoring.rs

+47-6
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ use util::logger::Logger;
6262
use util::time::Time;
6363

6464
use prelude::*;
65-
use core::fmt;
65+
use core::{cmp, fmt};
6666
use core::cell::{RefCell, RefMut};
6767
use core::convert::TryInto;
6868
use core::ops::{Deref, DerefMut};
@@ -436,6 +436,16 @@ pub struct ProbabilisticScoringParameters {
436436
/// [`liquidity_penalty_amount_multiplier_msat`]: Self::liquidity_penalty_amount_multiplier_msat
437437
pub historical_liquidity_penalty_amount_multiplier_msat: u64,
438438

439+
/// If we aren't learning any new datapoints for a channel, the historical liquidity bounds
440+
/// tracking can simply live on with increasingly stale data. Instead, when a channel has not
441+
/// seen a liquidity estimate update for this amount of time, the historical datapoints are
442+
/// decayed by half.
443+
///
444+
/// Note that after 16 or more half lives all historical data will be completely gone.
445+
///
446+
/// Default value: 14 days
447+
pub historical_no_updates_half_life: Duration,
448+
439449
/// Manual penalties used for the given nodes. Allows to set a particular penalty for a given
440450
/// node. Note that a manual penalty of `u64::max_value()` means the node would not ever be
441451
/// considered during path finding.
@@ -509,6 +519,13 @@ impl HistoricalBucketRangeTracker {
509519
self.buckets[bucket_idx as usize] = self.buckets[bucket_idx as usize].saturating_add(32);
510520
}
511521
}
522+
/// Decay all buckets by the given number of half-lives. Used to more aggressively remove old
523+
/// datapoints as we receive newer information.
524+
fn time_decay_data(&mut self, half_lives: u32) {
525+
for e in self.buckets.iter_mut() {
526+
*e = e.checked_shr(half_lives).unwrap_or(0);
527+
}
528+
}
512529
}
513530

514531
impl_writeable_tlv_based!(HistoricalBucketRangeTracker, { (0, buckets, required) });
@@ -645,6 +662,7 @@ impl ProbabilisticScoringParameters {
645662
liquidity_penalty_amount_multiplier_msat: 0,
646663
historical_liquidity_penalty_multiplier_msat: 0,
647664
historical_liquidity_penalty_amount_multiplier_msat: 0,
665+
historical_no_updates_half_life: Duration::from_secs(60 * 60 * 24 * 14),
648666
manual_node_penalties: HashMap::new(),
649667
anti_probing_penalty_msat: 0,
650668
considered_impossible_penalty_msat: 0,
@@ -670,6 +688,7 @@ impl Default for ProbabilisticScoringParameters {
670688
liquidity_penalty_amount_multiplier_msat: 192,
671689
historical_liquidity_penalty_multiplier_msat: 10_000,
672690
historical_liquidity_penalty_amount_multiplier_msat: 64,
691+
historical_no_updates_half_life: Duration::from_secs(60 * 60 * 24 * 14),
673692
manual_node_penalties: HashMap::new(),
674693
anti_probing_penalty_msat: 250,
675694
considered_impossible_penalty_msat: 1_0000_0000_000,
@@ -810,14 +829,30 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
810829
// sending less than 1/16th of a channel's capacity, or 1/8th if we used the top of the
811830
// bucket.
812831
let mut total_valid_points_tracked = 0;
832+
let required_decays = self.now.duration_since(*self.last_updated).as_secs()
833+
.checked_div(params.historical_no_updates_half_life.as_secs())
834+
.map_or(u32::max_value(), |decays| cmp::min(decays, u32::max_value() as u64) as u32);
835+
836+
// Rather than actually decaying the individual buckets, which would lose precision, we
837+
// simply track whether all buckets would be decayed to zero, in which case we treat it
838+
// as if we had no data.
839+
let mut is_fully_decayed = true;
840+
let mut check_track_bucket_contains_undecayed_points =
841+
|bucket_val: u16| if bucket_val.checked_shr(required_decays).unwrap_or(0) > 0 { is_fully_decayed = false; };
842+
813843
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
844+
check_track_bucket_contains_undecayed_points(*min_bucket);
814845
for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(8 - min_idx) {
815846
total_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
847+
check_track_bucket_contains_undecayed_points(*max_bucket);
816848
}
817849
}
818-
if total_valid_points_tracked == 0 {
819-
// If we don't have any valid points, redo the non-historical calculation with no
820-
// liquidity bounds tracked and the historical penalty multipliers.
850+
// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
851+
// treat it as if we were fully decayed.
852+
if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < 32*32 || is_fully_decayed {
853+
// If we don't have any valid points (or, once decayed, we have less than a full
854+
// point), redo the non-historical calculation with no liquidity bounds tracked and
855+
// the historical penalty multipliers.
821856
let max_capacity = self.capacity_msat.saturating_sub(amount_msat).saturating_add(1);
822857
let negative_log10_times_2048 =
823858
approx::negative_log10_times_2048(max_capacity, self.capacity_msat.saturating_add(1));
@@ -925,6 +960,12 @@ impl<L: DerefMut<Target = u64>, BRT: DerefMut<Target = HistoricalBucketRangeTrac
925960
}
926961

927962
fn update_history_buckets(&mut self) {
963+
let half_lives = self.now.duration_since(*self.last_updated).as_secs()
964+
.checked_div(self.params.historical_no_updates_half_life.as_secs())
965+
.map(|v| v.try_into().unwrap_or(u32::max_value())).unwrap_or(u32::max_value());
966+
self.min_liquidity_offset_history.time_decay_data(half_lives);
967+
self.max_liquidity_offset_history.time_decay_data(half_lives);
968+
928969
debug_assert!(*self.min_liquidity_offset_msat <= self.capacity_msat);
929970
self.min_liquidity_offset_history.track_datapoint(
930971
// Ensure the bucket index we pass is in the range [0, 7], even if the liquidity offset
@@ -947,8 +988,8 @@ impl<L: DerefMut<Target = u64>, BRT: DerefMut<Target = HistoricalBucketRangeTrac
947988
} else {
948989
self.decayed_offset_msat(*self.max_liquidity_offset_msat)
949990
};
950-
*self.last_updated = self.now;
951991
self.update_history_buckets();
992+
*self.last_updated = self.now;
952993
}
953994

954995
/// Adjusts the upper bound of the channel liquidity balance in this direction.
@@ -959,8 +1000,8 @@ impl<L: DerefMut<Target = u64>, BRT: DerefMut<Target = HistoricalBucketRangeTrac
9591000
} else {
9601001
self.decayed_offset_msat(*self.min_liquidity_offset_msat)
9611002
};
962-
*self.last_updated = self.now;
9631003
self.update_history_buckets();
1004+
*self.last_updated = self.now;
9641005
}
9651006
}
9661007

0 commit comments

Comments
 (0)