Skip to content

Commit 6852ea9

Browse files
committed
Calculate a new penalty based on historical channel liquidity range
Our current `ProbabilisticScorer` attempts to build a model of the current liquidity across the payment channel network. This works fine to ignore channels we *just* tried to pay through, but it fails to remember patterns over longer time horizons. Specifically, there are *many* channels within the network that are very often either fully saturated in one direction, or are regularly rebalanced and rarely saturated. While our model may discover that, when it decays its offsets or if there is a temporary change in liquidity, it effectively forgets the "normal" state of the channel. This causes substantially suboptimal routing in practice, and avoiding discarding older knowledge when new datapoints come in is a potential solution to this. Here, we implement one such design, using the decaying buckets added in the previous commit to calculate a probability of payment success based on a weighted average of recent liquidity estimates for a channel. For each min/max liquidity bucket pair (where the min liquidity is less than the max liquidity), we can calculate the probability that a payment succeeds using our traditional `amount / capacity` formula. From there, we weigh the probability by the number of points in each bucket pair, calculating a total probability for the payment, and assigning a penalty using the same log-probability calculation used for the non-historical penalties.
1 parent c5eab6e commit 6852ea9

File tree

1 file changed

+170
-24
lines changed

1 file changed

+170
-24
lines changed

lightning/src/routing/scoring.rs

