Skip to content

Commit 209cb2a

Browse files
Fix spurious MPP pathfinding failure
This bug was recently surfaced to us by a user who wrote a test where the sender is attempting to route an MPP payment split across the two channels that it has with its LSP, where the LSP has a single large channel to the recipient. Previously this led to a pathfinding failure because our router was not sending the maximum possible value over the first MPP path it found due to overestimating the fees needed to cover the following hops. Because the path that had just been found was not maxxed out, our router assumed that we had already found enough paths to cover the full payment amount and that we were finding additional paths for the purpose of redundant path selection. This caused the router to mark the recipient's only channel as exhausted, with the intention of choosing more unique paths in future iterations. In reality, this ended up with the recipient's only channel being disabled and subsequently failing to find a route entirely. Update the router to fully utilize the capacity of any paths it finds in this situation, preventing the "redundant path selection" behavior from kicking in.
1 parent 3a394df commit 209cb2a

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)