Skip to content

Commit 581ea7c

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 581ea7c

File tree

2 files changed

+94
-19
lines changed

2 files changed

+94
-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: 82 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,11 @@ impl PendingOutboundPayment {
169188
PendingOutboundPayment::Fulfilled { session_privs, .. } |
170189
PendingOutboundPayment::Abandoned { session_privs, .. } => {
171190
session_privs.remove(session_priv)
172-
}
191+
},
192+
PendingOutboundPayment::AwaitingInvoice { .. } => {
193+
debug_assert!(false);
194+
false
195+
},
173196
};
174197
if remove_res {
175198
if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self {
@@ -189,6 +212,10 @@ impl PendingOutboundPayment {
189212
PendingOutboundPayment::Retryable { session_privs, .. } => {
190213
session_privs.insert(session_priv)
191214
}
215+
PendingOutboundPayment::AwaitingInvoice { .. } => {
216+
debug_assert!(false);
217+
false
218+
},
192219
PendingOutboundPayment::Fulfilled { .. } => false,
193220
PendingOutboundPayment::Abandoned { .. } => false,
194221
};
@@ -210,7 +237,8 @@ impl PendingOutboundPayment {
210237
PendingOutboundPayment::Fulfilled { session_privs, .. } |
211238
PendingOutboundPayment::Abandoned { session_privs, .. } => {
212239
session_privs.len()
213-
}
240+
},
241+
PendingOutboundPayment::AwaitingInvoice { .. } => 0,
214242
}
215243
}
216244
}
@@ -679,7 +707,7 @@ impl OutboundPayments {
679707
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
680708
outbounds.retain(|pmt_id, pmt| {
681709
let mut retain = true;
682-
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 {
710+
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_awaiting_invoice() {
683711
pmt.mark_abandoned(PaymentFailureReason::RetriesExhausted);
684712
if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = pmt {
685713
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
@@ -697,7 +725,8 @@ impl OutboundPayments {
697725
pub(super) fn needs_abandon(&self) -> bool {
698726
let outbounds = self.pending_outbound_payments.lock().unwrap();
699727
outbounds.iter().any(|(_, pmt)|
700-
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled())
728+
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled() &&
729+
!pmt.is_awaiting_invoice())
701730
}
702731

703732
/// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may
@@ -845,6 +874,10 @@ impl OutboundPayments {
845874
log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102");
846875
return
847876
},
877+
PendingOutboundPayment::AwaitingInvoice { .. } => {
878+
log_error!(logger, "Payment not yet sent");
879+
return
880+
},
848881
PendingOutboundPayment::Fulfilled { .. } => {
849882
log_error!(logger, "Payment already completed");
850883
return
@@ -1051,6 +1084,21 @@ impl OutboundPayments {
10511084
}
10521085
}
10531086

1087+
#[allow(unused)]
1088+
pub(super) fn add_new_awaiting_invoice(&self, payment_id: PaymentId) -> Result<(), ()> {
1089+
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
1090+
match pending_outbounds.entry(payment_id) {
1091+
hash_map::Entry::Occupied(_) => Err(()),
1092+
hash_map::Entry::Vacant(entry) => {
1093+
entry.insert(PendingOutboundPayment::AwaitingInvoice {
1094+
timer_ticks_without_response: 0,
1095+
});
1096+
1097+
Ok(())
1098+
},
1099+
}
1100+
}
1101+
10541102
fn pay_route_internal<NS: Deref, F>(
10551103
&self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields,
10561104
keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, recv_value_msat: Option<u64>,
@@ -1256,19 +1304,19 @@ impl OutboundPayments {
12561304
}
12571305
}
12581306

1259-
pub(super) fn remove_stale_resolved_payments(&self,
1260-
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
1307+
pub(super) fn remove_stale_payments(
1308+
&self, pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
12611309
{
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.
12691310
let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
1270-
let pending_events = pending_events.lock().unwrap();
1311+
let mut pending_events = pending_events.lock().unwrap();
12711312
pending_outbound_payments.retain(|payment_id, payment| {
1313+
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1314+
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1315+
// this could race the user making a duplicate send_payment call and our idempotency
1316+
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1317+
// removal. This should be more than sufficient to ensure the idempotency of any
1318+
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1319+
// processed.
12721320
if let PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } = payment {
12731321
let mut no_remaining_entries = session_privs.is_empty();
12741322
if no_remaining_entries {
@@ -1293,6 +1341,16 @@ impl OutboundPayments {
12931341
*timer_ticks_without_htlcs = 0;
12941342
true
12951343
}
1344+
} else if let PendingOutboundPayment::AwaitingInvoice { timer_ticks_without_response, .. } = payment {
1345+
*timer_ticks_without_response += 1;
1346+
if *timer_ticks_without_response <= INVOICE_REQUEST_TIMEOUT_TICKS {
1347+
true
1348+
} else {
1349+
pending_events.push_back(
1350+
(events::Event::InvoiceRequestFailed { payment_id: *payment_id }, None)
1351+
);
1352+
false
1353+
}
12961354
} else { true }
12971355
});
12981356
}
@@ -1438,6 +1496,11 @@ impl OutboundPayments {
14381496
}, None));
14391497
payment.remove();
14401498
}
1499+
} else if let PendingOutboundPayment::AwaitingInvoice { .. } = payment.get() {
1500+
pending_events.lock().unwrap().push_back((events::Event::InvoiceRequestFailed {
1501+
payment_id,
1502+
}, None));
1503+
payment.remove();
14411504
}
14421505
}
14431506
}
@@ -1501,6 +1564,9 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
15011564
(1, reason, option),
15021565
(2, payment_hash, required),
15031566
},
1567+
(5, AwaitingInvoice) => {
1568+
(0, timer_ticks_without_response, required),
1569+
},
15041570
);
15051571

15061572
#[cfg(test)]

0 commit comments

Comments
 (0)