Lines changed: 170 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,8 @@ pub struct ProbabilisticScoringParameters {
350350
pub base_penalty_amount_multiplier_msat: u64,
351351

352352
/// A multiplier used in conjunction with the negative `log10` of the channel's success
353-
/// probability for a payment to determine the liquidity penalty.
353+
/// probability for a payment, as determined by our latest estimates of the channel's
354+
/// liquidity, to determine the liquidity penalty.
354355
///
355356
/// The penalty is based in part on the knowledge learned from prior successful and unsuccessful
356357
/// payments. This knowledge is decayed over time based on [`liquidity_offset_half_life`]. The
@@ -359,7 +360,7 @@ pub struct ProbabilisticScoringParameters {
359360
/// uncertainty bounds of the channel liquidity balance. Amounts above the upper bound will
360361
/// result in a `u64::max_value` penalty, however.
361362
///
362-
/// Default value: 40,000 msat
363+
/// Default value: 30,000 msat
363364
///
364365
/// [`liquidity_offset_half_life`]: Self::liquidity_offset_half_life
365366
pub liquidity_penalty_multiplier_msat: u64,
@@ -380,7 +381,8 @@ pub struct ProbabilisticScoringParameters {
380381
pub liquidity_offset_half_life: Duration,
381382

382383
/// A multiplier used in conjunction with a payment amount and the negative `log10` of the
383-
/// channel's success probability for the payment to determine the amount penalty.
384+
/// channel's success probability for the payment, as determined by our latest estimates of the
385+
/// channel's liquidity, to determine the amount penalty.
384386
///
385387
/// The purpose of the amount penalty is to avoid having fees dominate the channel cost (i.e.,
386388
/// fees plus penalty) for large payments. The penalty is computed as the product of this
@@ -395,9 +397,45 @@ pub struct ProbabilisticScoringParameters {
395397
/// probabilities, the multiplier will have a decreasing effect as the negative `log10` will
396398
/// fall below `1`.
397399
///
398-
/// Default value: 256 msat
400+
/// Default value: 192 msat
399401
pub liquidity_penalty_amount_multiplier_msat: u64,
400402

403+
/// A multiplier used in conjunction with the negative `log10` of the channel's success
404+
/// probability for the payment, as determined based on the history of our estimates of the
405+
/// channel's available liquidity, to determine a penalty.
406+
///
407+
/// This penalty is similar to [`liquidity_penalty_multiplier_msat`], however, instead of using
408+
/// only our latest estimate for the current liquidity available in the channel, it estimates
409+
/// success probability based on the estimated liquidity available in the channel through
410+
/// history. Specifically, every time we update our liquidity bounds on a given channel, we
411+
/// track which of several buckets those bounds fall into, exponentially decaying the
412+
/// probability of each bucket as new samples are added.
413+
///
414+
/// Default value: 10,000 msat
415+
///
416+
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
417+
pub historical_liquidity_penalty_multiplier_msat: u64,
418+
419+
/// A multiplier used in conjunction with the payment amount and the negative `log10` of the
420+
/// channel's success probability for the payment, as determined based on the history of our
421+
/// estimates of the channel's available liquidity, to determine a penalty.
422+
///
423+
/// The purpose of the amount penalty is to avoid having fees dominate the channel cost for
424+
/// large payments. The penalty is computed as the product of this multiplier and the `2^20`ths
425+
/// of the payment amount, weighted by the negative `log10` of the success probability.
426+
///
427+
/// This penalty is similar to [`liquidity_penalty_amount_multiplier_msat`], however, instead
428+
/// of using only our latest estimate for the current liquidity available in the channel, it
429+
/// estimates success probability based on the estimated liquidity available in the channel
430+
/// through history. Specifically, every time we update our liquidity bounds on a given
431+
/// channel, we track which of several buckets those bounds fall into, exponentially decaying
432+
/// the probability of each bucket as new samples are added.
433+
///
434+
/// Default value: 64 msat
435+
///
436+
/// [`liquidity_penalty_amount_multiplier_msat`]: Self::liquidity_penalty_amount_multiplier_msat
437+
pub historical_liquidity_penalty_amount_multiplier_msat: u64,
438+
401439
/// Manual penalties used for the given nodes. Allows to set a particular penalty for a given
402440
/// node. Note that a manual penalty of `u64::max_value()` means the node would not ever be
403441
/// considered during path finding.
@@ -605,6 +643,8 @@ impl ProbabilisticScoringParameters {
605643
liquidity_penalty_multiplier_msat: 0,
606644
liquidity_offset_half_life: Duration::from_secs(3600),
607645
liquidity_penalty_amount_multiplier_msat: 0,
646+
historical_liquidity_penalty_multiplier_msat: 0,
647+
historical_liquidity_penalty_amount_multiplier_msat: 0,
608648
manual_node_penalties: HashMap::new(),
609649
anti_probing_penalty_msat: 0,
610650
considered_impossible_penalty_msat: 0,
@@ -625,9 +665,11 @@ impl Default for ProbabilisticScoringParameters {
625665
Self {
626666
base_penalty_msat: 500,
627667
base_penalty_amount_multiplier_msat: 8192,
628-
liquidity_penalty_multiplier_msat: 40_000,
668+
liquidity_penalty_multiplier_msat: 30_000,
629669
liquidity_offset_half_life: Duration::from_secs(3600),
630-
liquidity_penalty_amount_multiplier_msat: 256,
670+
liquidity_penalty_amount_multiplier_msat: 192,
671+
historical_liquidity_penalty_multiplier_msat: 10_000,
672+
historical_liquidity_penalty_amount_multiplier_msat: 64,
631673
manual_node_penalties: HashMap::new(),
632674
anti_probing_penalty_msat: 250,
633675
considered_impossible_penalty_msat: 1_0000_0000_000,
@@ -718,14 +760,17 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
718760
fn penalty_msat(&self, amount_msat: u64, params: &ProbabilisticScoringParameters) -> u64 {
719761
let max_liquidity_msat = self.max_liquidity_msat();
720762
let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat);
721-
if amount_msat <= min_liquidity_msat {
763+
764+
let mut res = if amount_msat <= min_liquidity_msat {
722765
0
723766
} else if amount_msat >= max_liquidity_msat {
724767
// Equivalent to hitting the else clause below with the amount equal to the effective
725768
// capacity and without any certainty on the liquidity upper bound, plus the
726769
// impossibility penalty.
727770
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
728-
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
771+
Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
772+
params.liquidity_penalty_multiplier_msat,
773+
params.liquidity_penalty_amount_multiplier_msat)
729774
.saturating_add(params.considered_impossible_penalty_msat)
730775
} else {
731776
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
@@ -738,25 +783,96 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
738783
} else {
739784
let negative_log10_times_2048 =
740785
approx::negative_log10_times_2048(numerator, denominator);
741-
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
786+
Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
787+
params.liquidity_penalty_multiplier_msat,
788+
params.liquidity_penalty_amount_multiplier_msat)
789+
}
790+
};
791+
792+
if params.historical_liquidity_penalty_multiplier_msat != 0 ||
793+
params.historical_liquidity_penalty_amount_multiplier_msat != 0 {
794+
// If historical penalties are enabled, calculate the penalty by walking the set of
795+
// historical liquidity bucket (min, max) combinations (where min_idx < max_idx)
796+
// and, for each, calculate the probability of success given our payment amount, then
797+
// total the weighted average probability of success.
798+
//
799+
// We use a sliding scale to decide which point within a given bucket will be compared
800+
// to the amount being sent - for lower-bounds, the amount being sent is compared to
801+
// the lower edge of the first bucket (i.e. zero), but compared to the upper 7/8ths of
802+
// the last bucket (i.e. 9 times the index, or 63), with each bucket in between
803+
// increasing the comparison point by 1/64th. For upper-bounds, the same applies,
804+
// however with an offset of 1/64th (i.e. starting at one and ending at 64). This
805+
// avoids failing to assign penalties to channels at the edges.
806+
//
807+
// If we used the bottom edge of buckets, we'd end up never assigning any penalty at
808+
// all to such a channel when sending less than ~0.19% of the channel's capacity (e.g.
809+
// ~200k sats for a 1 BTC channel!).
810+
//
811+
// If we used the middle of each bucket we'd never assign any penalty at all when
812+
// sending less than 1/16th of a channel's capacity, or 1/8th if we used the top of the
813+
// bucket.
814+
let mut total_valid_points_tracked = 0;
815+
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
816+
for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(8 - min_idx) {
817+
total_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
818+
}
819+
}
820+
if total_valid_points_tracked == 0 {
821+
// If we don't have any valid points, redo the non-historical calculation with no
822+
// liquidity bounds tracked and the historical penalty multipliers.
823+
let max_capacity = self.capacity_msat.saturating_sub(amount_msat).saturating_add(1);
824+
let negative_log10_times_2048 =
825+
approx::negative_log10_times_2048(max_capacity, self.capacity_msat.saturating_add(1));
826+
res = res.saturating_add(Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
827+
params.historical_liquidity_penalty_multiplier_msat,
828+
params.historical_liquidity_penalty_amount_multiplier_msat));
829+
return res;
742830
}
831+
832+
let payment_amt_64th_bucket = amount_msat * 64 / self.capacity_msat;
833+
debug_assert!(payment_amt_64th_bucket <= 64);
834+
if payment_amt_64th_bucket > 64 { return res; }
835+
836+
let mut cumulative_success_prob_times_billion = 0;
837+
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
838+
for (max_idx, max_bucket) in self.max_liquidity_offset_history.buckets.iter().enumerate().take(8 - min_idx) {
839+
let bucket_prob_times_million = (*min_bucket as u64) * (*max_bucket as u64)
840+
* 1024 * 1024 / total_valid_points_tracked;
841+
let min_64th_bucket = min_idx as u64 * 9;
842+
let max_64th_bucket = (7 - max_idx as u64) * 9 + 1;
843+
if payment_amt_64th_bucket > max_64th_bucket {
844+
// Success probability 0, the payment amount is above the max liquidity
845+
} else if payment_amt_64th_bucket <= min_64th_bucket {
846+
cumulative_success_prob_times_billion += bucket_prob_times_million * 1024;
847+
} else {
848+
cumulative_success_prob_times_billion += bucket_prob_times_million *
849+
(max_64th_bucket - payment_amt_64th_bucket) * 1024 /
850+
(max_64th_bucket - min_64th_bucket);
851+
}
852+
}
853+
}
854+
let historical_negative_log10_times_2048 = approx::negative_log10_times_2048(cumulative_success_prob_times_billion + 1, 1024 * 1024 * 1024);
855+
res = res.saturating_add(Self::combined_penalty_msat(amount_msat,
856+
historical_negative_log10_times_2048, params.historical_liquidity_penalty_multiplier_msat,
857+
params.historical_liquidity_penalty_amount_multiplier_msat));
743858
}
859+
860+
res
744861
}
745862

746863
/// Computes the liquidity penalty from the penalty multipliers.
747864
#[inline(always)]
748-
fn combined_penalty_msat(
749-
&self, amount_msat: u64, negative_log10_times_2048: u64,
750-
params: &ProbabilisticScoringParameters
865+
fn combined_penalty_msat(amount_msat: u64, negative_log10_times_2048: u64,
866+
liquidity_penalty_multiplier_msat: u64, liquidity_penalty_amount_multiplier_msat: u64,
751867
) -> u64 {
752868
let liquidity_penalty_msat = {
753869
// Upper bound the liquidity penalty to ensure some channel is selected.
754-
let multiplier_msat = params.liquidity_penalty_multiplier_msat;
870+
let multiplier_msat = liquidity_penalty_multiplier_msat;
755871
let max_penalty_msat = multiplier_msat.saturating_mul(NEGATIVE_LOG10_UPPER_BOUND);
756872
(negative_log10_times_2048.saturating_mul(multiplier_msat) / 2048).min(max_penalty_msat)
757873
};
758874
let amount_penalty_msat = negative_log10_times_2048
759-
.saturating_mul(params.liquidity_penalty_amount_multiplier_msat)
875+
.saturating_mul(liquidity_penalty_amount_multiplier_msat)
760876
.saturating_mul(amount_msat) / 2048 / AMOUNT_PENALTY_DIVISOR;
761877

762878
liquidity_penalty_msat.saturating_add(amount_penalty_msat)
@@ -2199,35 +2315,35 @@ mod tests {
21992315
let usage = ChannelUsage {
22002316
effective_capacity: EffectiveCapacity::Total { capacity_msat: 3_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22012317
};
2202-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1985);
2318+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1983);
22032319
let usage = ChannelUsage {
22042320
effective_capacity: EffectiveCapacity::Total { capacity_msat: 4_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22052321
};
2206-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1639);
2322+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1637);
22072323
let usage = ChannelUsage {
22082324
effective_capacity: EffectiveCapacity::Total { capacity_msat: 5_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22092325
};
2210-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1607);
2326+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1606);
22112327
let usage = ChannelUsage {
22122328
effective_capacity: EffectiveCapacity::Total { capacity_msat: 6_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22132329
};
2214-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2330+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1331);
22152331
let usage = ChannelUsage {
22162332
effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_450_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22172333
};
2218-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2334+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1387);
22192335
let usage = ChannelUsage {
22202336
effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22212337
};
2222-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2338+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1379);
22232339
let usage = ChannelUsage {
22242340
effective_capacity: EffectiveCapacity::Total { capacity_msat: 8_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22252341
};
2226-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2342+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1363);
22272343
let usage = ChannelUsage {
22282344
effective_capacity: EffectiveCapacity::Total { capacity_msat: 9_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22292345
};
2230-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2346+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1355);
22312347
}
22322348

