Skip to content

ProbabilisticScorer improvements #1399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 2, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 137 additions & 37 deletions lightning/src/routing/scoring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ pub struct ProbabilisticScorerUsingTime<G: Deref<Target = NetworkGraph>, T: Time

/// Parameters for configuring [`ProbabilisticScorer`].
///
/// Used to configure a base penalty and a liquidity penalty, the sum of which is the channel
/// Used to configure base, liquidity, and amount penalties, the sum of which comprises the channel
/// penalty (i.e., the amount in msats willing to be paid to avoid routing through the channel).
#[derive(Clone, Copy)]
pub struct ProbabilisticScoringParameters {
Expand All @@ -529,9 +529,12 @@ pub struct ProbabilisticScoringParameters {
/// A multiplier used in conjunction with the negative `log10` of the channel's success
/// probability for a payment to determine the liquidity penalty.
///
/// The penalty is based in part by the knowledge learned from prior successful and unsuccessful
/// The penalty is based in part on the knowledge learned from prior successful and unsuccessful
/// payments. This knowledge is decayed over time based on [`liquidity_offset_half_life`]. The
/// penalty is effectively limited to `2 * liquidity_penalty_multiplier_msat`.
/// penalty is effectively limited to `2 * liquidity_penalty_multiplier_msat` (corresponding to
/// lower bounding the success probability to `0.01`) when the amount falls within the
/// uncertainty bounds of the channel liquidity balance. Amounts above the upper bound will
/// result in a `u64::max_value` penalty, however.
///
/// Default value: 40,000 msat
///
Expand All @@ -552,6 +555,25 @@ pub struct ProbabilisticScoringParameters {
/// When built with the `no-std` feature, time will never elapse. Therefore, the channel
/// liquidity knowledge will never decay except when the bounds cross.
pub liquidity_offset_half_life: Duration,

/// A multiplier used in conjunction with a payment amount and the negative `log10` of the
/// channel's success probability for the payment to determine the amount penalty.
///
/// The purpose of the amount penalty is to avoid having fees dominate the channel cost (i.e.,
/// fees plus penalty) for large payments. The penalty is computed as the product of this
/// multiplier and `2^20`ths of the payment amount, weighted by the negative `log10` of the
/// success probability.
///
/// `-log10(success_probability) * amount_penalty_multiplier_msat * amount_msat / 2^20`
///
/// In practice, this means for 0.1 success probability (`-log10(0.1) == 1`) each `2^20`th of
/// the amount will result in a penalty of the multiplier. And, as the success probability
/// decreases, the negative `log10` weighting will increase dramatically. For higher success
/// probabilities, the multiplier will have a decreasing effect as the negative `log10` will
/// fall below `1`.
///
/// Default value: 256 msat
pub amount_penalty_multiplier_msat: u64,
}

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

impl ProbabilisticScoringParameters {
#[cfg(test)]
fn zero_penalty() -> Self {
Self {
base_penalty_msat: 0,
liquidity_penalty_multiplier_msat: 0,
liquidity_offset_half_life: Duration::from_secs(3600),
amount_penalty_multiplier_msat: 0,
}
}
}

impl Default for ProbabilisticScoringParameters {
fn default() -> Self {
Self {
base_penalty_msat: 500,
liquidity_penalty_multiplier_msat: 40_000,
liquidity_offset_half_life: Duration::from_secs(3600),
amount_penalty_multiplier_msat: 256,
}
}
}
Expand Down Expand Up @@ -662,27 +697,63 @@ impl<T: Time> ChannelLiquidity<T> {
}
}

/// Bounds `-log10` to avoid excessive liquidity penalties for payments with low success
/// probabilities.
const NEGATIVE_LOG10_UPPER_BOUND: u64 = 2;

/// The divisor used when computing the amount penalty.
const AMOUNT_PENALTY_DIVISOR: u64 = 1 << 20;

impl<L: Deref<Target = u64>, T: Time, U: Deref<Target = T>> DirectedChannelLiquidity<L, T, U> {
/// Returns a penalty for routing the given HTLC `amount_msat` through the channel in this
/// direction.
fn penalty_msat(&self, amount_msat: u64, liquidity_penalty_multiplier_msat: u64) -> u64 {
let max_penalty_msat = liquidity_penalty_multiplier_msat.saturating_mul(2);
fn penalty_msat(&self, amount_msat: u64, params: ProbabilisticScoringParameters) -> u64 {
let max_liquidity_msat = self.max_liquidity_msat();
let min_liquidity_msat = core::cmp::min(self.min_liquidity_msat(), max_liquidity_msat);
if amount_msat > max_liquidity_msat {
max_penalty_msat
} else if amount_msat <= min_liquidity_msat {
if amount_msat <= min_liquidity_msat {
0
} else if amount_msat >= max_liquidity_msat {
if amount_msat > max_liquidity_msat {
u64::max_value()
} else if max_liquidity_msat != self.capacity_msat {
// Avoid using the failed channel on retry.
u64::max_value()
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how likely is this case to ever be hit, that amount_msat is precisely equal to max_liquidity_msat?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just tried the payment, its possible, the final else here pretty unlikely I'd guess. Maybe for "very round" numbers - 1m sats, 100k sats, etc it could happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, also if the HTLC maximum is some round amount, it may get hit. We should really use the capacity here if known, but currently we do not. I have a commit to do this that was dropped from the original ProbabilisticScorer PR.

// Equivalent to hitting the else clause below with the amount equal to the
// effective capacity and without any certainty on the liquidity upper bound.
let negative_log10_times_1024 = NEGATIVE_LOG10_UPPER_BOUND * 1024;
self.combined_penalty_msat(amount_msat, negative_log10_times_1024, params)
}
} else {
let numerator = (max_liquidity_msat - amount_msat).saturating_add(1);
let denominator = (max_liquidity_msat - min_liquidity_msat).saturating_add(1);
let penalty_msat = approx::negative_log10_times_1024(numerator, denominator)
.saturating_mul(liquidity_penalty_multiplier_msat) / 1024;
// Upper bound the penalty to ensure some channel is selected.
penalty_msat.min(max_penalty_msat)
let negative_log10_times_1024 =
approx::negative_log10_times_1024(numerator, denominator);
self.combined_penalty_msat(amount_msat, negative_log10_times_1024, params)
}
}

/// Computes the liquidity and amount penalties and adds them to the base penalty.
#[inline(always)]
fn combined_penalty_msat(
&self, amount_msat: u64, negative_log10_times_1024: u64,
params: ProbabilisticScoringParameters
) -> u64 {
let liquidity_penalty_msat = {
// Upper bound the liquidity penalty to ensure some channel is selected.
let multiplier_msat = params.liquidity_penalty_multiplier_msat;
let max_penalty_msat = multiplier_msat.saturating_mul(NEGATIVE_LOG10_UPPER_BOUND);
(negative_log10_times_1024.saturating_mul(multiplier_msat) / 1024).min(max_penalty_msat)
};
let amount_penalty_msat = negative_log10_times_1024
.saturating_mul(params.amount_penalty_multiplier_msat)
.saturating_mul(amount_msat) / 1024 / AMOUNT_PENALTY_DIVISOR;

params.base_penalty_msat
.saturating_add(liquidity_penalty_msat)
.saturating_add(amount_penalty_msat)
}

/// Returns the lower bound of the channel liquidity balance in this direction.
fn min_liquidity_msat(&self) -> u64 {
self.decayed_offset_msat(*self.min_liquidity_offset_msat)
Expand Down Expand Up @@ -752,14 +823,12 @@ impl<G: Deref<Target = NetworkGraph>, T: Time> Score for ProbabilisticScorerUsin
&self, short_channel_id: u64, amount_msat: u64, capacity_msat: u64, source: &NodeId,
target: &NodeId
) -> u64 {
let liquidity_penalty_multiplier_msat = self.params.liquidity_penalty_multiplier_msat;
let liquidity_offset_half_life = self.params.liquidity_offset_half_life;
self.channel_liquidities
.get(&short_channel_id)
.unwrap_or(&ChannelLiquidity::new())
.as_directed(source, target, capacity_msat, liquidity_offset_half_life)
.penalty_msat(amount_msat, liquidity_penalty_multiplier_msat)
.saturating_add(self.params.base_penalty_msat)
.penalty_msat(amount_msat, self.params)
}

fn payment_path_failed(&mut self, path: &[&RouteHop], short_channel_id: u64) {
Expand Down Expand Up @@ -1737,7 +1806,8 @@ mod tests {
fn increased_penalty_nearing_liquidity_upper_bound() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
Expand All @@ -1762,7 +1832,8 @@ mod tests {
let last_updated = SinceEpoch::now();
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph)
.with_channel(42,
Expand All @@ -1774,15 +1845,16 @@ mod tests {

assert_eq!(scorer.channel_penalty_msat(42, 39, 100, &source, &target), 0);
assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), 0);
assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, 61, 100, &source, &target), 2_000);
assert_ne!(scorer.channel_penalty_msat(42, 50, 100, &source, &target), u64::max_value());
assert_eq!(scorer.channel_penalty_msat(42, 61, 100, &source, &target), u64::max_value());
}

#[test]
fn does_not_further_penalize_own_channel() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let sender = sender_node_id();
Expand All @@ -1803,7 +1875,8 @@ mod tests {
fn sets_liquidity_lower_bound_on_downstream_failure() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
Expand All @@ -1825,7 +1898,8 @@ mod tests {
fn sets_liquidity_upper_bound_on_failure() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
Expand All @@ -1839,15 +1913,16 @@ mod tests {
scorer.payment_path_failed(&path.iter().collect::<Vec<_>>(), 42);

assert_eq!(scorer.channel_penalty_msat(42, 250, 1_000, &source, &target), 300);
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), u64::max_value());
assert_eq!(scorer.channel_penalty_msat(42, 750, 1_000, &source, &target), u64::max_value());
}

#[test]
fn reduces_liquidity_upper_bound_along_path_on_success() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let sender = sender_node_id();
Expand All @@ -1871,9 +1946,9 @@ mod tests {
fn decays_liquidity_bounds_over_time() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0,
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
Expand All @@ -1888,19 +1963,19 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 0);
assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 97);
assert_eq!(scorer.channel_penalty_msat(42, 768, 1_024, &source, &target), 1_409);
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), u64::max_value());

