Skip to content

Commit b247bda

Browse files
authored
Merge pull request #3707 from valentinewallace/2025-03-lexe-router-bugfix
Fix spurious MPP pathfinding failure
2 parents 5dcd6c4 + 209cb2a commit b247bda

File tree

3 files changed

+138
-32
lines changed

3 files changed

+138
-32
lines changed

lightning/src/blinded_path/payment.rs

+34-18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph};
3131
use crate::sign::{EntropySource, NodeSigner, Recipient};
3232
use crate::types::features::BlindedHopFeatures;
3333
use crate::types::payment::PaymentSecret;
34+
use crate::types::routing::RoutingFees;
3435
use crate::util::ser::{
3536
FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, WithoutLength,
3637
Writeable, Writer,
@@ -692,22 +693,17 @@ pub(crate) fn amt_to_forward_msat(
692693
u64::try_from(amt_to_forward).ok()
693694
}
694695

695-
pub(super) fn compute_payinfo(
696-
intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &UnauthenticatedReceiveTlvs,
697-
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
698-
) -> Result<BlindedPayInfo, ()> {
696+
// Returns (aggregated_base_fee, aggregated_proportional_fee)
697+
pub(crate) fn compute_aggregated_base_prop_fee<I>(hops_fees: I) -> Result<(u64, u64), ()>
698+
where
699+
I: DoubleEndedIterator<Item = RoutingFees>,
700+
{
699701
let mut curr_base_fee: u64 = 0;
700702
let mut curr_prop_mil: u64 = 0;
701-
let mut cltv_expiry_delta: u16 = min_final_cltv_expiry_delta;
702-
for tlvs in intermediate_nodes.iter().rev().map(|n| &n.tlvs) {
703-
// In the future, we'll want to take the intersection of all supported features for the
704-
// `BlindedPayInfo`, but there are no features in that context right now.
705-
if tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) {
706-
return Err(());
707-
}
703+
for fees in hops_fees.rev() {
704+
let next_base_fee = fees.base_msat as u64;
705+
let next_prop_mil = fees.proportional_millionths as u64;
708706

709-
let next_base_fee = tlvs.payment_relay.fee_base_msat as u64;
710-
let next_prop_mil = tlvs.payment_relay.fee_proportional_millionths as u64;
711707
// Use integer arithmetic to compute `ceil(a/b)` as `(a+b-1)/b`
712708
// ((curr_base_fee * (1_000_000 + next_prop_mil)) / 1_000_000) + next_base_fee
713709
curr_base_fee = curr_base_fee
@@ -724,14 +720,34 @@ pub(super) fn compute_payinfo(
724720
.map(|f| f / 1_000_000)
725721
.and_then(|f| f.checked_sub(1_000_000))
726722
.ok_or(())?;
727-
728-
cltv_expiry_delta =
729-
cltv_expiry_delta.checked_add(tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
730723
}
731724

725+
Ok((curr_base_fee, curr_prop_mil))
726+
}
727+
728+
pub(super) fn compute_payinfo(
729+
intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &UnauthenticatedReceiveTlvs,
730+
payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16,
731+
) -> Result<BlindedPayInfo, ()> {
732+
let (aggregated_base_fee, aggregated_prop_fee) =
733+
compute_aggregated_base_prop_fee(intermediate_nodes.iter().map(|node| RoutingFees {
734+
base_msat: node.tlvs.payment_relay.fee_base_msat,
735+
proportional_millionths: node.tlvs.payment_relay.fee_proportional_millionths,
736+
}))?;
737+
732738
let mut htlc_minimum_msat: u64 = 1;
733739
let mut htlc_maximum_msat: u64 = 21_000_000 * 100_000_000 * 1_000; // Total bitcoin supply
740+
let mut cltv_expiry_delta: u16 = min_final_cltv_expiry_delta;
734741
for node in intermediate_nodes.iter() {
742+
// In the future, we'll want to take the intersection of all supported features for the
743+
// `BlindedPayInfo`, but there are no features in that context right now.
744+
if node.tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) {
745+
return Err(());
746+
}
747+
748+
cltv_expiry_delta =
749+
cltv_expiry_delta.checked_add(node.tlvs.payment_relay.cltv_expiry_delta).ok_or(())?;
750+
735751
// The min htlc for an intermediate node is that node's min minus the fees charged by all of the
736752
// following hops for forwarding that min, since that fee amount will automatically be included
737753
// in the amount that this node receives and contribute towards reaching its min.
@@ -754,8 +770,8 @@ pub(super) fn compute_payinfo(
754770
return Err(());
755771
}
756772
Ok(BlindedPayInfo {
757-
fee_base_msat: u32::try_from(curr_base_fee).map_err(|_| ())?,
758-
fee_proportional_millionths: u32::try_from(curr_prop_mil).map_err(|_| ())?,
773+
fee_base_msat: u32::try_from(aggregated_base_fee).map_err(|_| ())?,
774+
fee_proportional_millionths: u32::try_from(aggregated_prop_fee).map_err(|_| ())?,
759775
cltv_expiry_delta,
760776
htlc_minimum_msat,
761777
htlc_maximum_msat,

lightning/src/ln/payment_tests.rs

+51
Original file line numberDiff line numberDiff line change
@@ -4515,3 +4515,54 @@ fn pay_route_without_params() {
45154515
ClaimAlongRouteArgs::new(&nodes[0], &[&[&nodes[1]]], payment_preimage)
45164516
);
45174517
}
4518+
4519+
#[test]
4520+
fn max_out_mpp_path() {
4521+
// In this setup, the sender is attempting to route an MPP payment split across the two channels
4522+
// that it has with its LSP, where the LSP has a single large channel to the recipient.
4523+
//
4524+
// Previously a user ran into a pathfinding failure here because our router was not sending the
4525+
// maximum possible value over the first MPP path it found due to overestimating the fees needed
4526+
// to cover the following hops. Because the path that had just been found was not maxxed out, our
4527+
// router assumed that we had already found enough paths to cover the full payment amount and that
4528+
// we were finding additional paths for the purpose of redundant path selection. This caused the
4529+
// router to mark the recipient's only channel as exhausted, with the intention of choosing more
4530+
// unique paths in future iterations. In reality, this ended up with the recipient's only channel
4531+
// being disabled and subsequently failing to find a route entirely.
4532+
//
4533+
// The router has since been updated to fully utilize the capacity of any paths it finds in this
4534+
// situation, preventing the "redundant path selection" behavior from kicking in.
4535+
4536+
let mut user_cfg = test_default_channel_config();
4537+
user_cfg.channel_config.forwarding_fee_base_msat = 0;
4538+
user_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
4539+
let mut lsp_cfg = test_default_channel_config();
4540+
lsp_cfg.channel_config.forwarding_fee_base_msat = 0;
4541+
lsp_cfg.channel_config.forwarding_fee_proportional_millionths = 3000;
4542+
lsp_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100;
4543+
4544+
let chanmon_cfgs = create_chanmon_cfgs(3);
4545+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
4546+
let node_chanmgrs = create_node_chanmgrs(
4547+
3, &node_cfgs, &[Some(user_cfg.clone()), Some(lsp_cfg.clone()), Some(user_cfg.clone())]
4548+
);
4549+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
4550+
4551+
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 200_000, 0);
4552+
create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 300_000, 0);
4553+
create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 600_000, 0);
4554+
4555+
let amt_msat = 350_000_000;
4556+
let invoice_params = crate::ln::channelmanager::Bolt11InvoiceParameters {
4557+
amount_msats: Some(amt_msat),
4558+
..Default::default()
4559+
};
4560+
let invoice = nodes[2].node.create_bolt11_invoice(invoice_params).unwrap();
4561+
let route_params_cfg = crate::routing::router::RouteParametersConfig::default();
4562+
4563+
nodes[0].node.pay_for_bolt11_invoice(&invoice, PaymentId([42; 32]), None, route_params_cfg, Retry::Attempts(0)).unwrap();
4564+
4565+
assert!(nodes[0].node.list_recent_payments().len() == 1);
4566+
check_added_monitors(&nodes[0], 2); // one monitor update per MPP part
4567+
nodes[0].node.get_and_clear_pending_msg_events();
4568+
}

lightning/src/routing/router.rs

+53-14
Original file line numberDiff line numberDiff line change
@@ -2043,9 +2043,10 @@ impl<'a> PaymentPath<'a> {
20432043
// that it the value being transferred has decreased while we were doing path finding, leading
20442044
// to the fees being paid not lining up with the actual limits.
20452045
//
2046-
// Note that this function is not aware of the available_liquidity limit, and thus does not
2047-
// support increasing the value being transferred beyond what was selected during the initial
2048-
// routing passes.
2046+
// This function may also be used to increase the value being transferred in the case that
2047+
// overestimating later hops' fees caused us to underutilize earlier hops' capacity.
2048+
//
2049+
// Note that this function is not aware of the available_liquidity limit of any hops.
20492050
//
20502051
// Returns the amount that this path contributes to the total payment value, which may be greater
20512052
// than `value_msat` if we had to overpay to meet the final node's `htlc_minimum_msat`.
@@ -2110,15 +2111,56 @@ impl<'a> PaymentPath<'a> {
21102111
cur_hop.hop_use_fee_msat = new_fee;
21112112
total_fee_paid_msat += new_fee;
21122113
} else {
2113-
// It should not be possible because this function is called only to reduce the
2114-
// value. In that case, compute_fee was already called with the same fees for
2115-
// larger amount and there was no overflow.
2114+
// It should not be possible because this function is only called either to reduce the
2115+
// value or with a larger amount that was already checked for overflow in
2116+
// `compute_max_final_value_contribution`. In the former case, compute_fee was already
2117+
// called with the same fees for larger amount and there was no overflow.
21162118
unreachable!();
21172119
}
21182120
}
21192121
}
21202122
value_msat + extra_contribution_msat
21212123
}
2124+
2125+
// Returns the maximum contribution that this path can make to the final value of the payment. May
2126+
// be slightly lower than the actual max due to rounding errors when aggregating fees along the
2127+
// path.
2128+
fn compute_max_final_value_contribution(
2129+
&self, used_liquidities: &HashMap<CandidateHopId, u64>, channel_saturation_pow_half: u8
2130+
) -> u64 {
2131+
let mut max_path_contribution = u64::MAX;
2132+
for (idx, (hop, _)) in self.hops.iter().enumerate() {
2133+
let hop_effective_capacity_msat = hop.candidate.effective_capacity();
2134+
let hop_max_msat = max_htlc_from_capacity(
2135+
hop_effective_capacity_msat, channel_saturation_pow_half
2136+
).saturating_sub(*used_liquidities.get(&hop.candidate.id()).unwrap_or(&0_u64));
2137+
2138+
let next_hops_feerates_iter = self.hops
2139+
.iter()
2140+
.skip(idx + 1)
2141+
.map(|(hop, _)| hop.candidate.fees());
2142+
2143+
// Aggregate the fees of the hops that come after this one, and use those fees to compute the
2144+
// maximum amount that this hop can contribute to the final value received by the payee.
2145+
let (next_hops_aggregated_base, next_hops_aggregated_prop) =
2146+
crate::blinded_path::payment::compute_aggregated_base_prop_fee(next_hops_feerates_iter).unwrap();
2147+
2148+
// ceil(((hop_max_msat - agg_base) * 1_000_000) / (1_000_000 + agg_prop))
2149+
let hop_max_final_value_contribution = (hop_max_msat as u128)
2150+
.checked_sub(next_hops_aggregated_base as u128)
2151+
.and_then(|f| f.checked_mul(1_000_000))
2152+
.and_then(|f| f.checked_add(1_000_000 - 1))
2153+
.and_then(|f| f.checked_add(next_hops_aggregated_prop as u128))
2154+
.map(|f| f / ((next_hops_aggregated_prop as u128).saturating_add(1_000_000)));
2155+
2156+
if let Some(hop_contribution) = hop_max_final_value_contribution {
2157+
let hop_contribution: u64 = hop_contribution.try_into().unwrap_or(u64::MAX);
2158+
max_path_contribution = core::cmp::min(hop_contribution, max_path_contribution);
2159+
} else { debug_assert!(false); }
2160+
}
2161+
2162+
max_path_contribution
2163+
}
21222164
}
21232165

