Skip to content

Commit af31831

Browse files
authored
Merge pull request #1399 from jkczyz/2022-03-scoring-tweaks
ProbabilisticScorer improvements
2 parents 6b8ad4e + 5425b2b commit af31831

File tree

1 file changed

+137
-37
lines changed

1 file changed

+137
-37
lines changed

lightning/src/routing/scoring.rs

+137-37
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ pub struct ProbabilisticScorerUsingTime<G: Deref<Target = NetworkGraph>, T: Time
517517

518518
/// Parameters for configuring [`ProbabilisticScorer`].
519519
///
520-
/// Used to configure a base penalty and a liquidity penalty, the sum of which is the channel
520+
/// Used to configure base, liquidity, and amount penalties, the sum of which comprises the channel
521521
/// penalty (i.e., the amount in msats willing to be paid to avoid routing through the channel).
522522
#[derive(Clone, Copy)]
523523
pub struct ProbabilisticScoringParameters {
@@ -529,9 +529,12 @@ pub struct ProbabilisticScoringParameters {
529529
/// A multiplier used in conjunction with the negative `log10` of the channel's success
530530
/// probability for a payment to determine the liquidity penalty.
531531
///
532-
/// The penalty is based in part by the knowledge learned from prior successful and unsuccessful
532+
/// The penalty is based in part on the knowledge learned from prior successful and unsuccessful
533533
/// payments. This knowledge is decayed over time based on [`liquidity_offset_half_life`]. The
534-
/// penalty is effectively limited to `2 * liquidity_penalty_multiplier_msat`.
534+
/// penalty is effectively limited to `2 * liquidity_penalty_multiplier_msat` (corresponding to
535+
/// lower bounding the success probability to `0.01`) when the amount falls within the
536+
/// uncertainty bounds of the channel liquidity balance. Amounts above the upper bound will
537+
/// result in a `u64::max_value` penalty, however.
535538
///
536539
/// Default value: 40,000 msat
537540
///
@@ -552,6 +555,25 @@ pub struct ProbabilisticScoringParameters {
552555
/// When built with the `no-std` feature, time will never elapse. Therefore, the channel
553556
/// liquidity knowledge will never decay except when the bounds cross.
554557
pub liquidity_offset_half_life: Duration,
558+
559+
/// A multiplier used in conjunction with a payment amount and the negative `log10` of the
560+
/// channel's success probability for the payment to determine the amount penalty.
561+
///
562+
/// The purpose of the amount penalty is to avoid having fees dominate the channel cost (i.e.,
563+
/// fees plus penalty) for large payments. The penalty is computed as the product of this
564+
/// multiplier and `2^20`ths of the payment amount, weighted by the negative `log10` of the
565+
/// success probability.
566+
///
567+
/// `-log10(success_probability) * amount_penalty_multiplier_msat * amount_msat / 2^20`
568+
///
569+
/// In practice, this means for 0.1 success probability (`-log10(0.1) == 1`) each `2^20`th of
570+
/// the amount will result in a penalty of the multiplier. And, as the success probability
571+
/// decreases, the negative `log10` weighting will increase dramatically. For higher success
572+
/// probabilities, the multiplier will have a decreasing effect as the negative `log10` will
573+
/// fall below `1`.
574+
///
575+
/// Default value: 256 msat
576+
pub amount_penalty_multiplier_msat: u64,
555577
}
556578

557579
/// Accounting for channel liquidity balance uncertainty.
@@ -599,12 +621,25 @@ impl<G: Deref<Target = NetworkGraph>, T: Time> ProbabilisticScorerUsingTime<G, T
599621
}
600622
}
601623

