Skip to content

Commit 56a2035

Browse files
committed
Move retry-limiting to retry_payment_with_route
The documentation for `Retry` is very clear that it counts the number of failed paths, not discrete retries. When we added retries internally in `ChannelManager`, we switched to counting the number if discrete retries, even if multiple paths failed and were replace with multiple MPP HTLCs. Because we are now rewriting retries, we take this opportunity to reduce the places where retries are analyzed, specifically a good chunk of code is removed from `pay_internal`. Because we now retry multiple failed paths with one single retry, we keep the new behavior, updating the docs on `Retry` to describe the new behavior.
1 parent e337032 commit 56a2035

File tree

3 files changed

+58
-46
lines changed

3 files changed

+58
-46
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2493,7 +2493,7 @@ where
24932493
#[cfg(test)]
24942494
pub(crate) fn test_add_new_pending_payment(&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId, route: &Route) -> Result<Vec<[u8; 32]>, PaymentSendFailure> {
24952495
let best_block_height = self.best_block.read().unwrap().height();
2496-
self.pending_outbound_payments.test_add_new_pending_payment(payment_hash, payment_secret, payment_id, route, Retry::Attempts(0), &self.entropy_source, best_block_height)
2496+
self.pending_outbound_payments.test_add_new_pending_payment(payment_hash, payment_secret, payment_id, route, None, &self.entropy_source, best_block_height)
24972497
}
24982498

24992499

