Skip to content

Commit 979939b

Browse files
committed
Linear cost function ProbabilisticScorer option
Without recent knowledge of channel liquidity balances, ProbabilisticScorer tends to give low penalties unless the payment amount is a large portion of a channel's capacity. Provide an option that gives a linear penalty increase as the success probability decreases, which may be useful in such scenarios.
1 parent cb1d795 commit 979939b

File tree

1 file changed

+93
-24
lines changed

1 file changed

+93
-24
lines changed

lightning/src/routing/scoring.rs

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -506,19 +506,29 @@ pub struct ProbabilisticScorerUsingTime<G: Deref<Target = NetworkGraph>, T: Time
506506
/// Parameters for configuring [`ProbabilisticScorer`].
507507
#[derive(Clone, Copy)]
508508
pub struct ProbabilisticScoringParameters {
509-
/// A multiplier used to determine the amount in msats willing to be paid to avoid routing
510-
/// through a channel, as per multiplying by the negative `log10` of the channel's success
511-
/// probability for a payment.
509+
/// The function calculating the cost of routing an amount through a channel.
512510
///
513-
/// The success probability is determined by the effective channel capacity, the payment amount,
514-
/// and knowledge learned from prior successful and unsuccessful payments. The lower bound of
515-
/// the success probability is 0.01, effectively limiting the penalty to the range
516-
/// `0..=2*liquidity_penalty_multiplier_msat`. The knowledge learned is decayed over time based
517-
/// on [`liquidity_offset_half_life`].
511+
/// The cost is multiplied by [`liquidity_penalty_multiplier_msat`] to determine the channel
512+
/// penalty (i.e., the amount msats willing to be paid to avoid routing through the channel).
513+
/// Penalties are limited to `2 * liquidity_penalty_multiplier_msat`.
518514
///
519-
/// Default value: 10,000 msat
515+
/// The cost is based in part by the knowledge learned from prior successful and unsuccessful
516+
/// payments. This knowledge is decayed over time based on [`liquidity_offset_half_life`].
517+
///
518+
/// Default value: [`ProbabilisticScoringCostFunction::NegativeLogSuccessProbability`]
520519
///
520+
/// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat
521521
/// [`liquidity_offset_half_life`]: Self::liquidity_offset_half_life
522+
pub cost_function: ProbabilisticScoringCostFunction,
523+
524+
/// A multiplier used in conjunction with [`cost_function`] to determine the channel penalty.
525+
///
526+
/// The channel penalty is the amount in msats willing to be paid to avoid routing through a
527+
/// channel. It is effectively limited to `2 * liquidity_penalty_multiplier_msat`.
528+
///
529+
/// Default value: 10,000 msat
530+
///
531+
/// [`cost_function`]: Self::cost_function
522532
pub liquidity_penalty_multiplier_msat: u64,
523533

524534
/// The time required to elapse before any knowledge learned about channel liquidity balances is
@@ -537,10 +547,28 @@ pub struct ProbabilisticScoringParameters {
537547
pub liquidity_offset_half_life: Duration,
538548
}
539549

540-
impl_writeable_tlv_based!(ProbabilisticScoringParameters, {
541-
(0, liquidity_penalty_multiplier_msat, required),
542-
(2, liquidity_offset_half_life, required),
543-
});
550+
/// A function calculating the cost of routing an amount through a channel.
551+
///
552+
/// Costs are calculated in terms of a payment `success_probability`, which is determined by the
553+
/// effective channel capacity, the payment amount, and knowledge learned from prior successful and
554+
/// unsuccessful payments. Some cost functions may instead use the `failure_probability`, which is
555+
/// simply `1.0 - success_probability`. The `success_probability` has a lower bound of `0.01`.
556+
#[derive(Clone, Copy)]
557+
pub enum ProbabilisticScoringCostFunction {
558+
/// Uses `-log10(success_probability)`, which slowly increases the cost as `success_probability`
559+
/// decreases before rapidly increasing.
560+
///
561+
/// This effectively linearizes penalties across multiple hops as a result of the log product
562+
/// property (i.e., a penalty can be reasoned about in terms of the success probability of an
563+
/// entire path). However, this may result in low penalties when the channel liquidity balance
564+
/// uncertainty is high as payments are less likely to be close to the bounds (i.e., success
565+
/// probability is unlikely to be low).
566+
NegativeLogSuccessProbability,
567+
568+
/// Uses `2 * failure_probability`, which increases the cost linearly as `success_probability`
569+
/// decreases.
570+
TwiceFailureProbability,
571+
}
544572

545573
/// Accounting for channel liquidity balance uncertainty.
546574
///
@@ -590,6 +618,7 @@ impl<G: Deref<Target = NetworkGraph>, T: Time> ProbabilisticScorerUsingTime<G, T
590618
impl Default for ProbabilisticScoringParameters {
591619
fn default() -> Self {
592620
Self {
621+
cost_function: ProbabilisticScoringCostFunction::NegativeLogSuccessProbability,
593622
liquidity_penalty_multiplier_msat: 10_000,
594623
liquidity_offset_half_life: Duration::from_secs(3600),
595624
}
@@ -652,21 +681,32 @@ impl<T: Time> ChannelLiquidity<T> {
652681
impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiquidity<L, T, U> {
653682
/// Returns a penalty for routing the given HTLC `amount_msat` through the channel in this
654683
/// direction.
655-
fn penalty_msat(&self, amount_msat: u64, liquidity_penalty_multiplier_msat: u64) -> u64 {
684+
fn penalty_msat(&self, amount_msat: u64, params: &ProbabilisticScoringParameters) -> u64 {
685+
let max_penalty_msat = params.liquidity_penalty_multiplier_msat.saturating_mul(2);
656686
let max_liquidity_msat = self.max_liquidity_msat();
657687
let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat);
658688
if amount_msat > max_liquidity_msat {
659-
u64::max_value()
689+
max_penalty_msat
660690
} else if amount_msat <= min_liquidity_msat {
661691
0
662692
} else {
663693
let numerator = max_liquidity_msat + 1 - amount_msat;
664694
let denominator = max_liquidity_msat + 1 - min_liquidity_msat;
665-
approx::negative_log10_times_1024(numerator, denominator)
666-
.saturating_mul(liquidity_penalty_multiplier_msat) / 1024
695+
match params.cost_function {
696+
ProbabilisticScoringCostFunction::NegativeLogSuccessProbability => {
697+
let penalty_msat = approx::negative_log10_times_1024(numerator, denominator)
698+
.saturating_mul(params.liquidity_penalty_multiplier_msat) / 1024;
699+
// Upper bound the penalty to ensure some channel is selected.
700+
penalty_msat.min(max_penalty_msat)
701+
},
702+
ProbabilisticScoringCostFunction::TwiceFailureProbability => {
703+
// Avoid floating-point operations by multiplying the coefficient through:
704+
// 2 * liquidity_penalty_multiplier_msat * (1 - success_probability)
705+
let coefficient = max_penalty_msat;
706+
coefficient.saturating_sub(coefficient.saturating_mul(numerator) / denominator)
707+
},
708+
}
667709
}
668-
// Upper bound the penalty to ensure some channel is selected.
669-
.min(2 * liquidity_penalty_multiplier_msat)
670710
}
671711

672712
/// Returns the lower bound of the channel liquidity balance in this direction.
@@ -738,13 +778,11 @@ impl<G: Deref<Target = NetworkGraph>, T: Time> Score for ProbabilisticScorerUsin
738778
&self, short_channel_id: u64, amount_msat: u64, capacity_msat: u64, source: &NodeId,
739779
target: &NodeId
740780
) -> u64 {
741-
let liquidity_penalty_multiplier_msat = self.params.liquidity_penalty_multiplier_msat;
742-
let liquidity_offset_half_life = self.params.liquidity_offset_half_life;
743781
self.channel_liquidities
744782
.get(&short_channel_id)
745783
.unwrap_or(&ChannelLiquidity::new())
746-
.as_directed(source, target, capacity_msat, liquidity_offset_half_life)
747-
.penalty_msat(amount_msat, liquidity_penalty_multiplier_msat)
784+
.as_directed(source, target, capacity_msat, self.params.liquidity_offset_half_life)
785+
.penalty_msat(amount_msat, &self.params)
748786
}
749787

750788
fn payment_path_failed(&mut self, path: &[&RouteHop], short_channel_id: u64) {
@@ -1053,7 +1091,7 @@ pub(crate) use self::time::Time;
10531091

10541092
#[cfg(test)]
10551093
mod tests {
1056-
use super::{ChannelLiquidity, ProbabilisticScoringParameters, ProbabilisticScorerUsingTime, ScoringParameters, ScorerUsingTime, Time};
1094+
use super::{ChannelLiquidity, ProbabilisticScoringCostFunction, ProbabilisticScoringParameters, ProbabilisticScorerUsingTime, ScoringParameters, ScorerUsingTime, Time};
10571095
use super::time::Eternity;
10581096

10591097
use ln::features::{ChannelFeatures, NodeFeatures};
@@ -1742,6 +1780,32 @@ mod tests {
17421780
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 903);
17431781
}
17441782

1783+
#[test]
1784+
fn increased_penalty_linearly_nearing_liquidity_upper_bound() {
1785+
let network_graph = network_graph();
1786+
let params = ProbabilisticScoringParameters {
1787+
cost_function: ProbabilisticScoringCostFunction::TwiceFailureProbability,
1788+
liquidity_penalty_multiplier_msat: 1_000,
1789+
..Default::default()
1790+
};
1791+
let scorer = ProbabilisticScorer::new(params, &network_graph);
1792+
let source = source_node_id();
1793+
let target = target_node_id();
1794+
1795+
assert_eq!(scorer.channel_penalty_msat(42, 1_024, 1_024_000, &source, &target), 2);
1796+
assert_eq!(scorer.channel_penalty_msat(42, 10_240, 1_024_000, &source, &target), 20);
1797+
assert_eq!(scorer.channel_penalty_msat(42, 102_400, 1_024_000, &source, &target), 200);
1798+
assert_eq!(scorer.channel_penalty_msat(42, 1_024_000, 1_024_000, &source, &target), 2_000);
1799+
1800+
assert_eq!(scorer.channel_penalty_msat(42, 125, 1_000, &source, &target), 250);
1801+
assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 500);
1802+
assert_eq!(scorer.channel_penalty_msat(42, 375, 1_000, &source, &target), 750);
1803+
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 1_000);
1804+
assert_eq!(scorer.channel_penalty_msat(42, 625, 1_000, &source, &target), 1_249);
1805+
assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 1_499);
1806+
assert_eq!(scorer.channel_penalty_msat(42, 875, 1_000, &source, &target), 1_749);
1807+
}
1808+
17451809
#[test]
17461810
fn constant_penalty_outside_liquidity_bounds() {
17471811
let last_updated = SinceEpoch::now();
@@ -1858,6 +1922,7 @@ mod tests {
18581922
let params = ProbabilisticScoringParameters {
18591923
liquidity_penalty_multiplier_msat: 1_000,
18601924
liquidity_offset_half_life: Duration::from_secs(10),
1925+
..Default::default()
18611926
};
18621927
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
18631928
let source = source_node_id();
@@ -1909,6 +1974,7 @@ mod tests {
19091974
let params = ProbabilisticScoringParameters {
19101975
liquidity_penalty_multiplier_msat: 1_000,
19111976
liquidity_offset_half_life: Duration::from_secs(10),
1977+
..Default::default()
19121978
};
19131979
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
19141980
let source = source_node_id();
@@ -1933,6 +1999,7 @@ mod tests {
19331999
let params = ProbabilisticScoringParameters {
19342000
liquidity_penalty_multiplier_msat: 1_000,
19352001
liquidity_offset_half_life: Duration::from_secs(10),
2002+
..Default::default()
19362003
};
19372004
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
19382005
let source = source_node_id();
@@ -1970,6 +2037,7 @@ mod tests {
19702037
let params = ProbabilisticScoringParameters {
19712038
liquidity_penalty_multiplier_msat: 1_000,
19722039
liquidity_offset_half_life: Duration::from_secs(10),
2040+
..Default::default()
19732041
};
19742042
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
19752043
let source = source_node_id();
@@ -1999,6 +2067,7 @@ mod tests {
19992067
let params = ProbabilisticScoringParameters {
20002068
liquidity_penalty_multiplier_msat: 1_000,
20012069
liquidity_offset_half_life: Duration::from_secs(10),
2070+
..Default::default()
20022071
};
20032072
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
20042073
let source = source_node_id();

0 commit comments

Comments
 (0)