SinceEpoch::advance(Duration::from_secs(9));
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 0);
assert_eq!(scorer.channel_penalty_msat(42, 256, 1_024, &source, &target), 97);
assert_eq!(scorer.channel_penalty_msat(42, 768, 1_024, &source, &target), 1_409);
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), u64::max_value());

SinceEpoch::advance(Duration::from_secs(1));
assert_eq!(scorer.channel_penalty_msat(42, 64, 1_024, &source, &target), 0);
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 34);
assert_eq!(scorer.channel_penalty_msat(42, 896, 1_024, &source, &target), 1_773);
assert_eq!(scorer.channel_penalty_msat(42, 960, 1_024, &source, &target), 2_000);
assert_eq!(scorer.channel_penalty_msat(42, 960, 1_024, &source, &target), u64::max_value());

// Fully decay liquidity lower bound.
SinceEpoch::advance(Duration::from_secs(10 * 7));
Expand All @@ -1923,9 +1998,9 @@ mod tests {
fn decays_liquidity_bounds_without_shift_overflow() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0,
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
Expand All @@ -1948,9 +2023,9 @@ mod tests {
fn restricts_liquidity_bounds_after_decay() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0,
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
Expand Down Expand Up @@ -1986,16 +2061,16 @@ mod tests {
fn restores_persisted_liquidity_bounds() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0,
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
let target = target_node_id();

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

SinceEpoch::advance(Duration::from_secs(10));
assert_eq!(scorer.channel_penalty_msat(42, 500, 1_000, &source, &target), 472);
Expand All @@ -2016,16 +2091,16 @@ mod tests {
fn decays_persisted_liquidity_bounds() {
let network_graph = network_graph();
let params = ProbabilisticScoringParameters {
base_penalty_msat: 0,
liquidity_penalty_multiplier_msat: 1_000,
liquidity_offset_half_life: Duration::from_secs(10),
..ProbabilisticScoringParameters::zero_penalty()
};
let mut scorer = ProbabilisticScorer::new(params, &network_graph);
let source = source_node_id();
let target = target_node_id();

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

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

let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, liquidity_penalty_multiplier_msat: 1_000, ..Default::default()
liquidity_penalty_multiplier_msat: 1_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph);
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 58);
Expand All @@ -2063,14 +2139,38 @@ mod tests {
assert_eq!(scorer.channel_penalty_msat(42, 128, 1_024, &source, &target), 558);
}

#[test]
fn adds_amount_penalty_to_liquidity_penalty() {
let network_graph = network_graph();
let source = source_node_id();
let target = target_node_id();

let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
amount_penalty_multiplier_msat: 0,
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph);
assert_eq!(scorer.channel_penalty_msat(42, 512_000, 1_024_000, &source, &target), 300);

let params = ProbabilisticScoringParameters {
liquidity_penalty_multiplier_msat: 1_000,
amount_penalty_multiplier_msat: 256,
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph);
assert_eq!(scorer.channel_penalty_msat(42, 512_000, 1_024_000, &source, &target), 337);
}

#[test]
fn calculates_log10_without_overflowing_u64_max_value() {
let network_graph = network_graph();
let source = source_node_id();
let target = target_node_id();

let params = ProbabilisticScoringParameters {
base_penalty_msat: 0, ..Default::default()
liquidity_penalty_multiplier_msat: 40_000,
..ProbabilisticScoringParameters::zero_penalty()
};
let scorer = ProbabilisticScorer::new(params, &network_graph);
assert_eq!(
Expand Down