624+
impl ProbabilisticScoringParameters {
625+
#[cfg(test)]
626+
fn zero_penalty() -> Self {
627+
Self {
628+
base_penalty_msat: 0,
629+
liquidity_penalty_multiplier_msat: 0,
630+
liquidity_offset_half_life: Duration::from_secs(3600),
631+
amount_penalty_multiplier_msat: 0,
632+
}
633+
}
634+
}
635+
602636
impl Default for ProbabilisticScoringParameters {
603637
fn default() -> Self {
604638
Self {
605639
base_penalty_msat: 500,
606640
liquidity_penalty_multiplier_msat: 40_000,
607641
liquidity_offset_half_life: Duration::from_secs(3600),
642+
amount_penalty_multiplier_msat: 256,
608643
}
609644
}
610645
}
@@ -662,27 +697,63 @@ impl<T: Time> ChannelLiquidity<T> {
662697
}
663698
}
664699

700+
/// Bounds `-log10` to avoid excessive liquidity penalties for payments with low success
701+
/// probabilities.
702+
const NEGATIVE_LOG10_UPPER_BOUND: u64 = 2;
703+
704+
/// The divisor used when computing the amount penalty.
705+
const AMOUNT_PENALTY_DIVISOR: u64 = 1 << 20;
706+
665707
impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiquidity<L, T, U> {
666708
/// Returns a penalty for routing the given HTLC `amount_msat` through the channel in this
667709
/// direction.
668-
fn penalty_msat(&self, amount_msat: u64, liquidity_penalty_multiplier_msat: u64) -> u64 {
669-
let max_penalty_msat = liquidity_penalty_multiplier_msat.saturating_mul(2);
710+
fn penalty_msat(&self, amount_msat: u64, params: ProbabilisticScoringParameters) -> u64 {
670711
let max_liquidity_msat = self.max_liquidity_msat();
671712
let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat);
672-
if amount_msat > max_liquidity_msat {
673-
max_penalty_msat
674-
} else if amount_msat <= min_liquidity_msat {
713+
if amount_msat <= min_liquidity_msat {
675714
0
715+
} else if amount_msat >= max_liquidity_msat {
716+
if amount_msat > max_liquidity_msat {
717+
u64::max_value()
718+
} else if max_liquidity_msat != self.capacity_msat {
719+
// Avoid using the failed channel on retry.
720+
u64::max_value()
721+
} else {
722+
// Equivalent to hitting the else clause below with the amount equal to the
723+
// effective capacity and without any certainty on the liquidity upper bound.
724+
let negative_log10_times_1024 = NEGATIVE_LOG10_UPPER_BOUND * 1024;
725+
self.combined_penalty_msat(amount_msat, negative_log10_times_1024, params)
726+
}
676727
} else {
677728
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
678729
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
679-
let penalty_msat = approx::negative_log10_times_1024(numerator, denominator)
680-
.saturating_mul(liquidity_penalty_multiplier_msat) / 1024;
681-
// Upper bound the penalty to ensure some channel is selected.
682-
penalty_msat.min(max_penalty_msat)
730+
let negative_log10_times_1024 =
731+
approx::negative_log10_times_1024(numerator, denominator);
732+
self.combined_penalty_msat(amount_msat, negative_log10_times_1024, params)
683733
}
684734
}
685735

736+
/// Computes the liquidity and amount penalties and adds them to the base penalty.
737+
#[inline(always)]
738+
fn combined_penalty_msat(
739+
&self, amount_msat: u64, negative_log10_times_1024: u64,
740+
params: ProbabilisticScoringParameters
741+
) -> u64 {
742+
let liquidity_penalty_msat = {
743+
// Upper bound the liquidity penalty to ensure some channel is selected.
744+
let multiplier_msat = params.liquidity_penalty_multiplier_msat;
745+
let max_penalty_msat = multiplier_msat.saturating_mul(NEGATIVE_LOG10_UPPER_BOUND);
746+
(negative_log10_times_1024.saturating_mul(multiplier_msat) / 1024).min(max_penalty_msat)
747+
};
748+
let amount_penalty_msat = negative_log10_times_1024
749+
.saturating_mul(params.amount_penalty_multiplier_msat)
750+
.saturating_mul(amount_msat) / 1024 / AMOUNT_PENALTY_DIVISOR;
751+
752+
params.base_penalty_msat
753+
.saturating_add(liquidity_penalty_msat)
754+
.saturating_add(amount_penalty_msat)
755+
}
756+
686757
/// Returns the lower bound of the channel liquidity balance in this direction.
687758
fn min_liquidity_msat(&self) -> u64 {
688759
self.decayed_offset_msat(*self.min_liquidity_offset_msat)
@@ -752,14 +823,12 @@ impl<G: Deref<Target = NetworkGraph>, T: Time> Score for ProbabilisticScorerUsin
752823
&self, short_channel_id: u64, amount_msat: u64, capacity_msat: u64, source: &NodeId,
753824
target: &NodeId
754825
) -> u64 {
755-
let liquidity_penalty_multiplier_msat = self.params.liquidity_penalty_multiplier_msat;
756826
let liquidity_offset_half_life = self.params.liquidity_offset_half_life;
757827
self.channel_liquidities
758828
.get(&short_channel_id)
759829
.unwrap_or(&ChannelLiquidity::new())
760830
.as_directed(source, target, capacity_msat, liquidity_offset_half_life)
761-
.penalty_msat(amount_msat, liquidity_penalty_multiplier_msat)
762-
.saturating_add(self.params.base_penalty_msat)
831+
.penalty_msat(amount_msat, self.params)
763832
}
764833

765834
fn payment_path_failed(&mut self, path: &[&RouteHop], short_channel_id: u64) {
@@ -1737,7 +1806,8 @@ mod tests {
17371806
fn increased_penalty_nearing_liquidity_upper_bound() {
17381807
let network_graph = network_graph();
17391808
let params = ProbabilisticScoringParameters {
1740-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
1809+
liquidity_penalty_multiplier_msat: 1_000,
1810+
..ProbabilisticScoringParameters::zero_penalty()
17411811
};
17421812
let scorer = ProbabilisticScorer::new(params, &network_graph);
17431813
let source = source_node_id();
@@ -1762,7 +1832,8 @@ mod tests {
17621832
let last_updated = SinceEpoch::now();
17631833
let network_graph = network_graph();
17641834
let params = ProbabilisticScoringParameters {
1765-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
1835+
liquidity_penalty_multiplier_msat: 1_000,
1836+
..ProbabilisticScoringParameters::zero_penalty()
17661837
};
17671838
let scorer = ProbabilisticScorer::new(params, &network_graph)
17681839
.with_channel(42,
@@ -1774,15 +1845,16 @@ mod tests {
17741845

17751846
assert_eq!(scorer.channel_penalty_msat(42, 39, 100, &source, &target), 0);
17761847
assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), 0);
1777-
assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), 2_000);
1778-
assert_eq!(scorer.channel_penalty_msat(42, 61, 100, &source, &target), 2_000);
1848+
assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), u64::max_value());
1849+
assert_eq!(scorer.channel_penalty_msat(42, 61, 100, &source, &target), u64::max_value());
17791850
}
17801851

17811852
#[test]
17821853
fn does_not_further_penalize_own_channel() {
17831854
let network_graph = network_graph();
17841855
let params = ProbabilisticScoringParameters {
1785-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
1856+
liquidity_penalty_multiplier_msat: 1_000,
1857+
..ProbabilisticScoringParameters::zero_penalty()
17861858
};
17871859
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
17881860
let sender = sender_node_id();
@@ -1803,7 +1875,8 @@ mod tests {
18031875
fn sets_liquidity_lower_bound_on_downstream_failure() {
18041876
let network_graph = network_graph();
18051877
let params = ProbabilisticScoringParameters {
1806-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
1878+
liquidity_penalty_multiplier_msat: 1_000,
1879+
..ProbabilisticScoringParameters::zero_penalty()
18071880
};
18081881
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
18091882
let source = source_node_id();
@@ -1825,7 +1898,8 @@ mod tests {
18251898
fn sets_liquidity_upper_bound_on_failure() {
18261899
let network_graph = network_graph();
18271900
let params = ProbabilisticScoringParameters {
1828-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
1901+
liquidity_penalty_multiplier_msat: 1_000,
1902+
..ProbabilisticScoringParameters::zero_penalty()
18291903
};
18301904
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
18311905
let source = source_node_id();
@@ -1839,15 +1913,16 @@ mod tests {
18391913
scorer.payment_path_failed(&path.iter().collect::<Vec<_>>(), 42);
18401914

18411915
assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 300);
1842-
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
1843-
assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 2_000);
1916+
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), u64::max_value());
1917+
assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), u64::max_value());
18441918
}
18451919

