Skip to content

Commit 89a6131

Browse files
Fix outbound payments memory leak on buggy router
Prior to this patch, if we attempted to send a payment or probe to a buggy route, we would error but continue storing the pending outbound payment forever. Attempts to retry would result in a “duplicate payment” error. In the case of ChannelManager::send_payment, we would also fail to generate a PaymentFailed event, even if the user manually called abandon_payment. This bug is unlikely to have ever been hit in the wild as most users use LDK’s router. Discovered in the course of adding a new send_to_route API. Now, we’ll properly generate events and remove the outbound from storage.
1 parent c835e80 commit 89a6131

File tree

4 files changed

+105
-4
lines changed

4 files changed

+105
-4
lines changed

lightning/src/ln/channelmanager.rs

+4
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,9 @@ where
24212421
/// See `PendingOutboundPayment` documentation for more info.
24222422
///
24232423
/// See `ChannelManager` struct-level documentation for lock order requirements.
2424+
#[cfg(test)]
2425+
pub(super) pending_outbound_payments: OutboundPayments,
2426+
#[cfg(not(test))]
24242427
pending_outbound_payments: OutboundPayments,
24252428

24262429
/// SCID/SCID Alias -> forward infos. Key of 0 means payments received.
@@ -14783,6 +14786,7 @@ mod tests {
1478314786
},
1478414787
_ => panic!("unexpected error")
1478514788
}
14789+
assert!(nodes[0].node.pending_outbound_payments.pending_outbound_payments.lock().unwrap().is_empty());
1478614790
}
1478714791

1478814792
#[test]

lightning/src/ln/functional_tests.rs

+1
Original file line numberDiff line numberDiff line change
@@ -6235,6 +6235,7 @@ fn test_payment_route_reaching_same_channel_twice() {
62356235
RecipientOnionFields::secret_only(our_payment_secret), PaymentId(our_payment_hash.0)
62366236
), false, APIError::InvalidRoute { ref err },
62376237
assert_eq!(err, &"Path went through the same channel twice"));
6238+
assert!(nodes[0].node.pending_outbound_payments.pending_outbound_payments.lock().unwrap().is_empty());
62386239
}
62396240

62406241
// BOLT 2 Requirements for the Sender when constructing and sending an update_add_htlc message.

lightning/src/ln/outbound_payment.rs

