Skip to content

Commit 8ecabfb

Browse files
committed
Calculate a new penalty based on historical channel liquidity range
XXX: Write some text here
1 parent 1d20ab7 commit 8ecabfb

File tree

1 file changed

+161
-24
lines changed

1 file changed

+161
-24
lines changed

lightning/src/routing/scoring.rs

Lines changed: 161 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,38 @@ 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+
/// Default value: 64 msat
428+
///
429+
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
430+
pub historical_liquidity_penalty_amount_multiplier_msat: u64,
431+
401432
/// Manual penalties used for the given nodes. Allows to set a particular penalty for a given
402433
/// node. Note that a manual penalty of `u64::max_value()` means the node would not ever be
403434
/// considered during path finding.
@@ -605,6 +636,8 @@ impl ProbabilisticScoringParameters {
605636
liquidity_penalty_multiplier_msat: 0,
606637
liquidity_offset_half_life: Duration::from_secs(3600),
607638
liquidity_penalty_amount_multiplier_msat: 0,
639+
historical_liquidity_penalty_multiplier_msat: 0,
640+
historical_liquidity_penalty_amount_multiplier_msat: 0,
608641
manual_node_penalties: HashMap::new(),
609642
anti_probing_penalty_msat: 0,
610643
considered_impossible_penalty_msat: 0,
@@ -625,9 +658,11 @@ impl Default for ProbabilisticScoringParameters {
625658
Self {
626659
base_penalty_msat: 500,
627660
base_penalty_amount_multiplier_msat: 8192,
628-
liquidity_penalty_multiplier_msat: 40_000,
661+
liquidity_penalty_multiplier_msat: 30_000,
629662
liquidity_offset_half_life: Duration::from_secs(3600),
630-
liquidity_penalty_amount_multiplier_msat: 256,
663+
liquidity_penalty_amount_multiplier_msat: 192,
664+
historical_liquidity_penalty_multiplier_msat: 10_000,
665+
historical_liquidity_penalty_amount_multiplier_msat: 64,
631666
manual_node_penalties: HashMap::new(),
632667
anti_probing_penalty_msat: 250,
633668
considered_impossible_penalty_msat: 1_0000_0000_000,
@@ -718,14 +753,17 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
718753
fn penalty_msat(&self, amount_msat: u64, params: &ProbabilisticScoringParameters) -> u64 {
719754
let max_liquidity_msat = self.max_liquidity_msat();
720755
let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat);
721-
if amount_msat <= min_liquidity_msat {
756+
757+
let mut res = if amount_msat <= min_liquidity_msat {
722758
0
723759
} else if amount_msat >= max_liquidity_msat {
724760
// Equivalent to hitting the else clause below with the amount equal to the effective
725761
// capacity and without any certainty on the liquidity upper bound, plus the
726762
// impossibility penalty.
727763
let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048;
728-
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
764+
Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
765+
params.liquidity_penalty_multiplier_msat,
766+
params.liquidity_penalty_amount_multiplier_msat)
729767
.saturating_add(params.considered_impossible_penalty_msat)
730768
} else {
731769
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
@@ -738,25 +776,94 @@ impl<L: Deref<Target = u64>, BRT: Deref<Target = HistoricalBucketRangeTracker>,
738776
} else {
739777
let negative_log10_times_2048 =
740778
approx::negative_log10_times_2048(numerator, denominator);
741-
self.combined_penalty_msat(amount_msat, negative_log10_times_2048, params)
779+
Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
780+
params.liquidity_penalty_multiplier_msat,
781+
params.liquidity_penalty_amount_multiplier_msat)
782+
}
783+
};
784+
785+
if params.historical_liquidity_penalty_multiplier_msat != 0 ||
786+
params.historical_liquidity_penalty_amount_multiplier_msat != 0 {
787+
// If historical penalties are enabled, calculate the penalty by walking the set of
788+
// historical liquidity bucket (min, max) combinations (where min_idx < max_idx)
789+
// and, for each, calculate the probability of success given our payment amount, then
790+
// total the weighted average probability of success.
791+
//
792+
// We use a sliding scale to decide which point within a given bucket will be compared
793+
// to the amount being sent - the amount being sent is compared to the lower-bound of
794+
// the first bucket (i.e. zero), but compared to the upper-bound of the last bucket
795+
// (i.e. 9 times the index), with each bucket in between increasing the comparison
796+
// point by 1/64th. This avoids failing to assign penalties to channels at the edges.
797+
//
798+
// If we used the bottom edge of buckets, we'd end up never assigning any penalty at
799+
// all to such a channel when sending less than ~0.19% of the channel's capacity (e.g.
800+
// ~200k sats for a 1 BTC channel!).
801+
//
802+
// If we used the middle of each bucket we'd never assign any penalty at all when
803+
// sending less than 1/16th of a channel's capacity, or 1/8th if we used the top of the
804+
// bucket.
805+
let mut total_valid_points_tracked = 0;
806+
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
807+
for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(8 - min_idx) {
808+
total_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
809+
}
810+
}
811+
if total_valid_points_tracked == 0 {
812+
// If we don't have any valid points, redo the non-historical calculation with no
813+
// liquidity bounds tracked and the historical penalty multipliers.
814+
let max_capacity = self.capacity_msat.saturating_sub(amount_msat).saturating_add(1);
815+
let negative_log10_times_2048 =
816+
approx::negative_log10_times_2048(max_capacity, self.capacity_msat.saturating_add(1));
817+
res = res.saturating_add(Self::combined_penalty_msat(amount_msat, negative_log10_times_2048,
818+
params.historical_liquidity_penalty_multiplier_msat,
819+
params.historical_liquidity_penalty_amount_multiplier_msat));
820+
return res;
821+
}
822+
823+
let payment_amt_64th_bucket = amount_msat * 64 / self.capacity_msat;
824+
debug_assert!(payment_amt_64th_bucket <= 64);
825+
if payment_amt_64th_bucket > 64 { return res; }
826+
827+
let mut cumulative_success_prob_billions = 0;
828+
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
829+
for (max_idx, max_bucket) in self.max_liquidity_offset_history.buckets.iter().enumerate().take(8 - min_idx) {
830+
let bucket_prob_millions = (*min_bucket as u64) * (*max_bucket as u64)
831+
* 1024 * 1024 / total_valid_points_tracked;
832+
let min_64th_bucket = min_idx as u64 * 9;
833+
let max_64th_bucket = (7 - max_idx as u64) * 9 + 1;
834+
if payment_amt_64th_bucket > max_64th_bucket {
835+
// Success probability 0, the payment amount is above the max liquidity
836+
} else if payment_amt_64th_bucket <= min_64th_bucket {
837+
cumulative_success_prob_billions += bucket_prob_millions * 1024;
838+
} else {
839+
cumulative_success_prob_billions += bucket_prob_millions *
840+
(max_64th_bucket - payment_amt_64th_bucket) * 1024 /
841+
(max_64th_bucket - min_64th_bucket);
842+
}
843+
}
742844
}
845+
let historical_negative_log10_times_2048 = approx::negative_log10_times_2048(cumulative_success_prob_billions + 1, 1024 * 1024 * 1024);
846+
res = res.saturating_add(Self::combined_penalty_msat(amount_msat,
847+
historical_negative_log10_times_2048, params.historical_liquidity_penalty_multiplier_msat,
848+
params.historical_liquidity_penalty_amount_multiplier_msat));
743849
}
850+
851+
res
744852
}
745853

