+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), u64::max_value());
+ }
+
+ #[test]
+ fn remembers_historical_failures() {
+ let logger = TestLogger::new();
+ let network_graph = network_graph(&logger);
+ let params = ProbabilisticScoringFeeParameters {
+ historical_liquidity_penalty_multiplier_msat: 1024,
+ historical_liquidity_penalty_amount_multiplier_msat: 1024,
+ ..ProbabilisticScoringFeeParameters::zero_penalty()
+ };
+ let decay_params = ProbabilisticScoringDecayParameters {
+ liquidity_offset_half_life: Duration::from_secs(60 * 60),
+ historical_no_updates_half_life: Duration::from_secs(10),
+ };
+ let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ let usage = ChannelUsage {
+ amount_msat: 100,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
+ };
+ let usage_1 = ChannelUsage {
+ amount_msat: 1,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
+ };
+
+ // With no historical data the normal liquidity penalty calculation is used.
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 168);
+ assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
+ None);
+ assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 42, ¶ms),
+ None);
+
+ scorer.payment_path_failed(&payment_path_for_amount(1), 42);
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 2048);
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage_1, ¶ms), 249);
+ // The "it failed" increment is 32, where the probability should lie several buckets into
+ // the first octile.
+ assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
+ Some(([32, 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],
+ [0, 0, 0, 0, 0, 0, 32, 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, 1, ¶ms)
+ .unwrap() > 0.35);
+ assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 500, ¶ms),
+ Some(0.0));
+
+ // 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, ¶ms), 105);
+ // 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],
+ [0, 0, 0, 0, 0, 0, 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, 32])));
+
+ // The exact success probability is a bit complicated and involves integer rounding, so we
+ // simply check bounds here.
+ let five_hundred_prob =
+ scorer.historical_estimated_payment_success_probability(42, &target, 500, ¶ms).unwrap();
+ assert!(five_hundred_prob > 0.59);
+ assert!(five_hundred_prob < 0.60);
+ let one_prob =
+ scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms).unwrap();
+ assert!(one_prob < 0.85);
+ assert!(one_prob > 0.84);
+
+ // 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.
+ SinceEpoch::advance(Duration::from_secs(10 * 16));
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 168);
+ // 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),
+ None);
+ assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms), None);
+
+ let mut usage = ChannelUsage {
+ amount_msat: 100,
+ inflight_htlc_msat: 1024,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_024 },
+ };
+ scorer.payment_path_failed(&payment_path_for_amount(1), 42);
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 2050);
+ usage.inflight_htlc_msat = 0;
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 866);
+
+ let usage = ChannelUsage {
+ amount_msat: 1,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::AdvertisedMaxHTLC { amount_msat: 0 },
+ };
+ assert_eq!(scorer.channel_penalty_msat(42, &target, &source, usage, ¶ms), 2048);
+
+ // Advance to decay all liquidity offsets to zero.
+ SinceEpoch::advance(Duration::from_secs(60 * 60 * 10));
+
+ // Use a path in the opposite direction, which have zero for htlc_maximum_msat. This will
+ // ensure that the effective capacity is zero to test division-by-zero edge cases.
+ let path = vec![
+ path_hop(target_pubkey(), 43, 2),
+ path_hop(source_pubkey(), 42, 1),
+ path_hop(sender_pubkey(), 41, 0),
+ ];
+ scorer.payment_path_failed(&Path { hops: path, blinded_tail: None }, 42);
+ }
+
+ #[test]
+ fn adds_anti_probing_penalty() {
+ let logger = TestLogger::new();
+ let network_graph = network_graph(&logger);
+ let source = source_node_id();
+ let target = target_node_id();
+ let params = ProbabilisticScoringFeeParameters {
+ anti_probing_penalty_msat: 500,
+ ..ProbabilisticScoringFeeParameters::zero_penalty()
+ };
+ let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger);
+
+ // Check we receive no penalty for a low htlc_maximum_msat.
+ let usage = ChannelUsage {
+ amount_msat: 512_000,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_000 },
+ };
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 0);
+
+ // Check we receive anti-probing penalty for htlc_maximum_msat == channel_capacity.
+ let usage = ChannelUsage {
+ amount_msat: 512_000,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_024_000 },
+ };
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 500);
+
+ // Check we receive anti-probing penalty for htlc_maximum_msat == channel_capacity/2.
+ let usage = ChannelUsage {
+ amount_msat: 512_000,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 512_000 },
+ };
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 500);
+
+ // Check we receive no anti-probing penalty for htlc_maximum_msat == channel_capacity/2 - 1.
+ let usage = ChannelUsage {
+ amount_msat: 512_000,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 511_999 },
+ };
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 0);
+ }
+
+ #[test]
+ fn scores_with_blinded_path() {
+ // Make sure we'll account for a blinded path's final_value_msat in scoring
+ let logger = TestLogger::new();
+ let network_graph = network_graph(&logger);
+ let params = ProbabilisticScoringFeeParameters {
+ liquidity_penalty_multiplier_msat: 1_000,
+ ..ProbabilisticScoringFeeParameters::zero_penalty()
+ };
+ let decay_params = ProbabilisticScoringDecayParameters::default();
+ let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
+ let source = source_node_id();
+ let target = target_node_id();
+ let usage = ChannelUsage {
+ amount_msat: 512,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: 1_000 },
+ };
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 300);
+
+ let mut path = payment_path_for_amount(768);
+ let recipient_hop = path.hops.pop().unwrap();
+ let blinded_path = BlindedPath {
+ introduction_node_id: path.hops.last().as_ref().unwrap().pubkey,
+ blinding_point: test_utils::pubkey(42),
+ blinded_hops: vec![
+ BlindedHop { blinded_node_id: test_utils::pubkey(44), encrypted_payload: Vec::new() }
+ ],
+ };
+ path.blinded_tail = Some(BlindedTail {
+ hops: blinded_path.blinded_hops,
+ blinding_point: blinded_path.blinding_point,
+ excess_final_cltv_expiry_delta: recipient_hop.cltv_expiry_delta,
+ final_value_msat: recipient_hop.fee_msat,
+ });
+
+ // Check the liquidity before and after scoring payment failures to ensure the blinded path's
+ // final value is taken into account.
+ assert!(scorer.channel_liquidities.get(&42).is_none());
+
+ scorer.payment_path_failed(&path, 42);
+ path.blinded_tail.as_mut().unwrap().final_value_msat = 256;
+ scorer.payment_path_failed(&path, 43);
+
+ let liquidity = scorer.channel_liquidities.get(&42).unwrap()
+ .as_directed(&source, &target, 1_000, decay_params);
+ assert_eq!(liquidity.min_liquidity_msat(), 256);
+ assert_eq!(liquidity.max_liquidity_msat(), 768);
+ }
+
+ #[test]
+ fn realistic_historical_failures() {
+ // The motivation for the unequal sized buckets came largely from attempting to pay 10k
+ // sats over a one bitcoin channel. This tests that case explicitly, ensuring that we score
+ // properly.
+ let logger = TestLogger::new();
+ let mut network_graph = network_graph(&logger);
+ let params = ProbabilisticScoringFeeParameters {
+ historical_liquidity_penalty_multiplier_msat: 1024,
+ historical_liquidity_penalty_amount_multiplier_msat: 1024,
+ ..ProbabilisticScoringFeeParameters::zero_penalty()
+ };
+ let decay_params = ProbabilisticScoringDecayParameters {
+ liquidity_offset_half_life: Duration::from_secs(60 * 60),
+ historical_no_updates_half_life: Duration::from_secs(10),
+ ..ProbabilisticScoringDecayParameters::default()
+ };
+
+ let capacity_msat = 100_000_000_000;
+ update_channel(&mut network_graph, 42, source_privkey(), 0, capacity_msat, 200);
+ update_channel(&mut network_graph, 42, target_privkey(), 1, capacity_msat, 200);
+
+ let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger);
+ let source = source_node_id();
+ let target = target_node_id();
+
+ let mut amount_msat = 10_000_000;
+ let usage = ChannelUsage {
+ amount_msat,
+ inflight_htlc_msat: 0,
+ effective_capacity: EffectiveCapacity::Total { capacity_msat, htlc_maximum_msat: capacity_msat },
+ };
+ // With no historical data the normal liquidity penalty calculation is used, which results
+ // in a success probability of ~75%.
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms), 1269);
+ assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
+ None);
+ assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 42, ¶ms),
+ None);
+
+ // Fail to pay once, and then check the buckets and penalty.
+ scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42);
+ // The penalty should be the maximum penalty, as the payment we're scoring is now in the
+ // same bucket which is the only maximum datapoint.
+ assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage, ¶ms),
+ 2048 + 2048 * amount_msat / super::AMOUNT_PENALTY_DIVISOR);
+ // The "it failed" increment is 32, which we should apply to the first upper-bound (between
+ // 6k sats and 12k sats).
+ assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
+ Some(([32, 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],
+ [0, 32, 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])));
+ // The success probability estimate itself should be zero.
+ assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms),
+ Some(0.0));
+
+ // Now test again with the amount in the bottom bucket.
+ amount_msat /= 2;
+ // The new amount is entirely within the only minimum bucket with score, so the probability
+ // we assign is 1/2.
+ assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms),
+ Some(0.5));
+
+ // ...but once we see a failure, we consider the payment to be substantially less likely,
+ // even though not a probability of zero as we still look at the second max bucket which
+ // now shows 31.
+ scorer.payment_path_failed(&payment_path_for_amount(amount_msat), 42);
+ 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_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms),
+ Some(0.0));