Move to a constant for "bucket one" in the scoring buckets
[rust-lightning] / lightning / src / routing / scoring.rs
index 5fd355c23aee6376f95fdfca8bd5639d011c6d5b..5fdbf9ae3a9c8eff91ba0948ec86d8f9f3f7bdec 100644 (file)
@@ -706,9 +706,10 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
                                                let amt = directed_info.effective_capacity().as_msat();
                                                let dir_liq = liq.as_directed(source, target, 0, amt, self.decay_params);
 
-                                               let (min_buckets, max_buckets, _) = dir_liq.liquidity_history
+                                               let (min_buckets, max_buckets) = dir_liq.liquidity_history
                                                        .get_decayed_buckets(now, *dir_liq.last_updated,
-                                                               self.decay_params.historical_no_updates_half_life);
+                                                               self.decay_params.historical_no_updates_half_life)
+                                                       .unwrap_or(([0; 32], [0; 32]));
 
                                                log_debug!(self.logger, core::concat!(
                                                        "Liquidity from {} to {} via {} is in the range ({}, {}).\n",
@@ -787,7 +788,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
        /// in the top and bottom bucket, and roughly with similar (recent) frequency.
        ///
        /// Because the datapoints are decayed slowly over time, values will eventually return to
-       /// `Some(([0; 32], [0; 32]))`.
+       /// `Some(([1; 32], [1; 32]))` and then to `None` once no datapoints remain.
        ///
        /// In order to fetch a single success probability from the buckets provided here, as used in
        /// the scoring model, see [`Self::historical_estimated_payment_success_probability`].
@@ -801,9 +802,12 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref, T: Time> ProbabilisticScorerU
                                        let amt = directed_info.effective_capacity().as_msat();
                                        let dir_liq = liq.as_directed(source, target, 0, amt, self.decay_params);
 
-                                       let (min_buckets, mut max_buckets, _) = dir_liq.liquidity_history
-                                               .get_decayed_buckets(dir_liq.now, *dir_liq.last_updated,
-                                                       self.decay_params.historical_no_updates_half_life);
+                                       let (min_buckets, mut max_buckets) =
+                                               dir_liq.liquidity_history.get_decayed_buckets(
+                                                       dir_liq.now, *dir_liq.last_updated,
+                                                       self.decay_params.historical_no_updates_half_life
+                                               )?;
+
                                        // Note that the liquidity buckets are an offset from the edge, so we inverse
                                        // the max order to get the probabilities from zero.
                                        max_buckets.reverse();
@@ -1680,6 +1684,10 @@ mod bucketed_history {
                buckets: [u16; 32],
        }
 
+       /// Buckets are stored in fixed point numbers with a 5 bit fractional part. Thus, the value
+       /// "one" is 32, or this constant.
+       pub const BUCKET_FIXED_POINT_ONE: u16 = 32;
+
        impl HistoricalBucketRangeTracker {
                pub(super) fn new() -> Self { Self { buckets: [0; 32] } }
                pub(super) fn track_datapoint(&mut self, liquidity_offset_msat: u64, capacity_msat: u64) {
@@ -1710,7 +1718,7 @@ mod bucketed_history {
                                        *e = ((*e as u32) * 2047 / 2048) as u16;
                                }
                                let bucket = pos_to_bucket(pos);
-                               self.buckets[bucket] = self.buckets[bucket].saturating_add(32);
+                               self.buckets[bucket] = self.buckets[bucket].saturating_add(BUCKET_FIXED_POINT_ONE);
                        }
                }
                /// Decay all buckets by the given number of half-lives. Used to more aggressively remove old
@@ -1738,17 +1746,38 @@ mod bucketed_history {
        }
 
        impl<D: Deref<Target = HistoricalBucketRangeTracker>> HistoricalMinMaxBuckets<D> {
-               #[inline]
                pub(super) fn get_decayed_buckets<T: Time>(&self, now: T, last_updated: T, half_life: Duration)
-               -> ([u16; 32], [u16; 32], u32) {
-                       let required_decays = now.duration_since(last_updated).as_secs()
-                               .checked_div(half_life.as_secs())
-                               .map_or(u32::max_value(), |decays| cmp::min(decays, u32::max_value() as u64) as u32);
+               -> Option<([u16; 32], [u16; 32])> {
+                       let (_, required_decays) = self.get_total_valid_points(now, last_updated, half_life)?;
+
                        let mut min_buckets = *self.min_liquidity_offset_history;
                        min_buckets.time_decay_data(required_decays);
                        let mut max_buckets = *self.max_liquidity_offset_history;
                        max_buckets.time_decay_data(required_decays);
-                       (min_buckets.buckets, max_buckets.buckets, required_decays)
+                       Some((min_buckets.buckets, max_buckets.buckets))
+               }
+               #[inline]
+               pub(super) fn get_total_valid_points<T: Time>(&self, now: T, last_updated: T, half_life: Duration)
+               -> Option<(u64, u32)> {
+                       let required_decays = now.duration_since(last_updated).as_secs()
+                               .checked_div(half_life.as_secs())
+                               .map_or(u32::max_value(), |decays| cmp::min(decays, u32::max_value() as u64) as u32);
+
+                       let mut total_valid_points_tracked = 0;
+                       for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
+                               for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(32 - min_idx) {
+                                       total_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
+                               }
+                       }
+
+                       // If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
+                       // treat it as if we were fully decayed.
+                       const FULLY_DECAYED: u16 = BUCKET_FIXED_POINT_ONE * BUCKET_FIXED_POINT_ONE;
+                       if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < FULLY_DECAYED.into() {
+                               return None;
+                       }
+
+                       Some((total_valid_points_tracked, required_decays))
                }
 
                #[inline]
@@ -1762,32 +1791,45 @@ mod bucketed_history {
                        // state). For each pair, we calculate the probability as if the bucket's corresponding
                        // min- and max- liquidity bounds were our current liquidity bounds and then multiply
                        // that probability by the weight of the selected buckets.
-                       let mut total_valid_points_tracked = 0;
-
                        let payment_pos = amount_to_pos(amount_msat, capacity_msat);
                        if payment_pos >= POSITION_TICKS { return None; }
 
                        // Check if all our buckets are zero, once decayed and treat it as if we had no data. We
                        // don't actually use the decayed buckets, though, as that would lose precision.
-                       let (decayed_min_buckets, decayed_max_buckets, required_decays) =
-                               self.get_decayed_buckets(now, last_updated, half_life);
-                       if decayed_min_buckets.iter().all(|v| *v == 0) || decayed_max_buckets.iter().all(|v| *v == 0) {
-                               return None;
-                       }
+                       let (total_valid_points_tracked, _)
+                               = self.get_total_valid_points(now, last_updated, half_life)?;
 
-                       for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
-                               for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(32 - min_idx) {
-                                       total_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
+                       let mut cumulative_success_prob_times_billion = 0;
+                       // Special-case the 0th min bucket - it generally means we failed a payment, so only
+                       // consider the highest (i.e. largest-offset-from-max-capacity) max bucket for all
+                       // points against the 0th min bucket. This avoids the case where we fail to route
+                       // increasingly lower values over a channel, but treat each failure as a separate
+                       // datapoint, many of which may have relatively high maximum-available-liquidity
+                       // values, which will result in us thinking we have some nontrivial probability of
+                       // routing up to that amount.
+                       if self.min_liquidity_offset_history.buckets[0] != 0 {
+                               let mut highest_max_bucket_with_points = 0; // The highest max-bucket with any data
+                               let mut total_max_points = 0; // Total points in max-buckets to consider
+                               for (max_idx, max_bucket) in self.max_liquidity_offset_history.buckets.iter().enumerate() {
+                                       if *max_bucket >= BUCKET_FIXED_POINT_ONE {
+                                               highest_max_bucket_with_points = cmp::max(highest_max_bucket_with_points, max_idx);
+                                       }
+                                       total_max_points += *max_bucket as u64;
+                               }
+                               let max_bucket_end_pos = BUCKET_START_POS[32 - highest_max_bucket_with_points] - 1;
+                               if payment_pos < max_bucket_end_pos {
+                                       let bucket_prob_times_billion =
+                                               (self.min_liquidity_offset_history.buckets[0] as u64) * total_max_points
+                                                       * 1024 * 1024 * 1024 / total_valid_points_tracked;
+                                       cumulative_success_prob_times_billion += bucket_prob_times_billion *
+                                               ((max_bucket_end_pos - payment_pos) as u64) /
+                                               // Add an additional one in the divisor as the payment bucket has been
+                                               // rounded down.
+                                               (max_bucket_end_pos + 1) as u64;
                                }
-                       }
-                       // If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme), treat
-                       // it as if we were fully decayed.
-                       if total_valid_points_tracked.checked_shr(required_decays).unwrap_or(0) < 32*32 {
-                               return None;
                        }
 
-                       let mut cumulative_success_prob_times_billion = 0;
-                       for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
+                       for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate().skip(1) {
                                let min_bucket_start_pos = BUCKET_START_POS[min_idx];
                                for (max_idx, max_bucket) in self.max_liquidity_offset_history.buckets.iter().enumerate().take(32 - min_idx) {
                                        let max_bucket_end_pos = BUCKET_START_POS[32 - max_idx] - 1;
@@ -3054,7 +3096,7 @@ mod tests {
                // Even after we tell the scorer we definitely have enough available liquidity, it will
                // still remember that there was some failure in the past, and assign a non-0 penalty.
                scorer.payment_path_failed(&payment_path_for_amount(1000), 43);
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 198);
+               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 32);
                // The first points should be decayed just slightly and the last bucket has a new point.
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
                        Some(([31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0],
@@ -3064,12 +3106,12 @@ mod tests {
                // simply check bounds here.
                let five_hundred_prob =
                        scorer.historical_estimated_payment_success_probability(42, &target, 500).unwrap();
-               assert!(five_hundred_prob > 0.5);
-               assert!(five_hundred_prob < 0.52);
+               assert!(five_hundred_prob > 0.66);
+               assert!(five_hundred_prob < 0.68);
                let one_prob =
                        scorer.historical_estimated_payment_success_probability(42, &target, 1).unwrap();
-               assert!(one_prob < 0.95);
-               assert!(one_prob > 0.90);
+               assert!(one_prob < 1.0);
+               assert!(one_prob > 0.95);
 
                // Advance the time forward 16 half-lives (which the docs claim will ensure all data is
                // gone), and check that we're back to where we started.
@@ -3078,7 +3120,7 @@ mod tests {
                // Once fully decayed we still have data, but its all-0s. In the future we may remove the
                // data entirely instead.
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
-                       Some(([0; 32], [0; 32])));
+                       None);
                assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1), None);
 
                let mut usage = ChannelUsage {
@@ -3089,7 +3131,7 @@ mod tests {
                scorer.payment_path_failed(&payment_path_for_amount(1), 42);
                assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 2048);
                usage.inflight_htlc_msat = 0;
-               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 409);
+               assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, &params), 866);
 
                let usage = ChannelUsage {
                        amount_msat: 1,
@@ -3275,9 +3317,7 @@ mod tests {
                assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
                        Some(([63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                                [32, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));
-               assert!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat)
-                       .unwrap() > 0.24);
-               assert!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat)
-                       .unwrap() < 0.25);
+               assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat),
+                       Some(0.0));
        }
 }