Skip to content

Commit 0b3240e

Browse files
committed
Add a variant to PendingOutboundPayment for retries-exceeded
When a payer gives up trying to retry a payment, they don't know for sure what the current state of the event queue is. Specifically, they cannot be sure that there are not multiple additional `PaymentPathFailed` or even `PaymentSuccess` events pending which they will see later. Thus, they have a very hard time identifying whether a payment has truly failed (and informing the UI of that fact) or if it is still pending. See [1] for more information. In order to avoid this mess, we will resolve it here by having the payer give `ChannelManager` a bit more information - when they have given up on a payment - and using that to generate a `PaymentFailed` event when all paths have failed. This commit adds the neccessary storage and changes for the new state inside `ChannelManager` and a public method to mark a payment as failed, the next few commits will add the new `Event` and use the new features in our `PaymentRetrier`. [1] #1164
1 parent 8c9615e commit 0b3240e

File tree

1 file changed

+74
-8
lines changed

1 file changed

+74
-8
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,17 @@ pub(crate) enum PendingOutboundPayment {
454454
session_privs: HashSet<[u8; 32]>,
455455
payment_hash: Option<PaymentHash>,
456456
},
457+
/// When a payer gives up trying to retry a payment, they inform us, letting us generate a
458+
/// `PaymentFailed` event when all HTLCs have irrevocably failed. This avoids a number of race
459+
/// conditions in MPP-aware payment retriers (1), where the possibility of multiple
460+
/// `PaymentPathFailed` events with `all_paths_failed` can be pending at once, confusing a
461+
/// downstream event handler as to when a payment has actually failed.
462+
///
463+
/// (1) https://github.com/lightningdevkit/rust-lightning/issues/1164
464+
Abandoned {
465+
session_privs: HashSet<[u8; 32]>,
466+
payment_hash: PaymentHash,
467+
},
457468
}
458469

459470
impl PendingOutboundPayment {
@@ -469,6 +480,12 @@ impl PendingOutboundPayment {
469480
_ => false,
470481
}
471482
}
483+
fn abandoned(&self) -> bool {
484+
match self {
485+
PendingOutboundPayment::Abandoned { .. } => true,
486+
_ => false,
487+
}
488+
}
472489
fn get_pending_fee_msat(&self) -> Option<u64> {
473490
match self {
474491
PendingOutboundPayment::Retryable { pending_fee_msat, .. } => pending_fee_msat.clone(),
@@ -481,6 +498,7 @@ impl PendingOutboundPayment {
481498
PendingOutboundPayment::Legacy { .. } => None,
482499
PendingOutboundPayment::Retryable { payment_hash, .. } => Some(*payment_hash),
483500
PendingOutboundPayment::Fulfilled { payment_hash, .. } => *payment_hash,
501+
PendingOutboundPayment::Abandoned { payment_hash, .. } => Some(*payment_hash),
484502
}
485503
}
486504

@@ -489,19 +507,38 @@ impl PendingOutboundPayment {
489507
core::mem::swap(&mut session_privs, match self {
490508
PendingOutboundPayment::Legacy { session_privs } |
491509
PendingOutboundPayment::Retryable { session_privs, .. } |
492-
PendingOutboundPayment::Fulfilled { session_privs, .. }
493-
=> session_privs
510+
PendingOutboundPayment::Fulfilled { session_privs, .. } |
511+
PendingOutboundPayment::Abandoned { session_privs, .. }
512+
=> session_privs,
494513
});
495514
let payment_hash = self.payment_hash();
496515
*self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash };
497516
}
498517

