Skip to content

Commit 9e1181f

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 ae0cf8b commit 9e1181f

File tree

2 files changed

+82
-20
lines changed

2 files changed

+82
-20
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,9 +1341,13 @@ const CHECK_CLTV_EXPIRY_SANITY_2: u32 = MIN_CLTV_EXPIRY_DELTA as u32 - LATENCY_G
13411341
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until expiry of incomplete MPPs
13421342
pub(crate) const MPP_TIMEOUT_TICKS: u8 = 3;
13431343

1344+
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until an invoice request without
1345+
/// a response is timed out.
1346+
pub(crate) const INVOICE_REQUEST_TIMEOUT_TICKS: u8 = 3;
1347+
13441348
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until we time-out the
13451349
/// idempotency of payments by [`PaymentId`]. See
1346-
/// [`OutboundPayments::remove_stale_resolved_payments`].
1350+
/// [`OutboundPayments::remove_stale_payments`].
13471351
pub(crate) const IDEMPOTENCY_TIMEOUT_TICKS: u8 = 7;
13481352

13491353
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] where a peer is disconnected
@@ -1688,6 +1692,11 @@ pub enum ChannelShutdownState {
16881692
/// These include payments that have yet to find a successful path, or have unresolved HTLCs.
16891693
#[derive(Debug, PartialEq)]
16901694
pub enum RecentPaymentDetails {
1695+
/// When an invoice was requested but not yet received, and thus a payment has not been sent.
1696+
AwaitingInvoice {
1697+
/// Identifier for the payment to ensure idempotency.
1698+
payment_id: PaymentId,
1699+
},
16911700
/// When a payment is still being sent and awaiting successful delivery.
16921701
Pending {
16931702
/// Hash of the payment that is currently being sent but has yet to be fulfilled or
@@ -2419,7 +2428,10 @@ where
24192428
/// [`Event::PaymentSent`]: events::Event::PaymentSent
24202429
pub fn list_recent_payments(&self) -> Vec<RecentPaymentDetails> {
24212430
self.pending_outbound_payments.pending_outbound_payments.lock().unwrap().iter()
2422-
.filter_map(|(_, pending_outbound_payment)| match pending_outbound_payment {
2431+
.filter_map(|(payment_id, pending_outbound_payment)| match pending_outbound_payment {
2432+
PendingOutboundPayment::AwaitingInvoice { .. } => {
2433+
Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id })
2434+
},
24232435
PendingOutboundPayment::Retryable { payment_hash, total_msat, .. } => {
24242436
Some(RecentPaymentDetails::Pending {
24252437
payment_hash: *payment_hash,
@@ -4653,7 +4665,7 @@ where
46534665
let _ = handle_error!(self, err, counterparty_node_id);
46544666
}
46554667

4656-
self.pending_outbound_payments.remove_stale_resolved_payments(&self.pending_events);
4668+
self.pending_outbound_payments.remove_stale_payments(&self.pending_events);
46574669

46584670
// Technically we don't need to do this here, but if we have holding cell entries in a
46594671
// channel that need freeing, it's better to do that here and block a background task
@@ -8345,6 +8357,7 @@ where
83458357
session_priv.write(writer)?;
83468358
}
83478359
}
8360+
PendingOutboundPayment::AwaitingInvoice { .. } => {},
83488361
PendingOutboundPayment::Fulfilled { .. } => {},
83498362
PendingOutboundPayment::Abandoned { .. } => {},
83508363
}

lightning/src/ln/outbound_payment.rs

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
1616
use crate::sign::{EntropySource, NodeSigner, Recipient};
1717
use crate::events::{self, PaymentFailureReason};
1818
use crate::ln::{PaymentHash, PaymentPreimage, PaymentSecret};
19-
use crate::ln::channelmanager::{ChannelDetails, EventCompletionAction, HTLCSource, IDEMPOTENCY_TIMEOUT_TICKS, PaymentId};
19+
use crate::ln::channelmanager::{ChannelDetails, EventCompletionAction, HTLCSource, IDEMPOTENCY_TIMEOUT_TICKS, INVOICE_REQUEST_TIMEOUT_TICKS, PaymentId};
2020
use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason};
2121
use crate::routing::router::{InFlightHtlcs, Path, PaymentParameters, Route, RouteParameters, Router};
2222
use crate::util::errors::APIError;
@@ -38,6 +38,9 @@ pub(crate) enum PendingOutboundPayment {
3838
Legacy {
3939
session_privs: HashSet<[u8; 32]>,
4040
},
41+
AwaitingInvoice {
42+
timer_ticks_without_response: u8,
43+
},
4144
Retryable {
4245
retry_strategy: Option<Retry>,
4346
attempts: PaymentAttempts,
@@ -108,6 +111,12 @@ impl PendingOutboundPayment {
108111
params.previously_failed_channels.push(scid);
109112
}
110113
}
114+
fn is_awaiting_invoice(&self) -> bool {
115+
match self {
116+
PendingOutboundPayment::AwaitingInvoice { .. } => true,
117+
_ => false,
118+
}
119+
}
111120
pub(super) fn is_fulfilled(&self) -> bool {
112121
match self {
113122
PendingOutboundPayment::Fulfilled { .. } => true,
@@ -130,6 +139,7 @@ impl PendingOutboundPayment {
130139
fn payment_hash(&self) -> Option<PaymentHash> {
131140
match self {
132141
PendingOutboundPayment::Legacy { .. } => None,
142+
PendingOutboundPayment::AwaitingInvoice { .. } => None,
133143
PendingOutboundPayment::Retryable { payment_hash, .. } => Some(*payment_hash),
134144
PendingOutboundPayment::Fulfilled { payment_hash, .. } => *payment_hash,
135145
PendingOutboundPayment::Abandoned { payment_hash, .. } => Some(*payment_hash),
@@ -142,8 +152,11 @@ impl PendingOutboundPayment {
142152
PendingOutboundPayment::Legacy { session_privs } |
143153
PendingOutboundPayment::Retryable { session_privs, .. } |
144154
PendingOutboundPayment::Fulfilled { session_privs, .. } |
145-
PendingOutboundPayment::Abandoned { session_privs, .. }
146-
=> session_privs,
155+
PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs,
156+
PendingOutboundPayment::AwaitingInvoice { .. } => {
157+
debug_assert!(false);
158+
return;
159+
},
147160
});
148161
let payment_hash = self.payment_hash();
149162
*self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0 };
@@ -169,7 +182,8 @@ impl PendingOutboundPayment {
169182
PendingOutboundPayment::Fulfilled { session_privs, .. } |
170183
PendingOutboundPayment::Abandoned { session_privs, .. } => {
171184
session_privs.remove(session_priv)
172-
}
185+
},
186+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
173187
};
174188
if remove_res {
175189
if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self {
@@ -189,6 +203,7 @@ impl PendingOutboundPayment {
189203
PendingOutboundPayment::Retryable { session_privs, .. } => {
190204
session_privs.insert(session_priv)
191205
}
206+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
192207
PendingOutboundPayment::Fulfilled { .. } => false,
193208
PendingOutboundPayment::Abandoned { .. } => false,
194209
};
@@ -210,7 +225,8 @@ impl PendingOutboundPayment {
210225
PendingOutboundPayment::Fulfilled { session_privs, .. } |
211226
PendingOutboundPayment::Abandoned { session_privs, .. } => {
212227
session_privs.len()
213-
}
228+
},
229+
PendingOutboundPayment::AwaitingInvoice { .. } => 0,
214230
}
215231
}
216232
}
@@ -679,7 +695,7 @@ impl OutboundPayments {
679695
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
680696
outbounds.retain(|pmt_id, pmt| {
681697
let mut retain = true;
682-
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 {
698+
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_awaiting_invoice() {
683699
pmt.mark_abandoned(PaymentFailureReason::RetriesExhausted);
684700
if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = pmt {
685701
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
@@ -697,7 +713,8 @@ impl OutboundPayments {
697713
pub(super) fn needs_abandon(&self) -> bool {
698714
let outbounds = self.pending_outbound_payments.lock().unwrap();
699715
outbounds.iter().any(|(_, pmt)|
700-
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled())
716+
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled() &&
717+
!pmt.is_awaiting_invoice())
701718
}
702719

703720
/// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may
@@ -845,6 +862,10 @@ impl OutboundPayments {
845862
log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102");
846863
return
847864
},
865+
PendingOutboundPayment::AwaitingInvoice { .. } => {
866+
log_error!(logger, "Payment not yet sent");
867+
return
868+
},
848869
PendingOutboundPayment::Fulfilled { .. } => {
849870
log_error!(logger, "Payment already completed");
850871
return
@@ -1051,6 +1072,21 @@ impl OutboundPayments {
10511072
}
10521073
}
10531074

1075+
#[allow(unused)]
1076+
pub(super) fn add_new_awaiting_invoice(&self, payment_id: PaymentId) -> Result<(), ()> {
1077+
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
1078+
match pending_outbounds.entry(payment_id) {
1079+
hash_map::Entry::Occupied(_) => Err(()),
1080+
hash_map::Entry::Vacant(entry) => {
1081+
entry.insert(PendingOutboundPayment::AwaitingInvoice {
1082+
timer_ticks_without_response: 0,
1083+
});
1084+
1085+
Ok(())
1086+
},
1087+
}
1088+
}
1089+
10541090
fn pay_route_internal<NS: Deref, F>(
10551091
&self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields,
10561092
keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, recv_value_msat: Option<u64>,
@@ -1256,19 +1292,19 @@ impl OutboundPayments {
12561292
}
12571293
}
12581294

1259-
pub(super) fn remove_stale_resolved_payments(&self,
1260-
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
1295+
pub(super) fn remove_stale_payments(
1296+
&self, pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
12611297
{
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.
12691298
let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
1270-
let pending_events = pending_events.lock().unwrap();
1299+
let mut pending_events = pending_events.lock().unwrap();
12711300
pending_outbound_payments.retain(|payment_id, payment| {
1301+
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1302+
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1303+
// this could race the user making a duplicate send_payment call and our idempotency
1304+
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1305+
// removal. This should be more than sufficient to ensure the idempotency of any
1306+
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1307+
// processed.
12721308
if let PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } = payment {
12731309
let mut no_remaining_entries = session_privs.is_empty();
12741310
if no_remaining_entries {
@@ -1293,6 +1329,16 @@ impl OutboundPayments {
12931329
*timer_ticks_without_htlcs = 0;
12941330
true
12951331
}
1332+
} else if let PendingOutboundPayment::AwaitingInvoice { timer_ticks_without_response, .. } = payment {
1333+
*timer_ticks_without_response += 1;
1334+
if *timer_ticks_without_response <= INVOICE_REQUEST_TIMEOUT_TICKS {
1335+
true
1336+
} else {
1337+
pending_events.push_back(
1338+
(events::Event::InvoiceRequestFailed { payment_id: *payment_id }, None)
1339+
);
1340+
false
1341+
}
12961342
} else { true }
12971343
});
12981344
}
@@ -1501,6 +1547,9 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
15011547
(1, reason, option),
15021548
(2, payment_hash, required),
15031549
},
1550+
(5, AwaitingInvoice) => {
1551+
(0, timer_ticks_without_response, required),
1552+
},
15041553
);
15051554

15061555
#[cfg(test)]

0 commit comments

Comments
 (0)