@@ -7361,7 +7361,7 @@ where
73617361
hash_map::Entry::Vacant(entry) => {
73627362
let path_fee = path.get_path_fees();
73637363
entry.insert(PendingOutboundPayment::Retryable {
7364-
retry_strategy: Retry::Attempts(0),
7364+
retry_strategy: None,
73657365
attempts: PaymentAttempts::new(),
73667366
route_params: None,
73677367
session_privs: [session_priv_bytes].iter().map(|a| *a).collect(),

lightning/src/ln/outbound_payment.rs

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub(crate) enum PendingOutboundPayment {
4141
session_privs: HashSet<[u8; 32]>,
4242
},
4343
Retryable {
44-
retry_strategy: Retry,
44+
retry_strategy: Option<Retry>,
4545
attempts: PaymentAttempts,
4646
route_params: Option<RouteParameters>,
4747
session_privs: HashSet<[u8; 32]>,
@@ -82,11 +82,25 @@ impl PendingOutboundPayment {
8282
attempts.count += 1;
8383
}
8484
}
85+
fn is_auto_retryable_now(&self) -> bool {
86+
match self {
87+
PendingOutboundPayment::Retryable { retry_strategy: Some(strategy), attempts, .. } => {
88+
strategy.is_retryable_now(&attempts)
89+
},
90+
_ => false,
91+
}
92+
}
8593
fn is_retryable_now(&self) -> bool {
86-
if let PendingOutboundPayment::Retryable { retry_strategy, attempts, .. } = self {
87-
return retry_strategy.is_retryable_now(&attempts)
94+
match self {
95+
PendingOutboundPayment::Retryable { retry_strategy: None, .. } => {
96+
// We're handling retries manually, we can always retry.
97+
true
98+
},
99+
PendingOutboundPayment::Retryable { retry_strategy: Some(strategy), attempts, .. } => {
100+
strategy.is_retryable_now(&attempts)
101+
},
102+
_ => false,
88103
}
89-
false
90104
}
91105
pub fn insert_previously_failed_scid(&mut self, scid: u64) {
92106
if let PendingOutboundPayment::Retryable { route_params: Some(params), .. } = self {
@@ -212,9 +226,9 @@ impl PendingOutboundPayment {
212226
pub enum Retry {
213227
/// Max number of attempts to retry payment.
214228
///
215-
/// Note that this is the number of *path* failures, not full payment retries. For multi-path
216-
/// payments, if this is less than the total number of paths, we will never even retry all of the
217-
/// payment's paths.
229+
/// Each attempt may be multiple HTLCs along multiple paths if the router decides to split up a
230+
/// retry, and may retry multiple failed HTLCs at once if they failed around the same time and
231+
/// were retried along a route from a single call to [`Router::find_route`].
218232
Attempts(usize),
219233
#[cfg(not(feature = "no-std"))]
220234
/// Time elapsed before abandoning retries for a payment.
@@ -409,7 +423,7 @@ impl OutboundPayments {
409423
F: Fn(&Vec<RouteHop>, &Option<PaymentParameters>, &PaymentHash, &Option<PaymentSecret>, u64,
410424
u32, PaymentId, &Option<PaymentPreimage>, [u8; 32]) -> Result<(), APIError>
411425
{
412-
let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, route, Retry::Attempts(0), None, entropy_source, best_block_height)?;
426+
let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, route, None, None, entropy_source, best_block_height)?;
413427
self.pay_route_internal(route, payment_hash, payment_secret, None, payment_id, None,
414428
onion_session_privs, node_signer, best_block_height, &send_payment_along_path)
415429
.map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e })
@@ -430,7 +444,7 @@ impl OutboundPayments {
430444
None => PaymentPreimage(entropy_source.get_secure_random_bytes()),
431445
};
432446
let payment_hash = PaymentHash(Sha256::hash(&preimage.0).into_inner());
433-
let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, Retry::Attempts(0), None, entropy_source, best_block_height)?;
447+
let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, None, None, entropy_source, best_block_height)?;
434448

435449
match self.pay_route_internal(route, payment_hash, &None, Some(preimage), payment_id, None, onion_session_privs, node_signer, best_block_height, &send_payment_along_path) {
436450
Ok(()) => Ok(payment_hash),
@@ -459,11 +473,10 @@ impl OutboundPayments {
459473
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
460474
let mut retry_id_route_params = None;
461475
for (pmt_id, pmt) in outbounds.iter_mut() {
462-
if pmt.is_retryable_now() {
476+
if pmt.is_auto_retryable_now() {
463477
if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, route_params: Some(params), .. } = pmt {
464478
if pending_amt_msat < total_msat {
465479
retry_id_route_params = Some((*pmt_id, params.clone()));
466-
pmt.increment_attempts();
467480
break
468481
}
469482
}
@@ -509,35 +522,21 @@ impl OutboundPayments {
509522
}))?;
510523

511524
let res = if let Some((payment_hash, payment_secret, retry_strategy)) = initial_send_info {
512-
let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, &route, retry_strategy, Some(route_params.clone()), entropy_source, best_block_height)?;
525+
let onion_session_privs = self.add_new_pending_payment(payment_hash, *payment_secret, payment_id, &route, Some(retry_strategy), Some(route_params.clone()), entropy_source, best_block_height)?;
513526
self.pay_route_internal(&route, payment_hash, payment_secret, None, payment_id, None, onion_session_privs, node_signer, best_block_height, send_payment_along_path)
514527
} else {
515528
self.retry_payment_with_route(&route, payment_id, entropy_source, node_signer, best_block_height, send_payment_along_path)
516529
};
517530
match res {
518531
Err(PaymentSendFailure::AllFailedResendSafe(_)) => {
519-
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
520-
if let Some(payment) = outbounds.get_mut(&payment_id) {
521-
let retryable = payment.is_retryable_now();
522-
if retryable {
523-
payment.increment_attempts();
524-
} else { return res }
525-
} else { return res }
526-
core::mem::drop(outbounds);
527532
let retry_res = self.pay_internal(payment_id, None, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, logger, send_payment_along_path);
528533
log_info!(logger, "Result retrying payment id {}: {:?}", log_bytes!(payment_id.0), retry_res);
534+
if let Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError { err })) = &retry_res {
535+
if err.starts_with("Retries exhausted ") { return res; }
536+
}
529537
retry_res
530538
},
531-
Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, .. }) => {
532-
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
533-
if let Some(payment) = outbounds.get_mut(&payment_id) {
534-
let retryable = payment.is_retryable_now();
535-
if retryable {
536-
payment.increment_attempts();
537-
} else { return Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, payment_id }) }
538-
} else { return Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), results, payment_id }) }
539-
core::mem::drop(outbounds);
540-
539+
Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), .. }) => {
541540
// Some paths were sent, even if we failed to send the full MPP value our recipient may
542541
// misbehave and claim the funds, at which point we have to consider the payment sent, so
543542
// return `Ok()` here, ignoring any retry errors.
@@ -611,6 +610,12 @@ impl OutboundPayments {
611610
}));
612611
},
613612
};
613+
if !payment.is_retryable_now() {
614+
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
615+
err: format!("Retries exhausted for payment id {}", log_bytes!(payment_id.0)),
616+
}))
617+
}
618+
payment.increment_attempts();
614619
for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) {
615620
assert!(payment.insert(*session_priv_bytes, path));
616621
}
@@ -646,7 +651,7 @@ impl OutboundPayments {
646651
}
647652

648653
let route = Route { paths: vec![hops], payment_params: None };
649-
let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, Retry::Attempts(0), None, entropy_source, best_block_height)?;
654+
let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, None, None, entropy_source, best_block_height)?;
650655

651656
match self.pay_route_internal(&route, payment_hash, &None, None, payment_id, None, onion_session_privs, node_signer, best_block_height, &send_payment_along_path) {
652657
Ok(()) => Ok((payment_hash, payment_id)),
@@ -660,14 +665,14 @@ impl OutboundPayments {
660665
#[cfg(test)]
661666
pub(super) fn test_add_new_pending_payment<ES: Deref>(
662667
&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
663-
route: &Route, retry_strategy: Retry, entropy_source: &ES, best_block_height: u32
668+
route: &Route, retry_strategy: Option<Retry>, entropy_source: &ES, best_block_height: u32
664669
) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
665670
self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, retry_strategy, None, entropy_source, best_block_height)
666671
}
667672