18461920
#[test]
18471921
fn reduces_liquidity_upper_bound_along_path_on_success() {
18481922
let network_graph = network_graph();
18491923
let params = ProbabilisticScoringParameters {
1850-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
1924+
liquidity_penalty_multiplier_msat: 1_000,
1925+
..ProbabilisticScoringParameters::zero_penalty()
18511926
};
18521927
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
18531928
let sender = sender_node_id();
@@ -1871,9 +1946,9 @@ mod tests {
18711946
fn decays_liquidity_bounds_over_time() {
18721947
let network_graph = network_graph();
18731948
let params = ProbabilisticScoringParameters {
1874-
base_penalty_msat: 0,
18751949
liquidity_penalty_multiplier_msat: 1_000,
18761950
liquidity_offset_half_life: Duration::from_secs(10),
1951+
..ProbabilisticScoringParameters::zero_penalty()
18771952
};
18781953
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
18791954
let source = source_node_id();
@@ -1888,19 +1963,19 @@ mod tests {
18881963
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 0);
18891964
assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 97);
18901965
assert_eq!(scorer.channel_penalty_msat(42, 768, 1_024, &source, &target), 1_409);
1891-
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 2_000);
1966+
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), u64::max_value());
18921967

18931968
SinceEpoch::advance(Duration::from_secs(9));
18941969
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 0);
18951970
assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 97);
18961971
assert_eq!(scorer.channel_penalty_msat(42, 768, 1_024, &source, &target), 1_409);
1897-
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 2_000);
1972+
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), u64::max_value());
18981973

