Skip to content

Ignore late failures for successful payments - #1065

Merged
tnull merged 1 commit into
lightningdevkit:mainfrom
thesimplekid:late_failures
Sep 22, 2026
Merged

tnull merged 1 commit into
lightningdevkit:mainfrom
thesimplekid:late_failures

Conversation

@thesimplekid

Copy link
Copy Markdown
Contributor

rust-lightning documents that PaymentFailed can arrive after PaymentSent in rare cases. In that ordering, the failure must be ignored and the payment must be treated as successful:

https://github.com/lightningdevkit/rust-lightning/blob/9174965af9437196c527a9aa0df36bbcf050c8bb/lightning/src/events/mod.rs#L1230-L1233

Keep succeeded outbound Lightning records monotonic and suppress the contradictory user-facing PaymentFailed event. Cover both BOLT11 and BOLT12 on the persistence-backed store path.

Developed with assistance from OpenAI Codex.

@ldk-reviews-bot

ldk-reviews-bot commented Aug 20, 2026 •

Copy link
Copy Markdown

I've assigned @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ldk-reviews-bot
ldk-reviews-bot requested a review from tnull August 20, 2026 14:46
Comment thread src/event.rs
Comment on lines 1457 to 1501
};
},
LdkEvent::PaymentFailed { payment_id, payment_hash, reason, .. } => {
log_info!(
self.logger,
"Failed to send payment with ID {} due to {:?}.",
payment_id,
reason
);

let update = PaymentDetailsUpdate {
hash: Some(payment_hash),
status: Some(PaymentStatus::Failed),
..PaymentDetailsUpdate::new(payment_id)
};
match self.payment_store.update(update).await {
Ok(_) => {},
Err(e) => {
log_error!(self.logger, "Failed to access payment store: {}", e);
return Err(ReplayEvent());
},
};

// LDK may emit `PaymentFailed` after `PaymentSent` in exceedingly rare cases.
// The payment-store update above preserves success in that case; re-read the
// resulting state so we also avoid surfacing a contradictory public event.
match self.payment_store.get(&payment_id).await {
Ok(Some(payment)) if payment.status == PaymentStatus::Succeeded => {
log_info!(
self.logger,
"Ignoring late payment failure for already-succeeded payment with ID {}.",
payment_id
);
return Ok(());
},
Ok(_) => {},
Err(e) => {
log_error!(self.logger, "Failed to access payment store: {}", e);
return Err(ReplayEvent());
},
}

log_info!(
self.logger,
"Failed to send payment with ID {} due to {:?}.",
payment_id,
reason
);

let event = Event::PaymentFailed { payment_id, payment_hash, reason };
match self.event_queue.add_event(event).await {
Ok(_) => return Ok(()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is again related to the split-brain situation with persistence, but can the initial payment store write fail after LDK accepts the payment? PaymentSent then accepts Ok(NotFound) and emits success, so a replayed late failure could escape this check after restart.

@tnull tnull left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excuse the delay here, good catch.

Comment thread src/payment/store.rs Resolved
Comment thread src/event.rs Resolved
Comment thread src/event.rs Resolved
Comment thread src/payment/store.rs Resolved

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please squash, one minor comment though.

Comment thread src/event.rs Resolved

@tnull tnull left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please squash.

Ignore PaymentFailed when an outbound Lightning payment has already
succeeded. Check and update the record atomically, preserving its metadata
and suppressing the contradictory failure notification.

Continue emitting failure events for unchanged or missing records so
replay can recover from event-queue persistence failures.

Developed with assistance from OpenAI Codex.
@thesimplekid

Copy link
Copy Markdown
Contributor Author

Sorry, squashed into 1 commit now when you said prefactor commit I thought you wanted that one on its own.

@tnull

tnull commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Sorry, squashed into 1 commit now when you said prefactor commit I thought you wanted that one on its own.

Ah, indeed. Just skimmed the history and saw the fix so I assumed it was a fixup. Sorry for the confusion, but doesnt' matter all too much now.

@tnull
tnull merged commit 9e1d297 into lightningdevkit:main Sep 22, 2026
23 of 25 checks passed
joemphilips added a commit to joemphilips/cdk that referenced this pull request Nov 11, 2026
ldk-node 0.7.0 marks an in-flight outbound payment Failed when an
inbound HTLC uses the same payment hash. cdk-ldk-node reports that
status as a failed melt. A melt requester who holds the HTLC of their
own invoice and keysends the same hash to the mint could thus release
the melt proofs and then settle the HTLC.

ldk-node 0.7.1 fixes these bugs:
- an inbound HTLC that reuses an outbound payment hash no longer fails
  the outbound payment (lightningdevkit/ldk-node#1157);
- a late PaymentFailed no longer overwrites a succeeded payment
  (lightningdevkit/ldk-node#1065);
- a ChannelManager read error stops the build instead of starting a
  fresh node (lightningdevkit/ldk-node#1032).

lightning 0.2.8 (with lightning-liquidity, lightning-transaction-sync
and lightning-persister 0.2.8, and lightning-invoice 0.34.2) fixes:
- a malicious BOLT11 route hint that removes channels from the graph
  (lightningdevkit/rust-lightning#5503);
- an MPP startup panic after a channel monitor is archived (#5528);
- a ChannelManager that fails to load after several payments to one
  hash (#5541);
- anchor commitments held back when the fee-bump input cannot be
  funded (4825c089e);
- 64-byte transactions skipped in Esplora sync (#5559).

Only these six packages change in Cargo.lock and Cargo.lock.msrv.
cargo update also re-resolved unrelated multi-version range
dependencies, so those edges keep their earlier versions. The other
LDK crates are already at their latest compatible releases. The
workspace floors now require the fixed versions, so a regenerated lock
cannot select the vulnerable ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDmqYFNSovaSho9M89aXC9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants