Report the used success probabilities in scorer accessor methods
What changed, and why it matters
This commit fixes two related issues in rust-lightning's payment scorer. First, two public accessor methods that report estimated payment success probabilities could trigger a debug-only assertion (a crash in debug builds) when asked about an amount equal to or larger than a channel's maximum liquidity. Second, the methods now return the same probability values actually used internally for routing decisions, including a 1% minimum probability floor. The practical effect is that callers get consistent, non-crashing estimates instead of raw values that could be zero or trigger internal checks.
Review whether any downstream callers relied on the previous raw-zero behavior; otherwise this is a straightforward correctness/consistency fix. No immediate security response appears necessary beyond normal merge and release.
Security signals we found
Debug assertion reachable through public API when amount_msat equals or exceeds channel capacity/max liquidity
Public probability accessors returned raw estimates inconsistent with internal scoring, potentially misleading callers
New lower-bound clamping aligns accessor output with actual routing penalty calculations
Evidence from the diff
In lightning/src/routing/scoring.rs, ProbabilisticScorer::historical_estimated_payment_success_probability and live_estimated_payment_success_probability are updated. The historical path now returns PROB_LOWER_BOUND (1%) when amount_msat >= capacity_msat, avoiding a debug assertion in success_probability that required amount < max_liquidity_msat. Both paths now clamp tiny positive probabilities to PROB_LOWER_BOUND, matching the scoring logic. A new public constant PROB_LOWER_BOUND is exposed. Tests are updated to expect 0.01 instead of 0.0 in the relevant cases.
Changed components
lightning/src/routing/scoring.rsProbabilisticScorer::historical_estimated_payment_success_probabilityProbabilisticScorer::live_estimated_payment_success_probabilityChannelLiquidity success probability calculationsInspect captured patch +39 / −9
diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs
index 7130c92..e9c59b7 100644
--- a/lightning/src/routing/scoring.rs
+++ b/lightning/src/routing/scoring.rs
@@ -1110,6 +1110,9 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ProbabilisticScorer<G, L> {
/// with `scid` towards the given `target` node, based on the historical estimated liquidity
/// bounds.
///
+ /// Note that probabilities for paths which are highly unlikely to succeed, but not impossible
+ /// are capped to a lower-bound of [`PROB_LOWER_BOUND`].
+ ///
/// Returns `None` if:
/// - the given channel is not in the network graph, the provided `target` is not a party to
/// the channel, or we don't have forwarding parameters for either direction in the channel.
@@ -1130,13 +1133,20 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ProbabilisticScorer<G, L> {
if let Some((directed_info, source)) = chan.as_directed_to(target) {
if let Some(liq) = self.channel_liquidities.get(&scid) {
let capacity_msat = directed_info.effective_capacity().as_msat();
+ if amount_msat >= capacity_msat {
+ return Some(PROB_LOWER_BOUND);
+ }
let dir_liq = liq.as_directed(source, target, capacity_msat);
let res = dir_liq.liquidity_history.calculate_success_probability_times_billion(
¶ms, amount_msat, capacity_msat
).map(|p| p as f64 / (1024 * 1024 * 1024) as f64);
- if res.is_some() {
- return res;
+ if let Some(prob) = res {
+ if prob < PROB_LOWER_BOUND {
+ return Some(PROB_LOWER_BOUND);
+ } else {
+ return Some(prob);
+ }
}
}
if allow_fallback_estimation {
@@ -1163,19 +1173,29 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ProbabilisticScorer<G, L> {
.as_directed(&source, &target, capacity_msat);
let min_liq = liq.min_liquidity_msat();
let max_liq = liq.max_liquidity_msat();
- if amt <= liq.min_liquidity_msat() {
+ if amt <= min_liq {
return 1.0;
- } else if amt > liq.max_liquidity_msat() {
+ } else if amt > capacity_msat {
return 0.0;
+ } else if amt >= max_liq {
+ return PROB_LOWER_BOUND;
}
let (num, den) =
success_probability(amt, min_liq, max_liq, capacity_msat, ¶ms, min_zero_penalty);
- num as f64 / den as f64
+ let res = num as f64 / den as f64;
+ if res < PROB_LOWER_BOUND {
+ PROB_LOWER_BOUND
+ } else {
+ res
+ }
}
/// Query the probability of payment success sending the given `amount_msat` over the channel
/// with `scid` towards the given `target` node, based on the live estimated liquidity bounds.
///
+ /// Note that probabilities for paths which are highly unlikely to succeed, but not impossible
+ /// are capped to a lower-bound of [`PROB_LOWER_BOUND`].
+ ///
/// This will return `Some` for any channel which is present in the [`NetworkGraph`], including
/// if we have no bound information beside the channel's capacity.
#[rustfmt::skip]
@@ -1291,8 +1311,18 @@ impl ChannelLiquidity {
/// Bounds `-log10` to avoid excessive liquidity penalties for payments with low success
/// probabilities.
+///
+/// The log10 equivalent of [`PROB_LOWER_BOUND`].
const NEGATIVE_LOG10_UPPER_BOUND: u64 = 2;
+/// The minimum probability we will use when scoring a channel where we believe success may be
+/// possible, even if its unlikely.
+///
+/// Allowing the probability to go arbitrarily low results in penalties which grow unnecessarily
+/// huge for small changes in probability (as penalties are based on the `log10` of the
+/// probability).
+pub const PROB_LOWER_BOUND: f64 = 0.01;
+
/// The rough cutoff at which our precision falls off and we should stop bothering to try to log a
/// ratio, as X in 1/X.
const PRECISION_LOWER_BOUND_DENOMINATOR: u64 = log_approx::LOWER_BITS_BOUND;
@@ -3910,7 +3940,7 @@ mod tests {
assert!(scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms, false)
.unwrap() > 0.35);
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 500, ¶ms, false),
- Some(0.0));
+ Some(super::PROB_LOWER_BOUND));
// Even after we tell the scorer we definitely have enough available liquidity, it will
// still remember that there was some failure in the past, and assign a non-0 penalty.
@@ -4166,9 +4196,9 @@ mod tests {
assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target),
Some(([32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));
- // The success probability estimate itself should be zero.
+ // The success probability estimate itself should be PROB_LOWER_BOUND.
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false),
- Some(0.0));
+ Some(super::PROB_LOWER_BOUND));
// Now test again with the amount in the bottom bucket.
amount_msat /= 2;
@@ -4185,7 +4215,7 @@ mod tests {
Some(([63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[32, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])));
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false),
- Some(0.0));
+ Some(super::PROB_LOWER_BOUND));
}
#[test]
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.