Fix underflow in blinded path amt_to_forward
What changed, and why it matters
This commit fixes a subtraction underflow bug in the code that decides how much of a Lightning payment a blinded intermediate node should forward. With extremely high proportional fees and a small incoming payment amount, the code subtracted a larger fee from a smaller amount, which would crash in debug builds and silently produce a wrong, too-small result in release builds. In release builds that wrong result could have allowed a payment to be relayed that should have been rejected because it could not cover the node's fee. The fix uses Rust's checked subtraction so the function returns 'None' instead of underflowing.
Apply the patch and run the new regression test. Review other fee/amount subtractions in the blinded path and routing code for similar unchecked arithmetic, and prefer checked_sub / saturating_sub patterns. No immediate external advisory action is required beyond normal release notes, but consider noting the fix because it affects payment relay correctness.
Security signals we found
Integer underflow in fee/amount calculation
Debug-build panic / release-build silent wraparound
Potential relay of under-funded payment in release builds
Blinded path intermediate node fee logic
Regression test added for underflow case
Evidence from the diff
In lightning/src/blinded_path/payment.rs, amt_to_forward_msat computes the amount to forward after fees. After rounding up the forwarded amount, it computes fee = ((amt_to_forward * prop) / 1_000_000) + base, then previously did inbound_amt - fee unchecked. When fee > inbound_amt (e.g., fee_proportional_millionths = u32::MAX, base_msat = 1, inbound_amt = 2), this underflows: panic in debug, wrap-around in release. The patch replaces inbound_amt - fee with inbound_amt.checked_sub(fee)? so the function returns None. A regression test is added with u32::MAX proportional fee and inbound amount 2, asserting None.
Changed components
lightning/src/blinded_path/payment.rsamt_to_forward_msatPaymentRelay fee computation for blinded payment pathsInspect captured patch +16 / −1
diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs
index a531935..5fd608d 100644
--- a/lightning/src/blinded_path/payment.rs
+++ b/lightning/src/blinded_path/payment.rs
@@ -940,7 +940,7 @@ pub(crate) fn amt_to_forward_msat(
(post_base_fee_inbound_amt * 1_000_000 + 1_000_000 + prop - 1) / (prop + 1_000_000);
let fee = ((amt_to_forward * prop) / 1_000_000) + base;
- if inbound_amt - fee < amt_to_forward {
+ if inbound_amt.checked_sub(fee)? < amt_to_forward {
// Rounding up the forwarded amount resulted in underpaying this node, so take an extra 1 msat
// in fee to compensate.
amt_to_forward -= 1;
@@ -1415,4 +1415,19 @@ mod tests {
.unwrap();
assert_eq!(blinded_payinfo.htlc_maximum_msat, 3997);
}
+
+ #[test]
+ fn amt_to_forward_msat_underflow() {
+ // `amt_to_forward_msat` is documented to return `None` if underflow occurs, but the
+ // `inbound_amt - fee` subtraction was previously unguarded. With a high proportional fee
+ // and a small inbound amount, rounding the forwarded amount up leaves `fee` larger than
+ // `inbound_amt`, so the subtraction underflows (panicking in debug builds and returning a
+ // nonsensical result in release). Ensure we instead return `None`.
+ let payment_relay = PaymentRelay {
+ cltv_expiry_delta: 0,
+ fee_proportional_millionths: u32::MAX,
+ fee_base_msat: 1,
+ };
+ assert!(super::amt_to_forward_msat(2, &payment_relay).is_none());
+ }
}
Why this scored 70/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.