Skip to content

Commit 3f485c8

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 714eb64 commit 3f485c8

File tree

2 files changed

+64
-20
lines changed

2 files changed

+64
-20
lines changed

lightning/src/ln/channelmanager.rs

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

1236+
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until an invoice request without
1237+
/// a response is timed out.
1238+
pub(crate) const INVOICE_REQUEST_TIMEOUT_TICKS: u8 = 3;
1239+
12361240
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] until we time-out the
12371241
/// idempotency of payments by [`PaymentId`]. See
1238-
/// [`OutboundPayments::remove_stale_resolved_payments`].
1242+
/// [`OutboundPayments::remove_stale_payments`].
12391243
pub(crate) const IDEMPOTENCY_TIMEOUT_TICKS: u8 = 7;
12401244

12411245
/// The number of ticks of [`ChannelManager::timer_tick_occurred`] where a peer is disconnected
@@ -1551,6 +1555,11 @@ impl ChannelDetails {
15511555
/// These include payments that have yet to find a successful path, or have unresolved HTLCs.
15521556
#[derive(Debug, PartialEq)]
15531557
pub enum RecentPaymentDetails {
1558+
/// When an invoice was requested but not yet received, and thus a payment has not been sent.
1559+
AwaitingInvoice {
1560+
/// Identifier for the payment to ensure idempotency.
1561+
payment_id: PaymentId,
1562+
},
15541563
/// When a payment is still being sent and awaiting successful delivery.
15551564
Pending {
15561565
/// Hash of the payment that is currently being sent but has yet to be fulfilled or
@@ -2243,7 +2252,10 @@ where
22432252
/// [`Event::PaymentSent`]: events::Event::PaymentSent
22442253
pub fn list_recent_payments(&self) -> Vec<RecentPaymentDetails> {
22452254
self.pending_outbound_payments.pending_outbound_payments.lock().unwrap().iter()
2246-
.filter_map(|(_, pending_outbound_payment)| match pending_outbound_payment {
2255+
.filter_map(|(payment_id, pending_outbound_payment)| match pending_outbound_payment {
2256+
PendingOutboundPayment::AwaitingInvoice { .. } => {
2257+
Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id })
2258+
},
22472259
PendingOutboundPayment::Retryable { payment_hash, total_msat, .. } => {
22482260
Some(RecentPaymentDetails::Pending {
22492261
payment_hash: *payment_hash,
@@ -4332,7 +4344,7 @@ where
43324344
let _ = handle_error!(self, err, counterparty_node_id);
43334345
}
43344346

4335-
self.pending_outbound_payments.remove_stale_resolved_payments(&self.pending_events);
4347+
self.pending_outbound_payments.remove_stale_payments(&self.pending_events);
43364348

43374349
// Technically we don't need to do this here, but if we have holding cell entries in a
43384350
// channel that need freeing, it's better to do that here and block a background task
@@ -7831,6 +7843,7 @@ where
78317843
session_priv.write(writer)?;
78327844
}
78337845
}
7846+
PendingOutboundPayment::AwaitingInvoice { .. } => {},
78347847
PendingOutboundPayment::Fulfilled { .. } => {},
78357848
PendingOutboundPayment::Abandoned { .. } => {},
78367849
}

lightning/src/ln/outbound_payment.rs

Lines changed: 48 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::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,
@@ -107,6 +110,12 @@ impl PendingOutboundPayment {
107110
params.previously_failed_channels.push(scid);
108111
}
109112
}
113+
fn is_awaiting_invoice(&self) -> bool {
114+
match self {
115+
PendingOutboundPayment::AwaitingInvoice { .. } => true,
116+
_ => false,
117+
}
118+
}
110119
pub(super) fn is_fulfilled(&self) -> bool {
111120
match self {
112121
PendingOutboundPayment::Fulfilled { .. } => true,
@@ -129,6 +138,7 @@ impl PendingOutboundPayment {
129138
fn payment_hash(&self) -> Option<PaymentHash> {
130139
match self {
131140
PendingOutboundPayment::Legacy { .. } => None,
141+
PendingOutboundPayment::AwaitingInvoice { .. } => None,
132142
PendingOutboundPayment::Retryable { payment_hash, .. } => Some(*payment_hash),
133143
PendingOutboundPayment::Fulfilled { payment_hash, .. } => *payment_hash,
134144
PendingOutboundPayment::Abandoned { payment_hash, .. } => Some(*payment_hash),
@@ -141,8 +151,8 @@ impl PendingOutboundPayment {
141151
PendingOutboundPayment::Legacy { session_privs } |
142152
PendingOutboundPayment::Retryable { session_privs, .. } |
143153
PendingOutboundPayment::Fulfilled { session_privs, .. } |
144-
PendingOutboundPayment::Abandoned { session_privs, .. }
145-
=> session_privs,
154+
PendingOutboundPayment::Abandoned { session_privs, .. } => session_privs,
155+
PendingOutboundPayment::AwaitingInvoice { .. } => return,
146156
});
147157
let payment_hash = self.payment_hash();
148158
*self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0 };
@@ -168,7 +178,8 @@ impl PendingOutboundPayment {
168178
PendingOutboundPayment::Fulfilled { session_privs, .. } |
169179
PendingOutboundPayment::Abandoned { session_privs, .. } => {
170180
session_privs.remove(session_priv)
171-
}
181+
},
182+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
172183
};
173184
if remove_res {
174185
if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self {
@@ -188,6 +199,7 @@ impl PendingOutboundPayment {
188199
PendingOutboundPayment::Retryable { session_privs, .. } => {
189200
session_privs.insert(session_priv)
190201
}
202+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
191203
PendingOutboundPayment::Fulfilled { .. } => false,
192204
PendingOutboundPayment::Abandoned { .. } => false,
193205
};
@@ -209,7 +221,8 @@ impl PendingOutboundPayment {
209221
PendingOutboundPayment::Fulfilled { session_privs, .. } |
210222
PendingOutboundPayment::Abandoned { session_privs, .. } => {
211223
session_privs.len()
212-
}
224+
},
225+
PendingOutboundPayment::AwaitingInvoice { .. } => 0,
213226
}
214227
}
215228
}
@@ -619,7 +632,7 @@ impl OutboundPayments {
619632
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
620633
outbounds.retain(|pmt_id, pmt| {
621634
let mut retain = true;
622-
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 {
635+
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_awaiting_invoice() {
623636
pmt.mark_abandoned(PaymentFailureReason::RetriesExhausted);
624637
if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = pmt {
625638
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
@@ -637,7 +650,8 @@ impl OutboundPayments {
637650
pub(super) fn needs_abandon(&self) -> bool {
638651
let outbounds = self.pending_outbound_payments.lock().unwrap();
639652
outbounds.iter().any(|(_, pmt)|
640-
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled())
653+
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled() &&
654+
!pmt.is_awaiting_invoice())
641655
}
642656

643657
/// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may
@@ -774,6 +788,10 @@ impl OutboundPayments {
774788
log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102");
775789
return
776790
},
791+
PendingOutboundPayment::AwaitingInvoice { .. } => {
792+
log_error!(logger, "Payment not yet sent");
793+
return
794+
},
777795
PendingOutboundPayment::Fulfilled { .. } => {
778796
log_error!(logger, "Payment already completed");
779797
return
@@ -1185,19 +1203,19 @@ impl OutboundPayments {
11851203
}
11861204
}
11871205

1188-
pub(super) fn remove_stale_resolved_payments(&self,
1189-
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
1206+
pub(super) fn remove_stale_payments(
1207+
&self, pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
11901208
{
1191-
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1192-
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1193-
// this could race the user making a duplicate send_payment call and our idempotency
1194-
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1195-
// removal. This should be more than sufficient to ensure the idempotency of any
1196-
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1197-
// processed.
11981209
let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
1199-
let pending_events = pending_events.lock().unwrap();
1210+
let mut pending_events = pending_events.lock().unwrap();
12001211
pending_outbound_payments.retain(|payment_id, payment| {
1212+
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1213+
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1214+
// this could race the user making a duplicate send_payment call and our idempotency
1215+
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1216+
// removal. This should be more than sufficient to ensure the idempotency of any
1217+
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1218+
// processed.
12011219
if let PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } = payment {
12021220
let mut no_remaining_entries = session_privs.is_empty();
12031221
if no_remaining_entries {
@@ -1222,6 +1240,16 @@ impl OutboundPayments {
12221240
*timer_ticks_without_htlcs = 0;
12231241
true
12241242
}
1243+
} else if let PendingOutboundPayment::AwaitingInvoice { timer_ticks_without_response, .. } = payment {
1244+
*timer_ticks_without_response += 1;
1245+
if *timer_ticks_without_response <= INVOICE_REQUEST_TIMEOUT_TICKS {
1246+
true
1247+
} else {
1248+
pending_events.push_back(
1249+
(events::Event::InvoiceRequestFailed { payment_id: *payment_id }, None)
1250+
);
1251+
false
1252+
}
12251253
} else { true }
12261254
});
12271255
}
@@ -1426,6 +1454,9 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
14261454
(1, reason, option),
14271455
(2, payment_hash, required),
14281456
},
1457+
(5, AwaitingInvoice) => {
1458+
(0, timer_ticks_without_response, required),
1459+
},
14291460
);
14301461

14311462
#[cfg(test)]

0 commit comments

Comments
 (0)