18991974
SinceEpoch::advance(Duration::from_secs(1));
19001975
assert_eq!(scorer.channel_penalty_msat(42, 64, 1_024, &source, &target), 0);
19011976
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 34);
19021977
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 1_773);
1903-
assert_eq!(scorer.channel_penalty_msat(42, 960, 1_024, &source, &target), 2_000);
1978+
assert_eq!(scorer.channel_penalty_msat(42, 960, 1_024, &source, &target), u64::max_value());
19041979

19051980
// Fully decay liquidity lower bound.
19061981
SinceEpoch::advance(Duration::from_secs(10 * 7));
@@ -1923,9 +1998,9 @@ mod tests {
19231998
fn decays_liquidity_bounds_without_shift_overflow() {
19241999
let network_graph = network_graph();
19252000
let params = ProbabilisticScoringParameters {
1926-
base_penalty_msat: 0,
19272001
liquidity_penalty_multiplier_msat: 1_000,
19282002
liquidity_offset_half_life: Duration::from_secs(10),
2003+
..ProbabilisticScoringParameters::zero_penalty()
19292004
};
19302005
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
19312006
let source = source_node_id();
@@ -1948,9 +2023,9 @@ mod tests {
19482023
fn restricts_liquidity_bounds_after_decay() {
19492024
let network_graph = network_graph();
19502025
let params = ProbabilisticScoringParameters {
1951-
base_penalty_msat: 0,
19522026
liquidity_penalty_multiplier_msat: 1_000,
19532027
liquidity_offset_half_life: Duration::from_secs(10),
2028+
..ProbabilisticScoringParameters::zero_penalty()
19542029
};
19552030
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
19562031
let source = source_node_id();
@@ -1986,16 +2061,16 @@ mod tests {
19862061
fn restores_persisted_liquidity_bounds() {
19872062
let network_graph = network_graph();
19882063
let params = ProbabilisticScoringParameters {
1989-
base_penalty_msat: 0,
19902064
liquidity_penalty_multiplier_msat: 1_000,
19912065
liquidity_offset_half_life: Duration::from_secs(10),
2066+
..ProbabilisticScoringParameters::zero_penalty()
19922067
};
19932068
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
19942069
let source = source_node_id();
19952070
let target = target_node_id();
19962071

19972072
scorer.payment_path_failed(&payment_path_for_amount(500).iter().collect::<Vec<_>>(), 42);
1998-
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
2073+
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), u64::max_value());
19992074

20002075
SinceEpoch::advance(Duration::from_secs(10));
20012076
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 472);
@@ -2016,16 +2091,16 @@ mod tests {
20162091
fn decays_persisted_liquidity_bounds() {
20172092
let network_graph = network_graph();
20182093
let params = ProbabilisticScoringParameters {
2019-
base_penalty_msat: 0,
20202094
liquidity_penalty_multiplier_msat: 1_000,
20212095
liquidity_offset_half_life: Duration::from_secs(10),
2096+
..ProbabilisticScoringParameters::zero_penalty()
20222097
};
20232098
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
20242099
let source = source_node_id();
20252100
let target = target_node_id();
20262101

20272102
scorer.payment_path_failed(&payment_path_for_amount(500).iter().collect::<Vec<_>>(), 42);
2028-
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
2103+
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), u64::max_value());
20292104

20302105
let mut serialized_scorer = Vec::new();
20312106
scorer.write(&mut serialized_scorer).unwrap();
@@ -2051,7 +2126,8 @@ mod tests {
20512126
let target = target_node_id();
20522127

20532128
let params = ProbabilisticScoringParameters {
2054-
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
2129+
liquidity_penalty_multiplier_msat: 1_000,
2130+
..ProbabilisticScoringParameters::zero_penalty()
20552131
};
20562132
let scorer = ProbabilisticScorer::new(params, &network_graph);
20572133
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 58);
@@ -2063,14 +2139,38 @@ mod tests {
20632139
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 558);
20642140
}
20652141

2142+
#[test]
2143+
fn adds_amount_penalty_to_liquidity_penalty() {
2144+
let network_graph = network_graph();
2145+
let source = source_node_id();
2146+
let target = target_node_id();
2147+
2148+
let params = ProbabilisticScoringParameters {
2149+
liquidity_penalty_multiplier_msat: 1_000,
2150+
amount_penalty_multiplier_msat: 0,
2151+
..ProbabilisticScoringParameters::zero_penalty()
2152+
};
2153+
let scorer = ProbabilisticScorer::new(params, &network_graph);
2154+
assert_eq!(scorer.channel_penalty_msat(42, 512_000, 1_024_000, &source, &target), 300);
2155+
2156+
let params = ProbabilisticScoringParameters {
2157+
liquidity_penalty_multiplier_msat: 1_000,
2158+
amount_penalty_multiplier_msat: 256,
2159+
..ProbabilisticScoringParameters::zero_penalty()
2160+
};
2161+
let scorer = ProbabilisticScorer::new(params, &network_graph);
2162+
assert_eq!(scorer.channel_penalty_msat(42, 512_000, 1_024_000, &source, &target), 337);
2163+
}
2164+
20662165
#[test]
20672166
fn calculates_log10_without_overflowing_u64_max_value() {
20682167
let network_graph = network_graph();
20692168
let source = source_node_id();
20702169
let target = target_node_id();
20712170

20722171
let params = ProbabilisticScoringParameters {
2073-
base_penalty_msat: 0, ..Default::default()
2172+
liquidity_penalty_multiplier_msat: 40_000,
2173+
..ProbabilisticScoringParameters::zero_penalty()
20742174
};
20752175
let scorer = ProbabilisticScorer::new(params, &network_graph);
20762176
assert_eq!(

0 commit comments

Comments
 (0)