+24-3
Original file line numberDiff line numberDiff line change
@@ -1740,12 +1740,26 @@ impl OutboundPayments {
17401740
NS::Target: NodeSigner,
17411741
F: Fn(SendAlongPathArgs) -> Result<(), APIError>,
17421742
{
1743+
macro_rules! remove_session_privs {
1744+
() => {
1745+
if let Some(payment) = self.pending_outbound_payments.lock().unwrap().get_mut(&payment_id) {
1746+
for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.into_iter()) {
1747+
let removed = payment.remove(&session_priv_bytes, Some(path));
1748+
debug_assert!(removed, "This can't happen as the payment has an entry for this path added by callers");
1749+
}
1750+
} else {
1751+
debug_assert!(false, "This can't happen as the payment was added by callers");
1752+
}
1753+
}
1754+
}
1755+
17431756
if route.paths.len() < 1 {
17441757
return Err(PaymentSendFailure::ParameterError(APIError::InvalidRoute{err: "There must be at least one path to send over".to_owned()}));
17451758
}
17461759
if recipient_onion.payment_secret.is_none() && route.paths.len() > 1
17471760
&& !route.paths.iter().any(|p| p.blinded_tail.is_some())
17481761
{
1762+
remove_session_privs!();
17491763
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_owned()}));
17501764
}
17511765
let mut total_value = 0;
@@ -1775,6 +1789,7 @@ impl OutboundPayments {
17751789
path_errs.push(Ok(()));
17761790
}
17771791
if path_errs.iter().any(|e| e.is_err()) {
1792+
remove_session_privs!();
17781793
return Err(PaymentSendFailure::PathParameterError(path_errs));
17791794
}
17801795
if let Some(amt_msat) = recv_value_msat {
@@ -1880,9 +1895,15 @@ impl OutboundPayments {
18801895
// If we failed to send any paths, remove the new PaymentId from the `pending_outbound_payments`
18811896
// map as the payment is free to be resent.
18821897
fn remove_outbound_if_all_failed(&self, payment_id: PaymentId, err: &PaymentSendFailure) {
1883-
if let &PaymentSendFailure::AllFailedResendSafe(_) = err {
1884-
let removed = self.pending_outbound_payments.lock().unwrap().remove(&payment_id).is_some();
1885-
debug_assert!(removed, "We should always have a pending payment to remove here");
1898+
match err {
1899+
PaymentSendFailure::AllFailedResendSafe(_)
1900+
| PaymentSendFailure::ParameterError(_)
1901+
| PaymentSendFailure::PathParameterError(_) =>
1902+
{
1903+
let removed = self.pending_outbound_payments.lock().unwrap().remove(&payment_id).is_some();
1904+
debug_assert!(removed, "We should always have a pending payment to remove here");
1905+
},
1906+
_ => {}
18861907
}
18871908
}
18881909

lightning/src/ln/payment_tests.rs

+76-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::types::payment::{PaymentHash, PaymentSecret, PaymentPreimage};
2424
use crate::ln::chan_utils;
2525
use crate::ln::msgs::ChannelMessageHandler;
2626
use crate::ln::onion_utils;
27-
use crate::ln::outbound_payment::{IDEMPOTENCY_TIMEOUT_TICKS, Retry, RetryableSendFailure};
27+
use crate::ln::outbound_payment::{IDEMPOTENCY_TIMEOUT_TICKS, ProbeSendFailure, Retry, RetryableSendFailure};
2828
use crate::routing::gossip::{EffectiveCapacity, RoutingFees};
2929
use crate::routing::router::{get_route, Path, PaymentParameters, Route, Router, RouteHint, RouteHintHop, RouteHop, RouteParameters};
3030
use crate::routing::scoring::ChannelUsage;
@@ -1249,6 +1249,7 @@ fn sent_probe_is_probe_of_sending_node() {
12491249
// First check we refuse to build a single-hop probe
12501250
let (route, _, _, _) = get_route_and_payment_hash!(&nodes[0], nodes[1], 100_000);
12511251
assert!(nodes[0].node.send_probe(route.paths[0].clone()).is_err());
1252+
assert!(nodes[0].node.pending_outbound_payments.pending_outbound_payments.lock().unwrap().is_empty());
12521253

12531254
// Then build an actual two-hop probing path
12541255
let (route, _, _, _) = get_route_and_payment_hash!(&nodes[0], nodes[2], 100_000);
@@ -4375,3 +4376,77 @@ fn test_non_strict_forwarding() {
43754376
let events = nodes[0].node.get_and_clear_pending_events();
43764377
expect_payment_failed_conditions_event(events, payment_hash, false, PaymentFailedConditions::new().blamed_scid(routed_scid));
43774378
}
4379+
4380+
#[test]
4381+
fn remove_pending_outbounds_on_buggy_router() {
4382+
// Ensure that if a payment errors due to a bogus route, we'll abandon the payment and remove the
4383+
// pending outbound from storage.
4384+
let chanmon_cfgs = create_chanmon_cfgs(2);
4385+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
4386+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
4387+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
4388+
create_announced_chan_between_nodes(&nodes, 0, 1);
4389+
4390+
let amt_msat = 10_000;
4391+
let payment_id = PaymentId([42; 32]);
4392+
let payment_params = PaymentParameters::from_node_id(nodes[1].node.get_our_node_id(), 0)
4393+
.with_bolt11_features(nodes[1].node.bolt11_invoice_features()).unwrap();
4394+
let (mut route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, amt_msat);
4395+
4396+
// Extend the path by itself, essentially simulating route going through same channel twice
4397+
let cloned_hops = route.paths[0].hops.clone();
4398+
route.paths[0].hops.extend_from_slice(&cloned_hops);
4399+
let route_params = route.route_params.clone().unwrap();
4400+
nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone()));
4401+
4402+
nodes[0].node.send_payment(
4403+
payment_hash, RecipientOnionFields::secret_only(payment_secret), payment_id, route_params,
4404+
Retry::Attempts(1) // Even though another attempt is allowed, the payment should fail
4405+
).unwrap();
4406+
let events = nodes[0].node.get_and_clear_pending_events();
4407+
assert_eq!(events.len(), 2);
4408+
match &events[0] {
4409+
Event::PaymentPathFailed { failure, payment_failed_permanently, .. } => {
4410+
assert_eq!(failure, &PathFailure::InitialSend {
4411+
err: APIError::InvalidRoute { err: "Path went through the same channel twice".to_string() }
4412+
});
4413+
assert!(!payment_failed_permanently);
4414+
},
4415+
_ => panic!()
4416+
}
4417+
match events[1] {
4418+
Event::PaymentFailed { reason, .. } => {
4419+
assert_eq!(reason.unwrap(), PaymentFailureReason::UnexpectedError);
4420+
},
4421+
_ => panic!()
4422+
}
4423+
assert!(nodes[0].node.pending_outbound_payments.pending_outbound_payments.lock().unwrap().is_empty());
4424+
}
4425+
4426+
#[test]
4427+
fn remove_pending_outbound_probe_on_buggy_path() {
4428+
// Ensure that if a probe errors due to a bogus route, we'll return an error and remove the
4429+
// pending outbound from storage.
4430+
let chanmon_cfgs = create_chanmon_cfgs(2);
4431+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
4432+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
4433+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
4434+
create_announced_chan_between_nodes(&nodes, 0, 1);
4435+
4436+
let amt_msat = 10_000;
4437+
let payment_params = PaymentParameters::from_node_id(nodes[1].node.get_our_node_id(), 0)
4438+
.with_bolt11_features(nodes[1].node.bolt11_invoice_features()).unwrap();
4439+
let (mut route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, amt_msat);
4440+
4441+
// Extend the path by itself, essentially simulating route going through same channel twice
4442+
let cloned_hops = route.paths[0].hops.clone();
4443+
route.paths[0].hops.extend_from_slice(&cloned_hops);
4444+
4445+
assert_eq!(
4446+
nodes[0].node.send_probe(route.paths.pop().unwrap()).unwrap_err(),
4447+
ProbeSendFailure::ParameterError(
4448+
APIError::InvalidRoute { err: "Path went through the same channel twice".to_string() }
4449+
)
4450+
);
4451+
assert!(nodes[0].node.pending_outbound_payments.pending_outbound_payments.lock().unwrap().is_empty());
4452+
}

0 commit comments

Comments
 (0)