Don't auto-fail offers payments pre-HTLC lock in
What changed, and why it matters
This commit fixes a bug in the Lightning Dev Kit where certain asynchronous payments could be wrongly marked as failed before the actual payment HTLCs were locked in. The fix expands the definition of 'not yet ready to fail' to include payments that have received an invoice but have not yet locked in HTLCs, preventing premature abandonment.
Review and merge the patch; ensure downstream users relying on async/BOLT 12 payments update to include this fix, as premature abandonment could cause incorrect payment failure reporting and user confusion.
Security signals we found
Logic bug causing premature payment failure/abandonment
State-machine check too narrow for async payment lifecycle
Regression test added for the fixed behavior
No explicit security framing by vendor in commit message
Evidence from the diff
The patch renames is_awaiting_invoice() to is_pre_htlc_lock_in() and updates it to return true for AwaitingInvoice, InvoiceReceived, and StaticInvoiceReceived states. Two cleanup paths in OutboundPayments now use this broader check, so payments in InvoiceReceived/StaticInvoiceReceived are no longer auto-abandoned during process_pending_htlc_forwards before HTLCs are locked in. A regression test is added in async_payments_tests.rs.
Changed components
lightning/src/ln/outbound_payment.rslightning/src/ln/async_payments_tests.rsInspect captured patch +11 / −4
diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs
index 1fa1b77..8d3f678 100644
--- a/lightning/src/ln/async_payments_tests.rs
+++ b/lightning/src/ln/async_payments_tests.rs
@@ -240,6 +240,9 @@ fn pass_async_payments_oms(
.next_onion_message_for_peer(sender_node_id)
.unwrap();
sender.onion_messenger.handle_onion_message(always_online_node_id, &static_invoice_om);
+ // Check that the node will not lock in HTLCs yet.
+ sender.node.process_pending_htlc_forwards();
+ assert!(sender.node.get_and_clear_pending_msg_events().is_empty());
let held_htlc_available_om_0_1 =
sender.onion_messenger.next_onion_message_for_peer(always_online_node_id).unwrap();
diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs
index a66c21a..6b1719f 100644
--- a/lightning/src/ln/outbound_payment.rs
+++ b/lightning/src/ln/outbound_payment.rs
@@ -217,9 +217,13 @@ impl PendingOutboundPayment {
params.insert_previously_failed_blinded_path(blinded_tail);
}
}
- fn is_awaiting_invoice(&self) -> bool {
+ // Used for payments to BOLT 12 offers where we are either waiting for an invoice or have an
+ // invoice but have not locked in HTLCs for the payment yet.
+ fn is_pre_htlc_lock_in(&self) -> bool {
match self {
- PendingOutboundPayment::AwaitingInvoice { .. } => true,
+ PendingOutboundPayment::AwaitingInvoice { .. }
+ | PendingOutboundPayment::InvoiceReceived { .. }
+ | PendingOutboundPayment::StaticInvoiceReceived { .. } => true,
_ => false,
}
}
@@ -1368,7 +1372,7 @@ impl OutboundPayments {
let mut retain = true;
if !pmt.is_auto_retryable_now()
&& pmt.remaining_parts() == 0
- && !pmt.is_awaiting_invoice()
+ && !pmt.is_pre_htlc_lock_in()
{
pmt.mark_abandoned(PaymentFailureReason::RetriesExhausted);
if let PendingOutboundPayment::Abandoned { payment_hash, reason, .. } = pmt {
@@ -1396,7 +1400,7 @@ impl OutboundPayments {
|| !pmt.is_auto_retryable_now()
&& pmt.remaining_parts() == 0
&& !pmt.is_fulfilled()
- && !pmt.is_awaiting_invoice()
+ && !pmt.is_pre_htlc_lock_in()
})
}
Why this scored 57/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.