]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Add `probing_diversity_penalty_msat` to the scorer parameters 2024-11-probing-diversity
authorMatt Corallo <git@bluematt.me>
Sat, 23 Nov 2024 20:57:17 +0000 (20:57 +0000)
committerMatt Corallo <git@bluematt.me>
Sat, 23 Nov 2024 21:09:51 +0000 (21:09 +0000)
When doing background probing, its important to get a diverse view
of the network graph to ensure you have as many options when
pathfinding as possible.

Sadly, the naive probing we currently recommend users do does not
accomplish that - using the same success-optimized pathfinder when
sending as when probing results in always testing the same (good)
path over and over and over again.

Instead, here, we add a `probing_diversity_penalty_msat` parameter
to the scorer, allowing us to penalize channels for which we
already have recent data. It applies a penalty which scales with
the square of the inverse time since the channel was last updated,
up to one day.

lightning/src/routing/scoring.rs

index 1140bc83703504d6522368d23bba424b60cb0262..996ed164800f97134ef15da3d221398a72117cf2 100644 (file)
@@ -475,6 +475,9 @@ where L::Target: Logger {
        network_graph: G,
        logger: L,
        channel_liquidities: HashMap<u64, ChannelLiquidity>,
+       /// The last time we were given via a [`ScoreUpdate`] method. This does not imply that we've
+       /// decayed every liquidity bound up to that time.
+       last_duration_since_epoch: Duration,
 }
 
 /// Parameters for configuring [`ProbabilisticScorer`].
@@ -637,6 +640,22 @@ pub struct ProbabilisticScoringFeeParameters {
        ///
        /// Default value: false
        pub linear_success_probability: bool,
+
+       /// In order to ensure we have knowledge for as many paths as possible, when probing it makes
+       /// sense to bias away from channels for which we have very recent data.
+       ///
+       /// This value is a penalty that is applied based on the last time that we updated the bounds
+       /// on the available liquidity in a channel. The specified value is the maximum penalty that
+       /// will be applied.
+       ///
+       /// It obviously does not make sense to assign a non-0 value here unless you are using the
+       /// pathfinding result for background probing.
+       ///
+       /// Specifically, the following penalty is applied
+       /// `probing_diversity_penalty_msat * max(0, (86400 - current time + last update))^2 / 86400^2` is
+       ///
+       /// Default value: 0
+       pub probing_diversity_penalty_msat: u64,
 }
 
 impl Default for ProbabilisticScoringFeeParameters {
@@ -652,6 +671,7 @@ impl Default for ProbabilisticScoringFeeParameters {
                        historical_liquidity_penalty_multiplier_msat: 10_000,
                        historical_liquidity_penalty_amount_multiplier_msat: 64,
                        linear_success_probability: false,
+                       probing_diversity_penalty_msat: 0,
                }
        }
 }
@@ -706,6 +726,7 @@ impl ProbabilisticScoringFeeParameters {
                        anti_probing_penalty_msat: 0,
                        considered_impossible_penalty_msat: 0,
                        linear_success_probability: true,
+                       probing_diversity_penalty_msat: 0,
                }
        }
 }
@@ -850,6 +871,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ProbabilisticScorer<G, L> whe
                        network_graph,
                        logger,
                        channel_liquidities: new_hash_map(),
+                       last_duration_since_epoch: Duration::from_secs(0),
                }
        }
 
@@ -1172,7 +1194,7 @@ DirectedChannelLiquidity< L, HT, T> {
        /// Returns a liquidity penalty for routing the given HTLC `amount_msat` through the channel in
        /// this direction.
        fn penalty_msat(
-               &self, amount_msat: u64, inflight_htlc_msat: u64,
+               &self, amount_msat: u64, inflight_htlc_msat: u64, last_duration_since_epoch: Duration,
                score_params: &ProbabilisticScoringFeeParameters,
        ) -> u64 {
                let total_inflight_amount_msat = amount_msat.saturating_add(inflight_htlc_msat);
@@ -1247,6 +1269,13 @@ DirectedChannelLiquidity< L, HT, T> {
                        }
                }
 
+               if score_params.probing_diversity_penalty_msat != 0 {
+                       let time_since_update = last_duration_since_epoch.saturating_sub(*self.last_datapoint);
+                       let mul = Duration::from_secs(60 * 60 * 24).saturating_sub(time_since_update).as_secs();
+                       let penalty = score_params.probing_diversity_penalty_msat.saturating_mul(mul * mul);
+                       res = res.saturating_add(penalty / ((60 * 60 * 24) * (60 * 60 * 24)));
+               }
+
                res
        }
 