22332349
#[test]
@@ -2251,15 +2367,15 @@ mod tests {
22512367

22522368
let params = ProbabilisticScoringParameters {
22532369
base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
2254-
anti_probing_penalty_msat: 0, ..Default::default()
2370+
anti_probing_penalty_msat: 0, ..ProbabilisticScoringParameters::zero_penalty()
22552371
};
22562372
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
22572373
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 558);
22582374

22592375
let params = ProbabilisticScoringParameters {
22602376
base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
22612377
base_penalty_amount_multiplier_msat: (1 << 30),
2262-
anti_probing_penalty_msat: 0, ..Default::default()
2378+
anti_probing_penalty_msat: 0, ..ProbabilisticScoringParameters::zero_penalty()
22632379
};
22642380

22652381
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
@@ -2362,6 +2478,36 @@ mod tests {
23622478
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
23632479
}
23642480

2481+
#[test]
2482+
fn remembers_historical_failures() {
2483+
let logger = TestLogger::new();
2484+
let network_graph = network_graph(&logger);
2485+
let params = ProbabilisticScoringParameters {
2486+
historical_liquidity_penalty_multiplier_msat: 1024,
2487+
historical_liquidity_penalty_amount_multiplier_msat: 1024,
2488+
..ProbabilisticScoringParameters::zero_penalty()
2489+
};
2490+
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
2491+
let source = source_node_id();
2492+
let target = target_node_id();
2493+
2494+
let usage = ChannelUsage {
2495+
amount_msat: 100,
2496+
inflight_htlc_msat: 0,
2497+
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
2498+
};
2499+
// With no historical data the normal liquidity penalty calculation is used.
2500+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
2501+
2502+
scorer.payment_path_failed(&payment_path_for_amount(1).iter().collect::<Vec<_>>(), 42);
2503+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2048);
2504+
2505+
// Even after we tell the scorer we definitely have enough available liquidity, it will
2506+
// still remember that there was some failure in the past, and assign a non-0 penalty.
2507+
scorer.payment_path_failed(&payment_path_for_amount(1000).iter().collect::<Vec<_>>(), 43);
2508+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 198);
2509+
}
2510+
23652511
#[test]
23662512
fn adds_anti_probing_penalty() {
23672513
let logger = TestLogger::new();

0 commit comments

Comments
 (0)