Skip to content

Commit e17c968

Browse files
committed
Rewrite InvoicePayer retry to correctly handle MPP partial failures
This rewrites a good chunk of the retry logic in `InvoicePayer` to address two issues: * it was not considering the return value of `send_payment` (and `retry_payment`) may indicate a failure on some paths but not others, * it was not considering that more failures may still come later when removing elements from the retry count map. This could result in us seeing an MPP-partial-failure, failing to retry, removing the retries count entry, and then retrying other parts, potentially forever.
1 parent 9db2306 commit e17c968

File tree

1 file changed

+138
-74
lines changed

1 file changed

+138
-74
lines changed

lightning-invoice/src/payment.rs

+138-74
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ where
201201
if invoice.amount_milli_satoshis().is_none() {
202202
Err(PaymentError::Invoice("amount missing"))
203203
} else {
204-
self.pay_invoice_internal(invoice, None)
204+
self.pay_invoice_internal(invoice, None, 0)
205205
}
206206
}
207207

@@ -213,57 +213,142 @@ where
213213
if invoice.amount_milli_satoshis().is_some() {
214214
Err(PaymentError::Invoice("amount unexpected"))
215215
} else {
216-
self.pay_invoice_internal(invoice, Some(amount_msats))
216+
self.pay_invoice_internal(invoice, Some(amount_msats), 0)
217217
}
218218
}
219219

