From 1bb6aa38641c30687dbf6509abac5c3a06820e5c Mon Sep 17 00:00:00 2001 From: tsk Date: Tue, 22 Sep 2026 12:11:53 +0000 Subject: [PATCH] fix(payments): ignore late failures for successful Lightning payments 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. --- src/event.rs | 54 ++++++++++++++++++++++++++++++++------------ src/payment/store.rs | 12 ++++++++++ 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/event.rs b/src/event.rs index 728f625ab4..e700873cdc 100644 --- a/src/event.rs +++ b/src/event.rs @@ -37,7 +37,7 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage}; use crate::config::{may_announce_channel, Config, PEER_RECONNECTION_INTERVAL}; use crate::connection::ConnectionManager; -use crate::data_store::DataStoreUpdateResult; +use crate::data_store::{DataStoreUpdateResult, UpdatableObject}; use crate::fee_estimator::ConfirmationTarget; #[cfg(feature = "uniffi")] use crate::ffi::PaidBolt12Invoice; @@ -1472,26 +1472,50 @@ where }; }, LdkEvent::PaymentFailed { payment_id, payment_hash, reason, .. } => { + let mut ignore_late_failure = false; + self.payment_store + .mutate(&payment_id, |current| { + let current = current?; + // LDK may emit PaymentFailed after PaymentSent. Ignore the entire late + // failure so it cannot alter payment metadata or emit a contradictory event. + if current.direction == PaymentDirection::Outbound + && current.kind.is_lightning_payment() + && current.status == PaymentStatus::Succeeded + { + ignore_late_failure = true; + return None; + } + let mut updated = current.clone(); + let update = PaymentDetailsUpdate { + hash: Some(payment_hash), + status: Some(PaymentStatus::Failed), + ..PaymentDetailsUpdate::new(payment_id) + }; + updated.update(update).then_some(updated) + }) + .await + .map_err(|e| { + log_error!(self.logger, "Failed to access payment store: {}", e); + ReplayEvent() + })?; + + if ignore_late_failure { + log_info!( + self.logger, + "Ignoring late payment failure for already-succeeded payment with ID {}.", + payment_id + ); + return Ok(()); + } + + // An unchanged or absent record must still emit the event, including on replay + // after the payment update succeeded but event-queue persistence failed. 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()); - }, - }; - let event = Event::PaymentFailed { payment_id, payment_hash, reason }; match self.event_queue.add_event(event).await { Ok(_) => return Ok(()), diff --git a/src/payment/store.rs b/src/payment/store.rs index 41c39045f8..46cc57b87b 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -652,6 +652,18 @@ pub enum PaymentKind { }, } +impl PaymentKind { + pub(crate) fn is_lightning_payment(&self) -> bool { + match self { + Self::Onchain { .. } => false, + Self::Bolt11 { .. } + | Self::Bolt12Offer { .. } + | Self::Bolt12Refund { .. } + | Self::Spontaneous { .. } => true, + } + } +} + impl_writeable_tlv_based_enum!(PaymentKind, (0, Onchain) => { (0, txid, required),