746854
/// Computes the liquidity penalty from the penalty multipliers.
747855
#[inline(always)]
748-
fn combined_penalty_msat(
749-
&self, amount_msat: u64, negative_log10_times_2048: u64,
750-
params: &ProbabilisticScoringParameters
856+
fn combined_penalty_msat(amount_msat: u64, negative_log10_times_2048: u64,
857+
liquidity_penalty_multiplier_msat: u64, liquidity_penalty_amount_multiplier_msat: u64,
751858
) -> u64 {
752859
let liquidity_penalty_msat = {
753860
// Upper bound the liquidity penalty to ensure some channel is selected.
754-
let multiplier_msat = params.liquidity_penalty_multiplier_msat;
861+
let multiplier_msat = liquidity_penalty_multiplier_msat;
755862
let max_penalty_msat = multiplier_msat.saturating_mul(NEGATIVE_LOG10_UPPER_BOUND);
756863
(negative_log10_times_2048.saturating_mul(multiplier_msat) / 2048).min(max_penalty_msat)
757864
};
758865
let amount_penalty_msat = negative_log10_times_2048
759-
.saturating_mul(params.liquidity_penalty_amount_multiplier_msat)
866+
.saturating_mul(liquidity_penalty_amount_multiplier_msat)
760867
.saturating_mul(amount_msat) / 2048 / AMOUNT_PENALTY_DIVISOR;
761868

762869
liquidity_penalty_msat.saturating_add(amount_penalty_msat)
@@ -2195,35 +2302,35 @@ mod tests {
21952302
let usage = ChannelUsage {
21962303
effective_capacity: EffectiveCapacity::Total { capacity_msat: 3_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
21972304
};
2198-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1985);
2305+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1983);
21992306
let usage = ChannelUsage {
22002307
effective_capacity: EffectiveCapacity::Total { capacity_msat: 4_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22012308
};
2202-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1639);
2309+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1637);
22032310
let usage = ChannelUsage {
22042311
effective_capacity: EffectiveCapacity::Total { capacity_msat: 5_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22052312
};
2206-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1607);
2313+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1606);
22072314
let usage = ChannelUsage {
22082315
effective_capacity: EffectiveCapacity::Total { capacity_msat: 6_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22092316
};
2210-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2317+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1331);
22112318
let usage = ChannelUsage {
22122319
effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_450_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22132320
};
2214-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2321+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1387);
22152322
let usage = ChannelUsage {
22162323
effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22172324
};
2218-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2325+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1379);
22192326
let usage = ChannelUsage {
22202327
effective_capacity: EffectiveCapacity::Total { capacity_msat: 8_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22212328
};
2222-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2329+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1363);
22232330
let usage = ChannelUsage {
22242331
effective_capacity: EffectiveCapacity::Total { capacity_msat: 9_950_000_000, htlc_maximum_msat: Some(1_000) }, ..usage
22252332
};
2226-
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1262);
2333+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 1355);
22272334
}
22282335