220220
fn pay_invoice_internal(
221-
&self, invoice: &Invoice, amount_msats: Option<u64>
221+
&self, invoice: &Invoice, amount_msats: Option<u64>, retry_count: usize
222222
) -> Result<PaymentId, PaymentError> {
223223
debug_assert!(invoice.amount_milli_satoshis().is_some() ^ amount_msats.is_some());
224224
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
225-
let mut payment_cache = self.payment_cache.lock().unwrap();
226-
match payment_cache.entry(payment_hash) {
227-
hash_map::Entry::Vacant(entry) => {
228-
let payer = self.payer.node_id();
229-
let mut payee = Payee::new(invoice.recover_payee_pub_key())
230-
.with_expiry_time(expiry_time_from_unix_epoch(&invoice).as_secs())
231-
.with_route_hints(invoice.route_hints());
232-
if let Some(features) = invoice.features() {
233-
payee = payee.with_features(features.clone());
234-
}
235-
let params = RouteParameters {
236-
payee,
237-
final_value_msat: invoice.amount_milli_satoshis().or(amount_msats).unwrap(),
238-
final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
225+
let failed_paths_data = loop {
226+
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
227+
let mut payment_cache = self.payment_cache.lock().unwrap();
228+
match payment_cache.entry(payment_hash) {
229+
hash_map::Entry::Vacant(entry) => {
230+
let payer = self.payer.node_id();
231+
let mut payee = Payee::new(invoice.recover_payee_pub_key())
232+
.with_expiry_time(expiry_time_from_unix_epoch(&invoice).as_secs())
233+
.with_route_hints(invoice.route_hints());
234+
if let Some(features) = invoice.features() {
235+
payee = payee.with_features(features.clone());
236+
}
237+
let params = RouteParameters {
238+
payee,
239+
final_value_msat: invoice.amount_milli_satoshis().or(amount_msats).unwrap(),
240+
final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
241+
};
242+
let first_hops = self.payer.first_hops();
243+
let route = self.router.find_route(
244+
&payer,
245+
&params,
246+
Some(&first_hops.iter().collect::<Vec<_>>()),
247+
).map_err(|e| PaymentError::Routing(e))?;
248+
249+
let payment_secret = Some(invoice.payment_secret().clone());
250+
let payment_id = match self.payer.send_payment(&route, payment_hash, &payment_secret) {
251+
Ok(payment_id) => payment_id,
252+
Err(PaymentSendFailure::ParameterError(e)) =>
253+
Err(PaymentError::Sending(PaymentSendFailure::ParameterError(e)))?,
254+
Err(PaymentSendFailure::PathParameterError(e)) =>
255+
Err(PaymentError::Sending(PaymentSendFailure::PathParameterError(e)))?,
256+
Err(PaymentSendFailure::AllFailedRetrySafe(e)) => {
257+
if retry_count >= self.retry_attempts.0 {
258+
Err(PaymentError::Sending(PaymentSendFailure::AllFailedRetrySafe(e)))?
259+
}
260+
break None;
261+
},
262+
Err(PaymentSendFailure::PartialFailure { results: _, failed_paths_retry, payment_id }) => {
263+
if let Some(retry_data) = failed_paths_retry {
264+
entry.insert(retry_count);
265+
break Some((retry_data, payment_id));
266+
} else {
267+
// This may happen if we send a payment and some paths fail, but
268+
// only due to a temporary monitor failure or the like, implying
269+
// they're really in-flight, but we haven't sent the initial
270+
// HTLC-Add messages yet.
271+
payment_id
272+
}
273+
},
274+
};
275+
entry.insert(retry_count);
276+
return Ok(payment_id);
277+
},
278+
hash_map::Entry::Occupied(_) => Err(PaymentError::Invoice("payment pending"))?,
279+
}
280+
};
281+
if let Some((retry, payment_id)) = failed_paths_data {
282+
// Some paths were sent, even if we failed to send the full MPP value our recipient may
283+
// misbehave and claim the funds, at which point we have to consider the payment sent,
284+
// so return `Ok()` here, ignoring any retry errors.
285+
let _ = self.retry_payment(true, payment_id, payment_hash, &retry);
286+
Ok(payment_id)
287+
} else {
288+
self.pay_invoice_internal(invoice, amount_msats, retry_count + 1)
289+
}
290+
}
291+
292+
fn retry_payment(&self, other_paths_pending: bool, payment_id: PaymentId, payment_hash: PaymentHash, params: &RouteParameters)
293+
-> Result<(), ()> {
294+
let route;
295+
{
296+
let mut payment_cache = self.payment_cache.lock().unwrap();
297+
let entry = loop {
298+
let entry = payment_cache.entry(payment_hash);
299+
match entry {
300+
hash_map::Entry::Occupied(_) => break entry,
301+
hash_map::Entry::Vacant(entry) => entry.insert(0),
239302
};
303+
};
304+
if let hash_map::Entry::Occupied(mut entry) = entry {
305+
let max_payment_attempts = self.retry_attempts.0 + 1;
306+
let attempts = entry.get_mut();
307+
*attempts += 1;
308+
309+
if *attempts >= max_payment_attempts {
310+
log_trace!(self.logger, "Payment {} exceeded maximum attempts; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
311+
if !other_paths_pending { entry.remove(); }
312+
return Err(());
313+
} else if has_expired(params) {
314+
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
315+
if !other_paths_pending { entry.remove(); }
316+
return Err(());
317+
}
318+
319+
let payer = self.payer.node_id();
240320
let first_hops = self.payer.first_hops();
241-
let route = self.router.find_route(
242-
&payer,
243-
&params,
244-
Some(&first_hops.iter().collect::<Vec<_>>()),
245-
).map_err(|e| PaymentError::Routing(e))?;
246-
247-
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
248-
let payment_secret = Some(invoice.payment_secret().clone());
249-
let payment_id = self.payer.send_payment(&route, payment_hash, &payment_secret)
250-
.map_err(|e| PaymentError::Sending(e))?;
251-
entry.insert(0);
252-
Ok(payment_id)
253-
},
254-
hash_map::Entry::Occupied(_) => Err(PaymentError::Invoice("payment pending")),
321+
route = self.router.find_route(&payer, &params, Some(&first_hops.iter().collect::<Vec<_>>()));
322+
if route.is_err() {
323+
log_trace!(self.logger, "Failed to find a route for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
324+
if !other_paths_pending { entry.remove(); }
325+
return Err(());
326+
}
327+
} else {
328+
unreachable!();
329+
}
255330
}
256-
}
257331

258-
fn retry_payment(
259-
&self, payment_id: PaymentId, params: &RouteParameters
260-
) -> Result<(), PaymentError> {
261-
let payer = self.payer.node_id();
262-
let first_hops = self.payer.first_hops();
263-
let route = self.router.find_route(
264-
&payer, &params, Some(&first_hops.iter().collect::<Vec<_>>())
265-
).map_err(|e| PaymentError::Routing(e))?;
266-
self.payer.retry_payment(&route, payment_id).map_err(|e| PaymentError::Sending(e))
332+
let retry_res = self.payer.retry_payment(&route.unwrap(), payment_id);
333+
match retry_res {
334+
Ok(()) => Ok(()),
335+
Err(PaymentSendFailure::ParameterError(_))|
336+
Err(PaymentSendFailure::PathParameterError(_)) => {
337+
log_trace!(self.logger, "Failed to retry for payment {} due to bogus route/payment data, not retrying.", log_bytes!(payment_hash.0));
338+
if !other_paths_pending { self.payment_cache.lock().unwrap().remove(&payment_hash); }
339+
return Err(());
340+
},
341+
Err(PaymentSendFailure::AllFailedRetrySafe(_)) => {
342+
self.retry_payment(other_paths_pending, payment_id, payment_hash, params)
343+
},
344+
Err(PaymentSendFailure::PartialFailure { results: _, failed_paths_retry, .. }) => {
345+
if let Some(retry) = failed_paths_retry {
346+
self.retry_payment(true, payment_id, payment_hash, &retry)
347+
} else {
348+
Ok(())
349+
}
350+
},
351+
}
267352
}
268353

269354
/// Removes the payment cached by the given payment hash.
@@ -293,42 +378,21 @@ where
293378
{
294379
fn handle_event(&self, event: &Event) {
295380
match event {
296-
Event::PaymentPathFailed { payment_id, payment_hash, rejected_by_dest, retry, .. } => {
297-
let mut payment_cache = self.payment_cache.lock().unwrap();
298-
let entry = loop {
299-
let entry = payment_cache.entry(*payment_hash);
300-
match entry {
301-
hash_map::Entry::Occupied(_) => break entry,
302-
hash_map::Entry::Vacant(entry) => entry.insert(0),
303-
};
304-
};
305-
if let hash_map::Entry::Occupied(mut entry) = entry {
306-
let max_payment_attempts = self.retry_attempts.0 + 1;
307-
let attempts = entry.get_mut();
308-
*attempts += 1;
309-
310-
if *rejected_by_dest {
311-
log_trace!(self.logger, "Payment {} rejected by destination; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
312-
} else if payment_id.is_none() {
313-
log_trace!(self.logger, "Payment {} has no id; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
314-
} else if *attempts >= max_payment_attempts {
315-
log_trace!(self.logger, "Payment {} exceeded maximum attempts; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
316-
} else if retry.is_none() {
317-
log_trace!(self.logger, "Payment {} missing retry params; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
318-
} else if has_expired(retry.as_ref().unwrap()) {
319-
log_trace!(self.logger, "Invoice expired for payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
320-
} else if self.retry_payment(*payment_id.as_ref().unwrap(), retry.as_ref().unwrap()).is_err() {
321-
log_trace!(self.logger, "Error retrying payment {}; not retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
322-
} else {
323-
log_trace!(self.logger, "Payment {} failed; retrying (attempts: {})", log_bytes!(payment_hash.0), attempts);
381+
Event::PaymentPathFailed { all_paths_failed, payment_id, payment_hash, rejected_by_dest, retry, .. } => {
382+
if *rejected_by_dest {
383+
log_trace!(self.logger, "Payment {} rejected by destination; not retrying", log_bytes!(payment_hash.0));
384+
if *all_paths_failed { self.payment_cache.lock().unwrap().remove(payment_hash); }
385+
} else if payment_id.is_none() {
386+
log_trace!(self.logger, "Payment {} has no id; not retrying", log_bytes!(payment_hash.0));
387+
if *all_paths_failed { self.payment_cache.lock().unwrap().remove(payment_hash); }
388+
} else if let Some(params) = retry {
389+
if self.retry_payment(!all_paths_failed, payment_id.unwrap(), *payment_hash, params).is_ok() {
390+
// We retried at least somewhat, don't provide the PaymentPathFailed event to the user.
324391
return;
325392
}
326-
327-
// Either the payment was rejected, the maximum attempts were exceeded, or an
328-
// error occurred when attempting to retry.
329-
entry.remove();
330393
} else {
331-
unreachable!();
394+
log_trace!(self.logger, "Payment {} missing retry params; not retrying", log_bytes!(payment_hash.0));
395+
if *all_paths_failed { self.payment_cache.lock().unwrap().remove(payment_hash); }
332396
}
333397
},
334398
Event::PaymentSent { payment_hash, .. } => {

0 commit comments

Comments
 (0)