@@ -1398,11 +1427,12 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreLookUp for Probabilistic
                }
 
                let capacity_msat = usage.effective_capacity.as_msat();
+               let time = self.last_duration_since_epoch;
                self.channel_liquidities
                        .get(scid)
                        .unwrap_or(&ChannelLiquidity::new(Duration::ZERO))
                        .as_directed(&source, &target, capacity_msat)
-                       .penalty_msat(usage.amount_msat, usage.inflight_htlc_msat, score_params)
+                       .penalty_msat(usage.amount_msat, usage.inflight_htlc_msat, time, score_params)
                        .saturating_add(anti_probing_penalty_msat)
                        .saturating_add(base_penalty_msat)
        }
@@ -1448,6 +1478,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for Probabilistic
                        }
                        if at_failed_channel { break; }
                }
+               self.last_duration_since_epoch = duration_since_epoch;
        }
 
        fn payment_path_successful(&mut self, path: &Path, duration_since_epoch: Duration) {
@@ -1475,6 +1506,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for Probabilistic
                                        hop.short_channel_id);
                        }
                }
+               self.last_duration_since_epoch = duration_since_epoch;
        }
 
        fn probe_failed(&mut self, path: &Path, short_channel_id: u64, duration_since_epoch: Duration) {
@@ -1506,6 +1538,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for Probabilistic
                        liquidity.min_liquidity_offset_msat != 0 || liquidity.max_liquidity_offset_msat != 0 ||
                                liquidity.liquidity_history.has_datapoints()
                });
+               self.last_duration_since_epoch = duration_since_epoch;
        }
 }
 
@@ -1939,15 +1972,20 @@ ReadableArgs<(ProbabilisticScoringDecayParameters, G, L)> for ProbabilisticScore
                r: &mut R, args: (ProbabilisticScoringDecayParameters, G, L)
        ) -> Result<Self, DecodeError> {
                let (decay_params, network_graph, logger) = args;
-               let mut channel_liquidities = new_hash_map();
+               let mut channel_liquidities: HashMap<u64, ChannelLiquidity> = new_hash_map();
                read_tlv_fields!(r, {
                        (0, channel_liquidities, required),
                });
+               let mut last_duration_since_epoch = Duration::from_secs(0);
+               for (_, liq) in channel_liquidities.iter() {
+                       last_duration_since_epoch = cmp::max(last_duration_since_epoch, liq.last_updated);
+               }
                Ok(Self {
                        decay_params,
                        network_graph,
                        logger,
                        channel_liquidities,
+                       last_duration_since_epoch,
                })
        }
 }
@@ -3542,6 +3580,52 @@ mod tests {
                assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, &params),
                        Some(0.0));
        }
+
+       #[test]
+       fn probes_for_diversity() {
+               // Tests the probing_diversity_penalty_msat is applied
+               let logger = TestLogger::new();
+               let network_graph = network_graph(&logger);
+               let params = ProbabilisticScoringFeeParameters {
+                       probing_diversity_penalty_msat: 1_000_000,
+                       ..ProbabilisticScoringFeeParameters::zero_penalty()
+               };
+               let decay_params = ProbabilisticScoringDecayParameters {
+                       liquidity_offset_half_life: Duration::from_secs(10),
+                       ..ProbabilisticScoringDecayParameters::zero_penalty()
+               };
+               let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
+               let source = source_node_id();
+
+               let usage = ChannelUsage {
+                       amount_msat: 512,
+                       inflight_htlc_msat: 0,
+                       effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
+               };
+               let channel = network_graph.read_only().channel(42).unwrap().to_owned();
+               let (info, _) = channel.as_directed_from(&source).unwrap();
+               let candidate = CandidateRouteHop::PublicHop(PublicHopCandidate {
+                       info,
+                       short_channel_id: 42,
+               });
+
+               // Apply some update to set the last-update time to now
+               scorer.payment_path_failed(&payment_path_for_amount(1000), 42, Duration::ZERO);
+
+               // If no time has passed, we get the full probing_diversity_penalty_msat
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 1_000_000);
+
+               // As time passes the penalty decreases.
+               scorer.time_passed(Duration::from_secs(1));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 999_976);
+
+               scorer.time_passed(Duration::from_secs(2));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 999_953);
+
+               // Once we've gotten halfway through the day our penalty is 1/4 the configured value.
+               scorer.time_passed(Duration::from_secs(86400/2));
+               assert_eq!(scorer.channel_penalty_msat(&candidate, usage, &params), 250_000);
+       }
 }
 
 #[cfg(ldk_bench)]