Skip to content

Commit 410e532

Browse files
committed
Move retry-limiting to retry_payment_with_route and count paths
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`, that broke and we started counting the number of discrete retries, even if multiple paths failed and were replaced 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 choose somewhat arbitrarily to count the retried paths, not the failed paths, which leaves the code simpler as we only have to count the paths in a `Route` in front of us in `retry_payment_with_route` rather than having to track the number of original paths and offset the retry count against that while handling failures.
1 parent 09ae81e commit 410e532

File tree

3 files changed

+72
-54
lines changed

3 files changed

+72
-54
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,7 +2489,7 @@ where
24892489
#[cfg(test)]
24902490
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> {
24912491
let best_block_height = self.best_block.read().unwrap().height();
2492-
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)
2492+
self.pending_outbound_payments.test_add_new_pending_payment(payment_hash, payment_secret, payment_id, route, None, &self.entropy_source, best_block_height)
24932493
}
24942494

24952495

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

lightning/src/ln/outbound_payment.rs

Lines changed: 52 additions & 42 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_retryable_now(&self) -> bool {
86-
if let PendingOutboundPayment::Retryable { retry_strategy, attempts, .. } = self {
87-
return retry_strategy.is_retryable_now(&attempts)
85+
fn auto_retries_remaining_now(&self) -> usize {
86+
match self {
87+
PendingOutboundPayment::Retryable { retry_strategy: Some(strategy), attempts, .. } => {
88+
strategy.retries_remaining_now(&attempts)
89+
},
90+
_ => 0,
91+
}
92+
}
93+
fn is_retryable_now(&self, pending_paths: usize) -> bool {
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.retries_remaining_now(&attempts) >= pending_paths
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,27 +226,27 @@ 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+
/// This is the number of additional paths along which the payment will be retried after the
230+
/// first payment. If two paths fail at around the same time and are retried with a single
231+
/// MPP path/HTLC, it will only count as a single retry.
218232
Attempts(usize),
219233
#[cfg(not(feature = "no-std"))]
220234
/// Time elapsed before abandoning retries for a payment.
221235
Timeout(core::time::Duration),
222236
}
223237

224238
impl Retry {
225-
pub(crate) fn is_retryable_now(&self, attempts: &PaymentAttempts) -> bool {
239+
pub(crate) fn retries_remaining_now(&self, attempts: &PaymentAttempts) -> usize {
226240
match (self, attempts) {
227241
(Retry::Attempts(max_retry_count), PaymentAttempts { count, .. }) => {
228-
max_retry_count > count
242+
*max_retry_count - count
229243
},
230244
#[cfg(all(not(feature = "no-std"), not(test)))]
231245
(Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) =>
232-
*max_duration >= std::time::Instant::now().duration_since(*first_attempted_at),
246+
if *max_duration >= std::time::Instant::now().duration_since(*first_attempted_at) { usize::max_value() } else { 0 },
233247
#[cfg(all(not(feature = "no-std"), test))]
234248
(Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) =>
235-
*max_duration >= SinceEpoch::now().duration_since(*first_attempted_at),
249+
if *max_duration >= SinceEpoch::now().duration_since(*first_attempted_at) { usize::max_value() } else { 0 },
236250
}
237251
}
238252
}
@@ -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,15 @@ 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+
let retries_remaining = pmt.auto_retries_remaining_now();
477+
if retries_remaining > 0 {
463478
if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, route_params: Some(params), .. } = pmt {
464479
if pending_amt_msat < total_msat {
465-
retry_id_route_params = Some((*pmt_id, params.clone()));
466-
pmt.increment_attempts();
480+
let mut params = params.clone();
481+
if params.payment_params.max_path_count as usize > retries_remaining {
482+
params.payment_params.max_path_count = retries_remaining as u8;
483+
}
484+
retry_id_route_params = Some((*pmt_id, params));
467485
break
468486
}
469487
}
@@ -509,35 +527,21 @@ impl OutboundPayments {
509527
}))?;
510528

511529
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)?;
530+
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)?;
513531
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)
514532
} else {
515533
self.retry_payment_with_route(&route, payment_id, entropy_source, node_signer, best_block_height, send_payment_along_path)
516534
};
517535
match res {
518536
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);
527537
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);
528538
log_info!(logger, "Errored retrying payment: {:?}", retry_res);
539+
if let Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError { err })) = &retry_res {
540+
if err.starts_with("Retries exhausted ") { return res; }
541+
}
529542
retry_res
530543
},
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-
544+
Err(PaymentSendFailure::PartialFailure { failed_paths_retry: Some(retry), .. }) => {
541545
// Some paths were sent, even if we failed to send the full MPP value our recipient may
542546
// misbehave and claim the funds, at which point we have to consider the payment sent, so
543547
// return `Ok()` here, ignoring any retry errors.
@@ -611,8 +615,14 @@ impl OutboundPayments {
611615
}));
612616
},
613617
};
618+
if !payment.is_retryable_now(route.paths.len()) {
619+
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
620+
err: format!("Retries exhausted for payment id {}", log_bytes!(payment_id.0)),
621+
}))
622+
}
614623
for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) {
615624
assert!(payment.insert(*session_priv_bytes, path));
625+
payment.increment_attempts();
616626
}
617627
res
618628
},
@@ -646,7 +656,7 @@ impl OutboundPayments {
646656
}
647657

648658
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)?;
659+
let onion_session_privs = self.add_new_pending_payment(payment_hash, None, payment_id, &route, None, None, entropy_source, best_block_height)?;
650660

651661
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) {
652662
Ok(()) => Ok((payment_hash, payment_id)),
@@ -660,14 +670,14 @@ impl OutboundPayments {
660670
#[cfg(test)]
661671
pub(super) fn test_add_new_pending_payment<ES: Deref>(
662672
&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
663-
route: &Route, retry_strategy: Retry, entropy_source: &ES, best_block_height: u32
673+
route: &Route, retry_strategy: Option<Retry>, entropy_source: &ES, best_block_height: u32
664674
) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
665675
self.add_new_pending_payment(payment_hash, payment_secret, payment_id, route, retry_strategy, None, entropy_source, best_block_height)
666676
}
667677

668678
pub(super) fn add_new_pending_payment<ES: Deref>(
669679
&self, payment_hash: PaymentHash, payment_secret: Option<PaymentSecret>, payment_id: PaymentId,
670-
route: &Route, retry_strategy: Retry, route_params: Option<RouteParameters>,
680+
route: &Route, retry_strategy: Option<Retry>, route_params: Option<RouteParameters>,
671681
entropy_source: &ES, best_block_height: u32
672682
) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
673683
let mut onion_session_privs = Vec::with_capacity(route.paths.len());
@@ -969,7 +979,7 @@ impl OutboundPayments {
969979
log_trace!(logger, "Received failure of HTLC with payment_hash {} after payment completion", log_bytes!(payment_hash.0));
970980
return
971981
}
972-
let is_retryable_now = payment.get().is_retryable_now();
982+
let is_retryable_now = payment.get().auto_retries_remaining_now() > 0;
973983
if let Some(scid) = short_channel_id {
974984
payment.get_mut().insert_previously_failed_scid(scid);
975985
}
@@ -1110,7 +1120,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
11101120
(0, session_privs, required),
11111121
(1, pending_fee_msat, option),
11121122
(2, payment_hash, required),
1113-
(not_written, retry_strategy, (static_value, Retry::Attempts(0))),
1123+
(not_written, retry_strategy, (static_value, None)),
11141124
(4, payment_secret, option),
11151125
(not_written, attempts, (static_value, PaymentAttempts::new())),
11161126
(6, total_msat, required),
@@ -1207,7 +1217,7 @@ mod tests {
12071217

12081218
let err = if on_retry {
12091219
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()),
1220+
&Route { paths: vec![], payment_params: None }, Some(Retry::Attempts(1)), Some(route_params.clone()),
12111221
&&keys_manager, 0).unwrap();
12121222
outbound_payments.pay_internal(
12131223
PaymentId([0; 32]), None, route_params, &&router, vec![], InFlightHtlcs::new(),

lightning/src/ln/payment_tests.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,7 +2134,7 @@ fn retry_multi_path_single_failed_payment() {
21342134
final_value_msat: 100_000_001, final_cltv_expiry_delta: TEST_FINAL_CLTV
21352135
}, Ok(route.clone()));
21362136

2137-
nodes[0].node.send_payment_with_retry(payment_hash, &Some(payment_secret), PaymentId(payment_hash.0), route_params, Retry::Attempts(1)).unwrap();
2137+
nodes[0].node.send_payment_with_retry(payment_hash, &Some(payment_secret), PaymentId(payment_hash.0), route_params, Retry::Attempts(2)).unwrap();
21382138
let htlc_msgs = nodes[0].node.get_and_clear_pending_msg_events();
21392139
assert_eq!(htlc_msgs.len(), 2);
21402140
check_added_monitors!(nodes[0], 2);
@@ -2195,7 +2195,7 @@ fn immediate_retry_on_failure() {
21952195
final_value_msat: amt_msat, final_cltv_expiry_delta: TEST_FINAL_CLTV
21962196
}, Ok(route.clone()));
21972197

2198-
nodes[0].node.send_payment_with_retry(payment_hash, &Some(payment_secret), PaymentId(payment_hash.0), route_params, Retry::Attempts(1)).unwrap();
2198+
nodes[0].node.send_payment_with_retry(payment_hash, &Some(payment_secret), PaymentId(payment_hash.0), route_params, Retry::Attempts(2)).unwrap();
21992199
let htlc_msgs = nodes[0].node.get_and_clear_pending_msg_events();
22002200
assert_eq!(htlc_msgs.len(), 2);
22012201
check_added_monitors!(nodes[0], 2);
@@ -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,12 @@ 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+
// We'll only have one retry left at the end, so we'll hlepfully get a max_path_count of 1
2286+
second_payment_params.max_path_count = 1;
2287+
route.paths.remove(1);
2288+
route.paths[0][1].fee_msat = amt_msat;
22872289
nodes[0].router.expect_find_route(RouteParameters {
22882290
payment_params: second_payment_params,
22892291
final_value_msat: amt_msat, final_cltv_expiry_delta: TEST_FINAL_CLTV,
@@ -2351,10 +2353,16 @@ fn no_extra_retries_on_back_to_back_fail() {
23512353

23522354
// At this point A has sent two HTLCs which both failed due to lack of fee. It now has two
23532355
// 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.
2356+
// with it set.
2357+
//
2358+
// Previously, we retried payments in an event consumer, which would retry each
2359+
// `PaymentPathFailed` individually. In that setup, we had retried the payment in response to
2360+
// the first `PaymentPathFailed`, then seen the second `PaymentPathFailed` with
2361+
// `all_paths_failed` set and assumed the payment was completely failed. We ultimately fixed it
2362+
// by adding the `PaymentFailed` event.
2363+
//
2364+
// Because we now retry payments as a batch, we simply return a single-path route in the
2365+
// second, batched, request, have that fail, then complete the payment via `abandon_payment`.
23582366
let mut events = nodes[0].node.get_and_clear_pending_events();
23592367
assert_eq!(events.len(), 4);
23602368
match events[0] {

0 commit comments

Comments
 (0)