518+
fn mark_abandoned(&mut self) -> Result<(), ()> {
519+
let mut session_privs = HashSet::new();
520+
let our_payment_hash;
521+
core::mem::swap(&mut session_privs, match self {
522+
PendingOutboundPayment::Legacy { .. } |
523+
PendingOutboundPayment::Fulfilled { .. } =>
524+
return Err(()),
525+
PendingOutboundPayment::Retryable { session_privs, payment_hash, .. } |
526+
PendingOutboundPayment::Abandoned { session_privs, payment_hash, .. } => {
527+
our_payment_hash = *payment_hash;
528+
session_privs
529+
},
530+
});
531+
*self = PendingOutboundPayment::Abandoned { session_privs, payment_hash: our_payment_hash };
532+
Ok(())
533+
}
534+
499535
/// panics if path is None and !self.is_fulfilled
500536
fn remove(&mut self, session_priv: &[u8; 32], path: Option<&Vec<RouteHop>>) -> bool {
501537
let remove_res = match self {
502538
PendingOutboundPayment::Legacy { session_privs } |
503539
PendingOutboundPayment::Retryable { session_privs, .. } |
504-
PendingOutboundPayment::Fulfilled { session_privs, .. } => {
540+
PendingOutboundPayment::Fulfilled { session_privs, .. } |
541+
PendingOutboundPayment::Abandoned { session_privs, .. } => {
505542
session_privs.remove(session_priv)
506543
}
507544
};
@@ -524,7 +561,8 @@ impl PendingOutboundPayment {
524561
PendingOutboundPayment::Retryable { session_privs, .. } => {
525562
session_privs.insert(session_priv)
526563
}
527-
PendingOutboundPayment::Fulfilled { .. } => false
564+
PendingOutboundPayment::Fulfilled { .. } => false,
565+
PendingOutboundPayment::Abandoned { .. } => false,
528566
};
529567
if insert_res {
530568
if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self {
@@ -542,7 +580,8 @@ impl PendingOutboundPayment {
542580
match self {
543581
PendingOutboundPayment::Legacy { session_privs } |
544582
PendingOutboundPayment::Retryable { session_privs, .. } |
545-
PendingOutboundPayment::Fulfilled { session_privs, .. } => {
583+
PendingOutboundPayment::Fulfilled { session_privs, .. } |
584+
PendingOutboundPayment::Abandoned { session_privs, .. } => {
546585
session_privs.len()
547586
}
548587
}
@@ -2313,10 +2352,12 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
23132352
///
23142353
/// Errors returned are a superset of those returned from [`send_payment`], so see
23152354
/// [`send_payment`] documentation for more details on errors. This method will also error if the
2316-
/// retry amount puts the payment more than 10% over the payment's total amount, or if the payment
2317-
/// for the given `payment_id` cannot be found (likely due to timeout or success).
2355+
/// retry amount puts the payment more than 10% over the payment's total amount, if the payment
2356+
/// for the given `payment_id` cannot be found (likely due to timeout or success), or if
2357+
/// further retries have been disabled with [`abandon_payment`].
23182358
///
23192359
/// [`send_payment`]: [`ChannelManager::send_payment`]
2360+
/// [`abandon_payment`]: [`ChannelManager::abandon_payment`]
23202361
pub fn retry_payment(&self, route: &Route, payment_id: PaymentId) -> Result<(), PaymentSendFailure> {
23212362
const RETRY_OVERFLOW_PERCENTAGE: u64 = 10;
23222363
for path in route.paths.iter() {
@@ -2352,6 +2393,11 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
23522393
err: "Payment already completed"
23532394
}));
23542395
},
2396+
PendingOutboundPayment::Abandoned { .. } => {
2397+
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
2398+
err: "Payment already abandoned (with some HTLCs still pending)".to_owned()
2399+
}));
2400+
},
23552401
}
23562402
} else {
23572403
return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError {
@@ -2362,6 +2408,21 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
23622408
return self.send_payment_internal(route, payment_hash, &payment_secret, None, Some(payment_id), Some(total_msat)).map(|_| ())
23632409
}
23642410

2411+
/// Signals that no further retries for the given payment will occur.
2412+
///
2413+
/// After this method returns, any future calls to [`retry_payment`] for the given `payment_id`
2414+
/// will fail with [`PaymentSendFailure::ParameterError`].
2415+
///
2416+
/// [`retry_payment`]: Self::retry_payment
2417+
pub fn abandon_payment(&self, payment_id: PaymentId) {
2418+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(&self.total_consistency_lock, &self.persistence_notifier);
2419+
2420+
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
2421+
if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(payment_id) {
2422+
let _ = payment.get_mut().mark_abandoned();
2423+
}
2424+
}
2425+
23652426
/// Send a spontaneous payment, which is a payment that does not require the recipient to have
23662427
/// generated an invoice. Optionally, you may specify the preimage. If you do choose to specify
23672428
/// the preimage, it must be a cryptographically secure random value that no intermediate node
@@ -5605,6 +5666,10 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
56055666
(8, pending_amt_msat, required),
56065667
(10, starting_block_height, required),
56075668
},
5669+
(3, Abandoned) => {
5670+
(0, session_privs, required),
5671+
(2, payment_hash, required),
5672+
},
56085673
);
56095674

56105675
impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable for ChannelManager<Signer, M, T, K, F, L>
@@ -5698,7 +5763,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable f
56985763
// For backwards compat, write the session privs and their total length.
56995764
let mut num_pending_outbounds_compat: u64 = 0;
57005765
for (_, outbound) in pending_outbound_payments.iter() {
5701-
if !outbound.is_fulfilled() {
5766+
if !outbound.is_fulfilled() && !outbound.abandoned() {
57025767
num_pending_outbounds_compat += outbound.remaining_parts() as u64;
57035768
}
57045769
}
@@ -5712,6 +5777,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable f
57125777
}
57135778
}
57145779
PendingOutboundPayment::Fulfilled { .. } => {},
5780+
PendingOutboundPayment::Abandoned { .. } => {},
57155781
}
57165782
}
57175783

0 commit comments

Comments
 (0)