Skip to content

Commit be1f8be

Browse files
committed
Add PendingOutboundPayment::AwaitingInvoice
When a BOLT 12 invoice has been requested, in order to guarantee invoice payment idempotency the future payment needs to be tracked. Add an AwaitingInvoice variant to PendingOutboundPayment such that only requested invoices are paid only once. Timeout after a few timer ticks if a request has not been received.
1 parent 6d0c5f0 commit be1f8be

File tree

2 files changed

+88
-19
lines changed

2 files changed

+88
-19
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,7 @@ pub(crate) const MPP_TIMEOUT_TICKS: u8 = 3;
13431343

13441344
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until we time-out the
13451345
/// idempotency of payments by [`PaymentId`]. See
1346-
/// [`OutboundPayments::remove_stale_resolved_payments`].
1346+
/// [`OutboundPayments::remove_stale_payments`].
13471347
pub(crate) const IDEMPOTENCY_TIMEOUT_TICKS: u8 = 7;
13481348

13491349
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] where a peer is disconnected
@@ -1688,6 +1688,11 @@ pub enum ChannelShutdownState {
16881688
/// These include payments that have yet to find a successful path, or have unresolved HTLCs.
16891689
#[derive(Debug, PartialEq)]
16901690
pub enum RecentPaymentDetails {
1691+
/// When an invoice was requested but not yet received, and thus a payment has not been sent.
1692+
AwaitingInvoice {
1693+
/// Identifier for the payment to ensure idempotency.
1694+
payment_id: PaymentId,
1695+
},
16911696
/// When a payment is still being sent and awaiting successful delivery.
16921697
Pending {
16931698
/// Hash of the payment that is currently being sent but has yet to be fulfilled or
@@ -2419,7 +2424,10 @@ where
24192424
/// [`Event::PaymentSent`]: events::Event::PaymentSent
24202425
pub fn list_recent_payments(&self) -> Vec<RecentPaymentDetails> {
24212426
self.pending_outbound_payments.pending_outbound_payments.lock().unwrap().iter()
2422-
.filter_map(|(_, pending_outbound_payment)| match pending_outbound_payment {
2427+
.filter_map(|(payment_id, pending_outbound_payment)| match pending_outbound_payment {
2428+
PendingOutboundPayment::AwaitingInvoice { .. } => {
2429+
Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id })
2430+
},
24232431
PendingOutboundPayment::Retryable { payment_hash, total_msat, .. } => {
24242432
Some(RecentPaymentDetails::Pending {
24252433
payment_hash: *payment_hash,
@@ -4665,7 +4673,7 @@ where
46654673
let _ = handle_error!(self, err, counterparty_node_id);
46664674
}
46674675

4668-
self.pending_outbound_payments.remove_stale_resolved_payments(&self.pending_events);
4676+
self.pending_outbound_payments.remove_stale_payments(&self.pending_events);
46694677

46704678
// Technically we don't need to do this here, but if we have holding cell entries in a
46714679
// channel that need freeing, it's better to do that here and block a background task
@@ -8357,6 +8365,7 @@ where
83578365
session_priv.write(writer)?;
83588366
}
83598367
}
8368+
PendingOutboundPayment::AwaitingInvoice { .. } => {},
83608369
PendingOutboundPayment::Fulfilled { .. } => {},
83618370
PendingOutboundPayment::Abandoned { .. } => {},
83628371
}

lightning/src/ln/outbound_payment.rs

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,21 @@ use core::ops::Deref;
3232
use crate::prelude::*;
3333
use crate::sync::Mutex;
3434

35+
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until an invoice request without
36+
/// a response is timed out.
37+
///
38+
/// [`ChannelManager::timer_tick_occurred`]: crate::ln::channelmanager::ChannelManager::timer_tick_occurred
39+
const INVOICE_REQUEST_TIMEOUT_TICKS: u8 = 3;
40+
3541
/// Stores the session_priv for each part of a payment that is still pending. For versions 0.0.102
3642
/// and later, also stores information for retrying the payment.
3743
pub(crate) enum PendingOutboundPayment {
3844
Legacy {
3945
session_privs: HashSet<[u8; 32]>,
4046
},
47+
AwaitingInvoice {
48+
timer_ticks_without_response: u8,
49+
},
4150
Retryable {
4251
retry_strategy: Option<Retry>,
4352
attempts: PaymentAttempts,
@@ -108,6 +117,12 @@ impl PendingOutboundPayment {
108117
params.previously_failed_channels.push(scid);
109118
}
110119
}
120+
fn is_awaiting_invoice(&self) -> bool {
121+
match self {
122+
PendingOutboundPayment::AwaitingInvoice { .. } => true,
123+
_ => false,
124+
}
125+
}
111126
pub(super) fn is_fulfilled(&self) -> bool {
112127
match self {
113128
PendingOutboundPayment::Fulfilled { .. } => true,
@@ -130,6 +145,7 @@ impl PendingOutboundPayment {
130145
fn payment_hash(&self) -> Option<PaymentHash> {
131146
match self {
132147
PendingOutboundPayment::Legacy { .. } => None,
148+
PendingOutboundPayment::AwaitingInvoice { .. } => None,
133149
PendingOutboundPayment::Retryable { payment_hash, .. } => Some(*payment_hash),
134150
PendingOutboundPayment::Fulfilled { payment_hash, .. } => *payment_hash,
135151
PendingOutboundPayment::Abandoned { payment_hash, .. } => Some(*payment_hash),
@@ -142,8 +158,11 @@ impl PendingOutboundPayment {
142158
PendingOutboundPayment::Legacy { session_privs } |
143159
PendingOutboundPayment::Retryable { session_privs, .. } |
144160
PendingOutboundPayment::Fulfilled { session_privs, .. } |
145-
PendingOutboundPayment::Abandoned { session_privs, .. }
146-
=> session_privs,
161+
PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs,
162+
PendingOutboundPayment::AwaitingInvoice { .. } => {
163+
debug_assert!(false);
164+
return;
165+
},
147166
});
148167
let payment_hash = self.payment_hash();
149168
*self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0 };
@@ -169,7 +188,8 @@ impl PendingOutboundPayment {
169188
PendingOutboundPayment::Fulfilled { session_privs, .. } |
170189
PendingOutboundPayment::Abandoned { session_privs, .. } => {
171190
session_privs.remove(session_priv)
172-
}
191+
},
192+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
173193
};
174194
if remove_res {
175195
if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self {
@@ -189,6 +209,7 @@ impl PendingOutboundPayment {
189209
PendingOutboundPayment::Retryable { session_privs, .. } => {
190210
session_privs.insert(session_priv)
191211
}
212+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
192213
PendingOutboundPayment::Fulfilled { .. } => false,
193214
PendingOutboundPayment::Abandoned { .. } => false,
194215
};
@@ -210,7 +231,8 @@ impl PendingOutboundPayment {
210231
PendingOutboundPayment::Fulfilled { session_privs, .. } |
211232
PendingOutboundPayment::Abandoned { session_privs, .. } => {
212233
session_privs.len()
213-
}
234+
},
235+
PendingOutboundPayment::AwaitingInvoice { .. } => 0,
214236
}
215237
}
216238
}
@@ -679,7 +701,7 @@ impl OutboundPayments {
679701
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
680702
outbounds.retain(|pmt_id, pmt| {
681703
let mut retain = true;
682-
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 {
704+
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_awaiting_invoice() {
683705
pmt.mark_abandoned(PaymentFailureReason::RetriesExhausted);
684706
if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = pmt {
685707
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
@@ -697,7 +719,8 @@ impl OutboundPayments {
697719
pub(super) fn needs_abandon(&self) -> bool {
698720
let outbounds = self.pending_outbound_payments.lock().unwrap();
699721
outbounds.iter().any(|(_, pmt)|
700-
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled())
722+
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled() &&
723+
!pmt.is_awaiting_invoice())
701724
}
702725

703726
/// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may
@@ -845,6 +868,10 @@ impl OutboundPayments {
845868
log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102");
846869
return
847870
},
871+
PendingOutboundPayment::AwaitingInvoice { .. } => {
872+
log_error!(logger, "Payment not yet sent");
873+
return
874+
},
848875
PendingOutboundPayment::Fulfilled { .. } => {
849876
log_error!(logger, "Payment already completed");
850877
return
@@ -1051,6 +1078,21 @@ impl OutboundPayments {
10511078
}
10521079
}
10531080

1081+
#[allow(unused)]
1082+
pub(super) fn add_new_awaiting_invoice(&self, payment_id: PaymentId) -> Result<(), ()> {
1083+
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
1084+
match pending_outbounds.entry(payment_id) {
1085+
hash_map::Entry::Occupied(_) => Err(()),
1086+
hash_map::Entry::Vacant(entry) => {
1087+
entry.insert(PendingOutboundPayment::AwaitingInvoice {
1088+
timer_ticks_without_response: 0,
1089+
});
1090+
1091+
Ok(())
1092+
},
1093+
}
1094+
}
1095+
10541096
fn pay_route_internal<NS: Deref, F>(
10551097
&self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields,
10561098
keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, recv_value_msat: Option<u64>,
@@ -1256,19 +1298,19 @@ impl OutboundPayments {
12561298
}
12571299
}
12581300

1259-
pub(super) fn remove_stale_resolved_payments(&self,
1260-
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
1301+
pub(super) fn remove_stale_payments(
1302+
&self, pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
12611303
{
1262-
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1263-
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1264-
// this could race the user making a duplicate send_payment call and our idempotency
1265-
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1266-
// removal. This should be more than sufficient to ensure the idempotency of any
1267-
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1268-
// processed.
12691304
let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
1270-
let pending_events = pending_events.lock().unwrap();
1305+
let mut pending_events = pending_events.lock().unwrap();
12711306
pending_outbound_payments.retain(|payment_id, payment| {
1307+
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1308+
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1309+
// this could race the user making a duplicate send_payment call and our idempotency
1310+
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1311+
// removal. This should be more than sufficient to ensure the idempotency of any
1312+
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1313+
// processed.
12721314
if let PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } = payment {
12731315
let mut no_remaining_entries = session_privs.is_empty();
12741316
if no_remaining_entries {
@@ -1293,6 +1335,16 @@ impl OutboundPayments {
12931335
*timer_ticks_without_htlcs = 0;
12941336
true
12951337
}
1338+
} else if let PendingOutboundPayment::AwaitingInvoice { timer_ticks_without_response, .. } = payment {
1339+
*timer_ticks_without_response += 1;
1340+
if *timer_ticks_without_response <= INVOICE_REQUEST_TIMEOUT_TICKS {
1341+
true
1342+
} else {
1343+
pending_events.push_back(
1344+
(events::Event::InvoiceRequestFailed { payment_id: *payment_id }, None)
1345+
);
1346+
false
1347+
}
12961348
} else { true }
12971349
});
12981350
}
@@ -1438,6 +1490,11 @@ impl OutboundPayments {
14381490
}, None));
14391491
payment.remove();
14401492
}
1493+
} else if let PendingOutboundPayment::AwaitingInvoice { .. } = payment.get() {
1494+
pending_events.lock().unwrap().push_back((events::Event::InvoiceRequestFailed {
1495+
payment_id,
1496+
}, None));
1497+
payment.remove();
14411498
}
14421499
}
14431500
}
@@ -1501,6 +1558,9 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
15011558
(1, reason, option),
15021559
(2, payment_hash, required),
15031560
},
1561+
(5, AwaitingInvoice) => {
1562+
(0, timer_ticks_without_response, required),
1563+
},
15041564
);
15051565

15061566
#[cfg(test)]

0 commit comments

Comments
 (0)