668673
pub(super) fn add_new_pending_payment<ES: Deref>(
669674
&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
670-
route: &Route, retry_strategy: Retry, route_params: Option<RouteParameters>,
675+
route: &Route, retry_strategy: Option<Retry>, route_params: Option<RouteParameters>,
671676
entropy_source: &ES, best_block_height: u32
672677
) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
673678
let mut onion_session_privs = Vec::with_capacity(route.paths.len());
@@ -969,7 +974,7 @@ impl OutboundPayments {
969974
log_trace!(logger, "Received failure of HTLC with payment_hash {} after payment completion", log_bytes!(payment_hash.0));
970975
return
971976
}
972-
let is_retryable_now = payment.get().is_retryable_now();
977+
let is_retryable_now = payment.get().is_auto_retryable_now();
973978
if let Some(scid) = short_channel_id {
974979
payment.get_mut().insert_previously_failed_scid(scid);
975980
}
@@ -1110,7 +1115,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
11101115
(0, session_privs, required),
11111116
(1, pending_fee_msat, option),
11121117
(2, payment_hash, required),
1113-
(not_written, retry_strategy, (static_value, Retry::Attempts(0))),
1118+
(not_written, retry_strategy, (static_value, None)),
11141119
(4, payment_secret, option),
11151120
(not_written, attempts, (static_value, PaymentAttempts::new())),
11161121
(6, total_msat, required),
@@ -1207,7 +1212,7 @@ mod tests {
12071212

12081213
let err = if on_retry {
12091214
outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), None, PaymentId([0; 32]),
1210-
&Route { paths: vec![], payment_params: None }, Retry::Attempts(1), Some(route_params.clone()),
1215+
&Route { paths: vec![], payment_params: None }, Some(Retry::Attempts(1)), Some(route_params.clone()),
12111216
&&keys_manager, 0).unwrap();
12121217
outbound_payments.pay_internal(
12131218
PaymentId([0; 32]), None, route_params, &&router, vec![], InFlightHtlcs::new(),

lightning/src/ln/payment_tests.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2251,7 +2251,7 @@ fn no_extra_retries_on_back_to_back_fail() {
22512251
node_features: nodes[1].node.node_features(),
22522252
short_channel_id: chan_1_scid,
22532253
channel_features: nodes[1].node.channel_features(),
2254-
fee_msat: 0,
2254+
fee_msat: 0, // nodes[1] will fail the payment as we don't pay its fee
22552255
cltv_expiry_delta: 100,
22562256
}, RouteHop {
22572257
pubkey: nodes[2].node.get_our_node_id(),
@@ -2266,7 +2266,7 @@ fn no_extra_retries_on_back_to_back_fail() {
22662266
node_features: nodes[1].node.node_features(),
22672267
short_channel_id: chan_1_scid,
22682268
channel_features: nodes[1].node.channel_features(),
2269-
fee_msat: 0,
2269+
fee_msat: 0, // nodes[1] will fail the payment as we don't pay its fee
22702270
cltv_expiry_delta: 100,
22712271
}, RouteHop {
22722272
pubkey: nodes[2].node.get_our_node_id(),
@@ -2280,10 +2280,11 @@ fn no_extra_retries_on_back_to_back_fail() {
22802280
payment_params: Some(PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV)),
22812281
};
22822282
nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone()));
2283-
// On retry, we'll only be asked for one path
2284-
route.paths.remove(1);
22852283
let mut second_payment_params = route_params.payment_params.clone();
22862284
second_payment_params.previously_failed_channels = vec![chan_2_scid, chan_2_scid];
2285+
// On retry, we'll only return one path
2286+
route.paths.remove(1);
2287+
route.paths[0][1].fee_msat = amt_msat;
22872288
nodes[0].router.expect_find_route(RouteParameters {
22882289
payment_params: second_payment_params,
22892290
final_value_msat: amt_msat, final_cltv_expiry_delta: TEST_FINAL_CLTV,
@@ -2351,10 +2352,16 @@ fn no_extra_retries_on_back_to_back_fail() {
23512352

23522353
// At this point A has sent two HTLCs which both failed due to lack of fee. It now has two
23532354
// pending `PaymentPathFailed` events, one with `all_paths_failed` unset, and the second
2354-
// with it set. The first event will use up the only retry we are allowed, with the second
2355-
// `PaymentPathFailed` being passed up to the user (us, in this case). Previously, we'd
2356-
// treated this as "HTLC complete" and dropped the retry counter, causing us to retry again
2357-
// if the final HTLC failed.
2355+
// with it set.
2356+
//
2357+
// Previously, we retried payments in an event consumer, which would retry each
2358+
// `PaymentPathFailed` individually. In that setup, we had retried the payment in response to
2359+
// the first `PaymentPathFailed`, then seen the second `PaymentPathFailed` with
2360+
// `all_paths_failed` set and assumed the payment was completely failed. We ultimately fixed it
2361+
// by adding the `PaymentFailed` event.
2362+
//
2363+
// Because we now retry payments as a batch, we simply return a single-path route in the
2364+
// second, batched, request, have that fail, then complete the payment via `abandon_payment`.
23582365
let mut events = nodes[0].node.get_and_clear_pending_events();
23592366
assert_eq!(events.len(), 4);
23602367
match events[0] {

0 commit comments

Comments
 (0)