@@ -476,6 +476,9 @@ where L::Target: Logger {
476476 network_graph : G ,
477477 logger : L ,
478478 channel_liquidities : ChannelLiquidities ,
479+ /// The last time we were given via a [`ScoreUpdate`] method. This does not imply that we've
480+ /// decayed every liquidity bound up to that time.
481+ last_update_time : Duration ,
479482}
480483/// Container for live and historical liquidity bounds for each channel.
481484#[ derive( Clone ) ]
@@ -745,6 +748,29 @@ pub struct ProbabilisticScoringFeeParameters {
745748 ///
746749 /// Default value: false
747750 pub linear_success_probability : bool ,
751+
752+ /// In order to ensure we have knowledge for as many paths as possible, when probing it makes
753+ /// sense to bias away from channels for which we have very recent data.
754+ ///
755+ /// This value is a penalty that is applied based on the last time that we updated the bounds
756+ /// on the available liquidity in a channel. The specified value is the maximum penalty that
757+ /// will be applied.
758+ ///
759+ /// It obviously does not make sense to assign a non-0 value here unless you are using the
760+ /// pathfinding result for background probing.
761+ ///
762+ /// Specifically, the following penalty is applied
763+ /// `probing_diversity_penalty_msat * max(0, (86400 - current time + last update))^2 / 86400^2` is
764+ ///
765+ /// As this is a maximum value, when setting this you should consider it in relation to the
766+ /// other values set to ensure that, at maximum, we strongly avoid paths which we recently
767+ /// tried (similar to if they have a low success probability). For example, you might set this
768+ /// to be the sum of [`Self::base_penalty_msat`] and
769+ /// [`Self::historical_liquidity_penalty_multiplier_msat`] (plus some multiple of their
770+ /// corresponding `amount_multiplier`s).
771+ ///
772+ /// Default value: 0
773+ pub probing_diversity_penalty_msat : u64 ,
748774}
749775
750776impl Default for ProbabilisticScoringFeeParameters {
@@ -760,6 +786,7 @@ impl Default for ProbabilisticScoringFeeParameters {
760786 historical_liquidity_penalty_multiplier_msat : 10_000 ,
761787 historical_liquidity_penalty_amount_multiplier_msat : 1_250 ,
762788 linear_success_probability : false ,
789+ probing_diversity_penalty_msat : 0 ,
763790 }
764791 }
765792}
@@ -814,6 +841,7 @@ impl ProbabilisticScoringFeeParameters {
814841 anti_probing_penalty_msat : 0 ,
815842 considered_impossible_penalty_msat : 0 ,
816843 linear_success_probability : true ,
844+ probing_diversity_penalty_msat : 0 ,
817845 }
818846 }
819847}
@@ -927,6 +955,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ProbabilisticScorer<G, L> whe
927955 network_graph,
928956 logger,
929957 channel_liquidities : ChannelLiquidities :: new ( ) ,
958+ last_update_time : Duration :: from_secs ( 0 ) ,
930959 }
931960 }
932961
@@ -1383,7 +1412,7 @@ DirectedChannelLiquidity< L, HT, T> {
13831412 /// Returns a liquidity penalty for routing the given HTLC `amount_msat` through the channel in
13841413 /// this direction.
13851414 fn penalty_msat (
1386- & self , amount_msat : u64 , inflight_htlc_msat : u64 ,
1415+ & self , amount_msat : u64 , inflight_htlc_msat : u64 , last_update_time : Duration ,
13871416 score_params : & ProbabilisticScoringFeeParameters ,
13881417 ) -> u64 {
13891418 let total_inflight_amount_msat = amount_msat. saturating_add ( inflight_htlc_msat) ;
@@ -1465,6 +1494,18 @@ DirectedChannelLiquidity< L, HT, T> {
14651494 }
14661495 }
14671496
1497+ if score_params. probing_diversity_penalty_msat != 0 {
1498+ // We use `last_update_time` as a stand-in for the current time as we don't want to
1499+ // fetch the current time in every score call (slowing things down substantially on
1500+ // some platforms where a syscall is required), don't want to add an unnecessary `std`
1501+ // requirement. Assuming we're probing somewhat regularly, it should reliably be close
1502+ // to the current time, (and using the last the last time we probed is also fine here).
1503+ let time_since_update = last_update_time. saturating_sub ( * self . last_datapoint_time ) ;
1504+ let mul = Duration :: from_secs ( 60 * 60 * 24 ) . saturating_sub ( time_since_update) . as_secs ( ) ;
1505+ let penalty = score_params. probing_diversity_penalty_msat . saturating_mul ( mul * mul) ;
1506+ res = res. saturating_add ( penalty / ( ( 60 * 60 * 24 ) * ( 60 * 60 * 24 ) ) ) ;
1507+ }
1508+
14681509 res
14691510 }
14701511
@@ -1616,11 +1657,12 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreLookUp for Probabilistic
16161657 }
16171658
16181659 let capacity_msat = usage. effective_capacity . as_msat ( ) ;
1660+ let time = self . last_update_time ;
16191661 self . channel_liquidities
16201662 . get ( scid)
16211663 . unwrap_or ( & ChannelLiquidity :: new ( Duration :: ZERO ) )
16221664 . as_directed ( & source, & target, capacity_msat)
1623- . penalty_msat ( usage. amount_msat , usage. inflight_htlc_msat , score_params)
1665+ . penalty_msat ( usage. amount_msat , usage. inflight_htlc_msat , time , score_params)
16241666 . saturating_add ( anti_probing_penalty_msat)
16251667 . saturating_add ( base_penalty_msat)
16261668 }
@@ -1666,6 +1708,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for Probabilistic
16661708 }
16671709 if at_failed_channel { break ; }
16681710 }
1711+ self . last_update_time = duration_since_epoch;
16691712 }
16701713
16711714 fn payment_path_successful ( & mut self , path : & Path , duration_since_epoch : Duration ) {
@@ -1693,6 +1736,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for Probabilistic
16931736 hop. short_channel_id) ;
16941737 }
16951738 }
1739+ self . last_update_time = duration_since_epoch;
16961740 }
16971741
16981742 fn probe_failed ( & mut self , path : & Path , short_channel_id : u64 , duration_since_epoch : Duration ) {
@@ -1705,6 +1749,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Deref> ScoreUpdate for Probabilistic
17051749
17061750 fn time_passed ( & mut self , duration_since_epoch : Duration ) {
17071751 self . channel_liquidities . time_passed ( duration_since_epoch, self . decay_params ) ;
1752+ self . last_update_time = duration_since_epoch;
17081753 }
17091754}
17101755
@@ -2361,11 +2406,16 @@ ReadableArgs<(ProbabilisticScoringDecayParameters, G, L)> for ProbabilisticScore
23612406 ) -> Result < Self , DecodeError > {
23622407 let ( decay_params, network_graph, logger) = args;
23632408 let channel_liquidities = ChannelLiquidities :: read ( r) ?;
2409+ let mut last_update_time = Duration :: from_secs ( 0 ) ;
2410+ for ( _, liq) in channel_liquidities. 0 . iter ( ) {
2411+ last_update_time = cmp:: max ( last_update_time, liq. last_updated ) ;
2412+ }
23642413 Ok ( Self {
23652414 decay_params,
23662415 network_graph,
23672416 logger,
23682417 channel_liquidities,
2418+ last_update_time,
23692419 } )
23702420 }
23712421}
@@ -4060,6 +4110,52 @@ mod tests {
40604110 combined_scorer. scorer . estimated_channel_liquidity_range ( 42 , & target_node_id ( ) ) ;
40614111 assert_eq ! ( liquidity_range. unwrap( ) , ( 0 , 0 ) ) ;
40624112 }
4113+
4114+ #[ test]
4115+ fn probes_for_diversity ( ) {
4116+ // Tests the probing_diversity_penalty_msat is applied
4117+ let logger = TestLogger :: new ( ) ;
4118+ let network_graph = network_graph ( & logger) ;
4119+ let params = ProbabilisticScoringFeeParameters {
4120+ probing_diversity_penalty_msat : 1_000_000 ,
4121+ ..ProbabilisticScoringFeeParameters :: zero_penalty ( )
4122+ } ;
4123+ let decay_params = ProbabilisticScoringDecayParameters {
4124+ liquidity_offset_half_life : Duration :: from_secs ( 10 ) ,
4125+ ..ProbabilisticScoringDecayParameters :: zero_penalty ( )
4126+ } ;
4127+ let mut scorer = ProbabilisticScorer :: new ( decay_params, & network_graph, & logger) ;
4128+ let source = source_node_id ( ) ;
4129+
4130+ let usage = ChannelUsage {
4131+ amount_msat : 512 ,
4132+ inflight_htlc_msat : 0 ,
4133+ effective_capacity : EffectiveCapacity :: Total { capacity_msat : 1_024 , htlc_maximum_msat : 1_024 } ,
4134+ } ;
4135+ let channel = network_graph. read_only ( ) . channel ( 42 ) . unwrap ( ) . to_owned ( ) ;
4136+ let ( info, _) = channel. as_directed_from ( & source) . unwrap ( ) ;
4137+ let candidate = CandidateRouteHop :: PublicHop ( PublicHopCandidate {
4138+ info,
4139+ short_channel_id : 42 ,
4140+ } ) ;
4141+
4142+ // Apply some update to set the last-update time to now
4143+ scorer. payment_path_failed ( & payment_path_for_amount ( 1000 ) , 42 , Duration :: ZERO ) ;
4144+
4145+ // If no time has passed, we get the full probing_diversity_penalty_msat
4146+ assert_eq ! ( scorer. channel_penalty_msat( & candidate, usage, & params) , 1_000_000 ) ;
4147+
4148+ // As time passes the penalty decreases.
4149+ scorer. time_passed ( Duration :: from_secs ( 1 ) ) ;
4150+ assert_eq ! ( scorer. channel_penalty_msat( & candidate, usage, & params) , 999_976 ) ;
4151+
4152+ scorer. time_passed ( Duration :: from_secs ( 2 ) ) ;
4153+ assert_eq ! ( scorer. channel_penalty_msat( & candidate, usage, & params) , 999_953 ) ;
4154+
4155+ // Once we've gotten halfway through the day our penalty is 1/4 the configured value.
4156+ scorer. time_passed ( Duration :: from_secs ( 86400 /2 ) ) ;
4157+ assert_eq ! ( scorer. channel_penalty_msat( & candidate, usage, & params) , 250_000 ) ;
4158+ }
40634159}
40644160
40654161#[ cfg( ldk_bench) ]
0 commit comments