22292336
#[test]
@@ -2247,15 +2354,15 @@ mod tests {
22472354

22482355
let params = ProbabilisticScoringParameters {
22492356
base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
2250-
anti_probing_penalty_msat: 0, ..Default::default()
2357+
anti_probing_penalty_msat: 0, ..ProbabilisticScoringParameters::zero_penalty()
22512358
};
22522359
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
22532360
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 558);
22542361

22552362
let params = ProbabilisticScoringParameters {
22562363
base_penalty_msat: 500, liquidity_penalty_multiplier_msat: 1_000,
22572364
base_penalty_amount_multiplier_msat: (1 << 30),
2258-
anti_probing_penalty_msat: 0, ..Default::default()
2365+
anti_probing_penalty_msat: 0, ..ProbabilisticScoringParameters::zero_penalty()
22592366
};
22602367

22612368
let scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
@@ -2358,6 +2465,36 @@ mod tests {
23582465
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), u64::max_value());
23592466
}
23602467

2468+
#[test]
2469+
fn remembers_historical_failures() {
2470+
let logger = TestLogger::new();
2471+
let network_graph = network_graph(&logger);
2472+
let params = ProbabilisticScoringParameters {
2473+
historical_liquidity_penalty_multiplier_msat: 1024,
2474+
historical_liquidity_penalty_amount_multiplier_msat: 1024,
2475+
..ProbabilisticScoringParameters::zero_penalty()
2476+
};
2477+
let mut scorer = ProbabilisticScorer::new(params, &network_graph, &logger);
2478+
let source = source_node_id();
2479+
let target = target_node_id();
2480+
2481+
let usage = ChannelUsage {
2482+
amount_msat: 100,
2483+
inflight_htlc_msat: 0,
2484+
effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024, htlc_maximum_msat: Some(1_024) },
2485+
};
2486+
// With no historical data the normal liquidity penalty calculation is used.
2487+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 47);
2488+
2489+
scorer.payment_path_failed(&payment_path_for_amount(1).iter().collect::<Vec<_>>(), 42);
2490+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 2048);
2491+
2492+
// Even after we tell the scorer we definitely have enough available liquidity, it will
2493+
// still remember that there was some failure in the past, and assign a non-0 penalty.
2494+
scorer.payment_path_failed(&payment_path_for_amount(1000).iter().collect::<Vec<_>>(), 43);
2495+
assert_eq!(scorer.channel_penalty_msat(42, &source, &target, usage), 198);
2496+
}
2497+
23612498
#[test]
23622499
fn adds_anti_probing_penalty() {
23632500
let logger = TestLogger::new();

0 commit comments

Comments
 (0)