Skip to content

Commit d5f149d

Browse files
shaavanTheBlueMatt
authored andcommitted
Make InvoiceReceived event generation idempotent
Ensures `InvoiceReceived` events are not generated multiple times when `manually_handle_bolt12_invoice` is enabled. Duplicate events for the same invoice could cause confusion—this change introduces an idempotency check to prevent that. Conflicts resolved in: * lightning/src/ln/outbound_payment.rs due to the migration upstream from `max_total_routing_fee_msat` to a more general config struct.
1 parent 406e031 commit d5f149d

File tree

2 files changed

+55
-23
lines changed

2 files changed

+55
-23
lines changed

lightning/src/ln/channelmanager.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12148,6 +12148,11 @@ where
1214812148
);
1214912149

1215012150
if self.default_configuration.manually_handle_bolt12_invoices {
12151+
// Update the corresponding entry in `PendingOutboundPayment` for this invoice.
12152+
// This ensures that event generation remains idempotent in case we receive
12153+
// the same invoice multiple times.
12154+
self.pending_outbound_payments.mark_invoice_received(&invoice, payment_id).ok()?;
12155+
1215112156
let event = Event::InvoiceReceived {
1215212157
payment_id, invoice, context, responder,
1215312158
};

lightning/src/ln/outbound_payment.rs

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ pub(crate) enum PendingOutboundPayment {
7373
max_total_routing_fee_msat: Option<u64>,
7474
retryable_invoice_request: Option<RetryableInvoiceRequest>
7575
},
76-
// This state will never be persisted to disk because we transition from `AwaitingInvoice` to
77-
// `Retryable` atomically within the `ChannelManager::total_consistency_lock`. Useful to avoid
78-
// holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding.
76+
// Represents the state after the invoice has been received, transitioning from the corresponding
77+
// `AwaitingInvoice` state.
78+
// Helps avoid holding the `OutboundPayments::pending_outbound_payments` lock during pathfinding.
7979
InvoiceReceived {
8080
payment_hash: PaymentHash,
8181
retry_strategy: Retry,
@@ -833,26 +833,8 @@ impl OutboundPayments {
833833
IH: Fn() -> InFlightHtlcs,
834834
SP: Fn(SendAlongPathArgs) -> Result<(), APIError>,
835835
{
836-
let payment_hash = invoice.payment_hash();
837-
let max_total_routing_fee_msat;
838-
let retry_strategy;
839-
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
840-
hash_map::Entry::Occupied(entry) => match entry.get() {
841-
PendingOutboundPayment::AwaitingInvoice {
842-
retry_strategy: retry, max_total_routing_fee_msat: max_total_fee, ..
843-
} => {
844-
retry_strategy = *retry;
845-
max_total_routing_fee_msat = *max_total_fee;
846-
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
847-
payment_hash,
848-
retry_strategy: *retry,
849-
max_total_routing_fee_msat,
850-
};
851-
},
852-
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
853-
},
854-
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
855-
}
836+
let (payment_hash, retry_strategy, max_total_routing_fee_msat, _) = self
837+
.mark_invoice_received_and_get_details(invoice, payment_id)?;
856838

857839
if invoice.invoice_features().requires_unknown_bits_from(&features) {
858840
self.abandon_payment(
@@ -1754,6 +1736,51 @@ impl OutboundPayments {
17541736
}
17551737
}
17561738

1739+
pub(super) fn mark_invoice_received(
1740+
&self, invoice: &Bolt12Invoice, payment_id: PaymentId
1741+
) -> Result<(), Bolt12PaymentError> {
1742+
self.mark_invoice_received_and_get_details(invoice, payment_id)
1743+
.and_then(|(_, _, _, is_newly_marked)| {
1744+
is_newly_marked
1745+
.then_some(())
1746+
.ok_or(Bolt12PaymentError::DuplicateInvoice)
1747+
})
1748+
}
1749+
1750+
fn mark_invoice_received_and_get_details(
1751+
&self, invoice: &Bolt12Invoice, payment_id: PaymentId
1752+
) -> Result<(PaymentHash, Retry, Option<u64>, bool), Bolt12PaymentError> {
1753+
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
1754+
hash_map::Entry::Occupied(entry) => match entry.get() {
1755+
PendingOutboundPayment::AwaitingInvoice {
1756+
retry_strategy: retry, max_total_routing_fee_msat: max_total_fee, ..
1757+
} => {
1758+
let payment_hash = invoice.payment_hash();
1759+
let retry = *retry;
1760+
let max_total_fee = *max_total_fee;
1761+
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
1762+
payment_hash,
1763+
retry_strategy: retry,
1764+
max_total_routing_fee_msat: max_total_fee,
1765+
};
1766+
1767+
Ok((payment_hash, retry, max_total_fee, true))
1768+
},
1769+
// When manual invoice handling is enabled, the corresponding `PendingOutboundPayment` entry
1770+
// is already updated at the time the invoice is received. This ensures that `InvoiceReceived`
1771+
// event generation remains idempotent, even if the same invoice is received again before the
1772+
// event is handled by the user.
1773+
PendingOutboundPayment::InvoiceReceived {
1774+
retry_strategy, max_total_routing_fee_msat, ..
1775+
} => {
1776+
Ok((invoice.payment_hash(), *retry_strategy, *max_total_routing_fee_msat, false))
1777+
},
1778+
_ => Err(Bolt12PaymentError::DuplicateInvoice),
1779+
},
1780+
hash_map::Entry::Vacant(_) => Err(Bolt12PaymentError::UnexpectedInvoice),
1781+
}
1782+
}
1783+
17571784
fn pay_route_internal<NS: Deref, F>(
17581785
&self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields,
17591786
keysend_preimage: Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>,

0 commit comments

Comments
 (0)