Skip to content

Commit 4ebb061

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 1a3de89 commit 4ebb061

File tree

2 files changed

+106
-40
lines changed

2 files changed

+106
-40
lines changed

lightning/src/ln/channelmanager.rs

+16-3
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,
@@ -4651,7 +4663,7 @@ where
46514663
let _ = handle_error!(self, err, counterparty_node_id);
46524664
}
46534665

4654-
self.pending_outbound_payments.remove_stale_resolved_payments(&self.pending_events);
4666+
self.pending_outbound_payments.remove_stale_payments(&self.pending_events);
46554667

46564668
// Technically we don't need to do this here, but if we have holding cell entries in a
46574669
// channel that need freeing, it's better to do that here and block a background task
@@ -8343,6 +8355,7 @@ where
83438355
session_priv.write(writer)?;
83448356
}
83458357
}
8358+
PendingOutboundPayment::AwaitingInvoice { .. } => {},
83468359
PendingOutboundPayment::Fulfilled { .. } => {},
83478360
PendingOutboundPayment::Abandoned { .. } => {},
83488361
}

lightning/src/ln/outbound_payment.rs

+90-37
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,8 @@ 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 { .. } => return,
147157
});
148158
let payment_hash = self.payment_hash();
149159
*self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0 };
@@ -169,7 +179,8 @@ impl PendingOutboundPayment {
169179
PendingOutboundPayment::Fulfilled { session_privs, .. } |
170180
PendingOutboundPayment::Abandoned { session_privs, .. } => {
171181
session_privs.remove(session_priv)
172-
}
182+
},
183+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
173184
};
174185
if remove_res {
175186
if let PendingOutboundPayment::Retryable { ref mut pending_amt_msat, ref mut pending_fee_msat, .. } = self {
@@ -189,6 +200,7 @@ impl PendingOutboundPayment {
189200
PendingOutboundPayment::Retryable { session_privs, .. } => {
190201
session_privs.insert(session_priv)
191202
}
203+
PendingOutboundPayment::AwaitingInvoice { .. } => false,
192204
PendingOutboundPayment::Fulfilled { .. } => false,
193205
PendingOutboundPayment::Abandoned { .. } => false,
194206
};
@@ -210,7 +222,8 @@ impl PendingOutboundPayment {
210222
PendingOutboundPayment::Fulfilled { session_privs, .. } |
211223
PendingOutboundPayment::Abandoned { session_privs, .. } => {
212224
session_privs.len()
213-
}
225+
},
226+
PendingOutboundPayment::AwaitingInvoice { .. } => 0,
214227
}
215228
}
216229
}
@@ -679,7 +692,7 @@ impl OutboundPayments {
679692
let mut outbounds = self.pending_outbound_payments.lock().unwrap();
680693
outbounds.retain(|pmt_id, pmt| {
681694
let mut retain = true;
682-
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 {
695+
if !pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_awaiting_invoice() {
683696
pmt.mark_abandoned(PaymentFailureReason::RetriesExhausted);
684697
if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = pmt {
685698
pending_events.lock().unwrap().push_back((events::Event::PaymentFailed {
@@ -697,7 +710,8 @@ impl OutboundPayments {
697710
pub(super) fn needs_abandon(&self) -> bool {
698711
let outbounds = self.pending_outbound_payments.lock().unwrap();
699712
outbounds.iter().any(|(_, pmt)|
700-
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled())
713+
!pmt.is_auto_retryable_now() && pmt.remaining_parts() == 0 && !pmt.is_fulfilled() &&
714+
!pmt.is_awaiting_invoice())
701715
}
702716

703717
/// Errors immediately on [`RetryableSendFailure`] error conditions. Otherwise, further errors may
@@ -834,6 +848,10 @@ impl OutboundPayments {
834848
log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102");
835849
return
836850
},
851+
PendingOutboundPayment::AwaitingInvoice { .. } => {
852+
log_error!(logger, "Payment not yet sent");
853+
return
854+
},
837855
PendingOutboundPayment::Fulfilled { .. } => {
838856
log_error!(logger, "Payment already completed");
839857
return
@@ -1006,36 +1024,58 @@ impl OutboundPayments {
10061024
keysend_preimage: Option<PaymentPreimage>, route: &Route, retry_strategy: Option<Retry>,
10071025
payment_params: Option<PaymentParameters>, entropy_source: &ES, best_block_height: u32
10081026
) -> Result<Vec<[u8; 32]>, PaymentSendFailure> where ES::Target: EntropySource {
1027+
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
1028+
let entry = pending_outbounds.entry(payment_id);
1029+
if let hash_map::Entry::Occupied(entry) = &entry {
1030+
if let PendingOutboundPayment::AwaitingInvoice { .. } = entry.get() { } else {
1031+
return Err(PaymentSendFailure::DuplicatePayment)
1032+
}
1033+
}
1034+
10091035
let mut onion_session_privs = Vec::with_capacity(route.paths.len());
10101036
for _ in 0..route.paths.len() {
10111037
onion_session_privs.push(entropy_source.get_secure_random_bytes());
10121038
}
10131039

1040+
let mut payment = PendingOutboundPayment::Retryable {
1041+
retry_strategy,
1042+
attempts: PaymentAttempts::new(),
1043+
payment_params,
1044+
session_privs: HashSet::new(),
1045+
pending_amt_msat: 0,
1046+
pending_fee_msat: Some(0),
1047+
payment_hash,
1048+
payment_secret: recipient_onion.payment_secret,
1049+
payment_metadata: recipient_onion.payment_metadata,
1050+
keysend_preimage,
1051+
custom_tlvs: recipient_onion.custom_tlvs,
1052+
starting_block_height: best_block_height,
1053+
total_msat: route.get_total_amount(),
1054+
};
1055+
1056+
for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) {
1057+
assert!(payment.insert(*session_priv_bytes, path));
1058+
}
1059+
1060+
match entry {
1061+
hash_map::Entry::Occupied(mut entry) => { entry.insert(payment); },
1062+
hash_map::Entry::Vacant(entry) => { entry.insert(payment); },
1063+
}
1064+
1065+
Ok(onion_session_privs)
1066+
}
1067+
1068+
#[allow(unused)]
1069+
pub(super) fn add_new_awaiting_invoice(&self, payment_id: PaymentId) -> Result<(), ()> {
10141070
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
10151071
match pending_outbounds.entry(payment_id) {
1016-
hash_map::Entry::Occupied(_) => Err(PaymentSendFailure::DuplicatePayment),
1072+
hash_map::Entry::Occupied(_) => Err(()),
10171073
hash_map::Entry::Vacant(entry) => {
1018-
let payment = entry.insert(PendingOutboundPayment::Retryable {
1019-
retry_strategy,
1020-
attempts: PaymentAttempts::new(),
1021-
payment_params,
1022-
session_privs: HashSet::new(),
1023-
pending_amt_msat: 0,
1024-
pending_fee_msat: Some(0),
1025-
payment_hash,
1026-
payment_secret: recipient_onion.payment_secret,
1027-
payment_metadata: recipient_onion.payment_metadata,
1028-
keysend_preimage,
1029-
custom_tlvs: recipient_onion.custom_tlvs,
1030-
starting_block_height: best_block_height,
1031-
total_msat: route.get_total_amount(),
1074+
entry.insert(PendingOutboundPayment::AwaitingInvoice {
1075+
timer_ticks_without_response: 0,
10321076
});
10331077

1034-
for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) {
1035-
assert!(payment.insert(*session_priv_bytes, path));
1036-
}
1037-
1038-
Ok(onion_session_privs)
1078+
Ok(())
10391079
},
10401080
}
10411081
}
@@ -1244,19 +1284,19 @@ impl OutboundPayments {
12441284
}
12451285
}
12461286

1247-
pub(super) fn remove_stale_resolved_payments(&self,
1248-
pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
1287+
pub(super) fn remove_stale_payments(
1288+
&self, pending_events: &Mutex<VecDeque<(events::Event, Option<EventCompletionAction>)>>)
12491289
{
1250-
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1251-
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1252-
// this could race the user making a duplicate send_payment call and our idempotency
1253-
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1254-
// removal. This should be more than sufficient to ensure the idempotency of any
1255-
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1256-
// processed.
12571290
let mut pending_outbound_payments = self.pending_outbound_payments.lock().unwrap();
1258-
let pending_events = pending_events.lock().unwrap();
1291+
let mut pending_events = pending_events.lock().unwrap();
12591292
pending_outbound_payments.retain(|payment_id, payment| {
1293+
// If an outbound payment was completed, and no pending HTLCs remain, we should remove it
1294+
// from the map. However, if we did that immediately when the last payment HTLC is claimed,
1295+
// this could race the user making a duplicate send_payment call and our idempotency
1296+
// guarantees would be violated. Instead, we wait a few timer ticks to do the actual
1297+
// removal. This should be more than sufficient to ensure the idempotency of any
1298+
// `send_payment` calls that were made at the same time the `PaymentSent` event was being
1299+
// processed.
12601300
if let PendingOutboundPayment::Fulfilled { session_privs, timer_ticks_without_htlcs, .. } = payment {
12611301
let mut no_remaining_entries = session_privs.is_empty();
12621302
if no_remaining_entries {
@@ -1281,6 +1321,16 @@ impl OutboundPayments {
12811321
*timer_ticks_without_htlcs = 0;
12821322
true
12831323
}
1324+
} else if let PendingOutboundPayment::AwaitingInvoice { timer_ticks_without_response, .. } = payment {
1325+
*timer_ticks_without_response += 1;
1326+
if *timer_ticks_without_response <= INVOICE_REQUEST_TIMEOUT_TICKS {
1327+
true
1328+
} else {
1329+
pending_events.push_back(
1330+
(events::Event::InvoiceRequestFailed { payment_id: *payment_id }, None)
1331+
);
1332+
false
1333+
}
12841334
} else { true }
12851335
});
12861336
}
@@ -1489,6 +1539,9 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
14891539
(1, reason, option),
14901540
(2, payment_hash, required),
14911541
},
1542+
(5, AwaitingInvoice) => {
1543+
(0, timer_ticks_without_response, required),
1544+
},
14921545
);
14931546

14941547
#[cfg(test)]

0 commit comments

Comments
 (0)