Skip to content

Commit 233aa39

Browse files
authored
Merge pull request #3658 from shaavan/i3653
Add idempotency check to prevent duplicate `InvoiceReceived` events
2 parents b247bda + d57b153 commit 233aa39

File tree

3 files changed

+64
-24
lines changed

3 files changed

+64
-24
lines changed

lightning/src/ln/channelmanager.rs

+5
Original file line numberDiff line numberDiff line change
@@ -12605,6 +12605,11 @@ where
1260512605
);
1260612606

1260712607
if self.default_configuration.manually_handle_bolt12_invoices {
12608+
// Update the corresponding entry in `PendingOutboundPayment` for this invoice.
12609+
// This ensures that event generation remains idempotent in case we receive
12610+
// the same invoice multiple times.
12611+
self.pending_outbound_payments.mark_invoice_received(&invoice, payment_id).ok()?;
12612+
1260812613
let event = Event::InvoiceReceived {
1260912614
payment_id, invoice, context, responder,
1261012615
};

lightning/src/ln/offers_tests.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -1184,7 +1184,14 @@ fn pays_bolt12_invoice_asynchronously() {
11841184
let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap();
11851185
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
11861186

1187-
let (invoice, context) = match get_event!(bob, Event::InvoiceReceived) {
1187+
// Re-process the same onion message to ensure idempotency —
1188+
// we should not generate a duplicate `InvoiceReceived` event.
1189+
bob.onion_messenger.handle_onion_message(alice_id, &onion_message);
1190+
1191+
let mut events = bob.node.get_and_clear_pending_events();
1192+
assert_eq!(events.len(), 1);
1193+
1194+
let (invoice, context) = match events.pop().unwrap() {
11881195
Event::InvoiceReceived { payment_id: actual_payment_id, invoice, context, .. } => {
11891196
assert_eq!(actual_payment_id, payment_id);
11901197
(invoice, context)

lightning/src/ln/outbound_payment.rs

+51-23
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ pub(crate) enum PendingOutboundPayment {
7272
route_params_config: RouteParametersConfig,
7373
retryable_invoice_request: Option<RetryableInvoiceRequest>
7474
},
75-
// This state will never be persisted to disk because we transition from `AwaitingInvoice` to
76-
// `Retryable` atomically within the `ChannelManager::total_consistency_lock`. Useful to avoid
77-
// holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding.
75+
// Represents the state after the invoice has been received, transitioning from the corresponding
76+
// `AwaitingInvoice` state.
77+
// Helps avoid holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding.
7878
InvoiceReceived {
7979
payment_hash: PaymentHash,
8080
retry_strategy: Retry,
@@ -931,26 +931,9 @@ impl OutboundPayments {
931931
IH: Fn() -> InFlightHtlcs,
932932
SP: Fn(SendAlongPathArgs) -> Result<(), APIError>,
933933
{
934-
let payment_hash = invoice.payment_hash();
935-
let params_config;
936-
let retry_strategy;
937-
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
938-
hash_map::Entry::Occupied(entry) => match entry.get() {
939-
PendingOutboundPayment::AwaitingInvoice {
940-
retry_strategy: retry, route_params_config, ..
941-
} => {
942-
retry_strategy = *retry;
943-
params_config = *route_params_config;
944-
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
945-
payment_hash,
946-
retry_strategy: *retry,
947-
route_params_config: *route_params_config,
948-
};
949-
},
950-
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
951-
},
952-
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
953-
}
934+
935+
let (payment_hash, retry_strategy, params_config, _) = self
936+
.mark_invoice_received_and_get_details(invoice, payment_id)?;
954937

955938
if invoice.invoice_features().requires_unknown_bits_from(&features) {
956939
self.abandon_payment(
@@ -1863,6 +1846,51 @@ impl OutboundPayments {
18631846
}
18641847
}
18651848

1849+
pub(super) fn mark_invoice_received(
1850+
&self, invoice: &Bolt12Invoice, payment_id: PaymentId
1851+
) -> Result<(), Bolt12PaymentError> {
1852+
self.mark_invoice_received_and_get_details(invoice, payment_id)
1853+
.and_then(|(_, _, _, is_newly_marked)| {
1854+
is_newly_marked
1855+
.then_some(())
1856+
.ok_or(Bolt12PaymentError::DuplicateInvoice)
1857+
})
1858+
}
1859+
1860+
fn mark_invoice_received_and_get_details(
1861+
&self, invoice: &Bolt12Invoice, payment_id: PaymentId
1862+
) -> Result<(PaymentHash, Retry, RouteParametersConfig, bool), Bolt12PaymentError> {
1863+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
1864+
hash_map::Entry::Occupied(entry) => match entry.get() {
1865+
PendingOutboundPayment::AwaitingInvoice {
1866+
retry_strategy: retry, route_params_config, ..
1867+
} => {
1868+
let payment_hash = invoice.payment_hash();
1869+
let retry = *retry;
1870+
let config = *route_params_config;
1871+
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
1872+
payment_hash,
1873+
retry_strategy: retry,
1874+
route_params_config: config,
1875+
};
1876+
1877+
Ok((payment_hash, retry, config, true))
1878+
},
1879+
// When manual invoice handling is enabled, the corresponding `PendingOutboundPayment` entry
1880+
// is already updated at the time the invoice is received. This ensures that `InvoiceReceived`
1881+
// event generation remains idempotent, even if the same invoice is received again before the
1882+
// event is handled by the user.
1883+
PendingOutboundPayment::InvoiceReceived {
1884+
retry_strategy, route_params_config, ..
1885+
} => {
1886+
Ok((invoice.payment_hash(), *retry_strategy, *route_params_config, false))
1887+
},
1888+
_ => Err(Bolt12PaymentError::DuplicateInvoice),
1889+
},
1890+
hash_map::Entry::Vacant(_) => Err(Bolt12PaymentError::UnexpectedInvoice),
1891+
}
1892+
}
1893+
18661894
fn pay_route_internal<NS: Deref, F>(
18671895
&self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields,
18681896
keysend_preimage: Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>,

0 commit comments

Comments
 (0)