21242166
#[inline(always)]
@@ -3269,7 +3311,10 @@ where L::Target: Logger {
32693311
// recompute the fees again, so that if that's the case, we match the currently
32703312
// underpaid htlc_minimum_msat with fees.
32713313
debug_assert_eq!(payment_path.get_value_msat(), value_contribution_msat);
3272-
let desired_value_contribution = cmp::min(value_contribution_msat, final_value_msat);
3314+
let max_path_contribution_msat = payment_path.compute_max_final_value_contribution(
3315+
&used_liquidities, channel_saturation_pow_half
3316+
);
3317+
let desired_value_contribution = cmp::min(max_path_contribution_msat, final_value_msat);
32733318
value_contribution_msat = payment_path.update_value_and_recompute_fees(desired_value_contribution);
32743319

32753320
// Since a path allows to transfer as much value as
@@ -3281,7 +3326,6 @@ where L::Target: Logger {
32813326
// might have been computed considering a larger value.
32823327
// Remember that we used these channels so that we don't rely
32833328
// on the same liquidity in future paths.
3284-
let mut prevented_redundant_path_selection = false;
32853329
for (hop, _) in payment_path.hops.iter() {
32863330
let spent_on_hop_msat = value_contribution_msat + hop.next_hops_fee_msat;
32873331
let used_liquidity_msat = used_liquidities
@@ -3290,14 +3334,9 @@ where L::Target: Logger {
32903334
.or_insert(spent_on_hop_msat);
32913335
let hop_capacity = hop.candidate.effective_capacity();
32923336
let hop_max_msat = max_htlc_from_capacity(hop_capacity, channel_saturation_pow_half);
3293-
if *used_liquidity_msat == hop_max_msat {
3294-
// If this path used all of this channel's available liquidity, we know
3295-
// this path will not be selected again in the next loop iteration.
3296-
prevented_redundant_path_selection = true;
3297-
}
32983337
debug_assert!(*used_liquidity_msat <= hop_max_msat);
32993338
}
3300-
if !prevented_redundant_path_selection {
3339+
if max_path_contribution_msat > value_contribution_msat {
33013340
// If we weren't capped by hitting a liquidity limit on a channel in the path,
33023341
// we'll probably end up picking the same path again on the next iteration.
33033342
// Decrease the available liquidity of a hop in the middle of the path.

0 commit comments

Comments
 (0)