From eab5e3639704897c63772950621006729dbf2504 Mon Sep 17 00:00:00 2001 From: tsk Date: Thu, 20 Aug 2026 14:05:05 +0000 Subject: [PATCH 1/3] Ignore late failures for successful payments 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. --- src/event.rs | 33 ++++++++++++++++++++------ src/payment/store.rs | 55 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/event.rs b/src/event.rs index 728f625ab4..4e383b0f18 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1472,13 +1472,6 @@ where }; }, 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), @@ -1492,6 +1485,32 @@ where }, }; + // 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(()), diff --git a/src/payment/store.rs b/src/payment/store.rs index 41c39045f8..81421dfcfa 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -320,7 +320,20 @@ impl UpdatableObject for PaymentDetails { } if let Some(status) = update.status { - update_if_necessary!(self.status, status); + // LDK may, in exceedingly rare cases, emit `PaymentFailed` after + // `PaymentSent` for the same outbound payment. In that case the + // failure must be ignored and the payment must remain succeeded. + // + // Keep this invariant scoped to outbound Lightning payments as + // on-chain payment state may legitimately be revised after a reorg. + let is_outbound_lightning_payment = self.direction == PaymentDirection::Outbound + && !matches!(self.kind, PaymentKind::Onchain { .. }); + let is_late_failure = is_outbound_lightning_payment + && self.status == PaymentStatus::Succeeded + && status == PaymentStatus::Failed; + if !is_late_failure { + update_if_necessary!(self.status, status); + } } if let Some(confirmation_status) = update.confirmation_status { @@ -1592,6 +1605,23 @@ mod bounded_cache_tests { ) } + fn bolt12_payment(seed: u8) -> PaymentDetails { + PaymentDetails::new( + PaymentId([seed; 32]), + PaymentKind::Bolt12Refund { + hash: Some(PaymentHash([seed; 32])), + preimage: Some(PaymentPreimage([seed.wrapping_add(1); 32])), + secret: Some(PaymentSecret([seed.wrapping_add(2); 32])), + payer_note: None, + quantity: None, + }, + Some(seed as u64 * 1_000), + Some(seed as u64 * 3), + PaymentDirection::Outbound, + PaymentStatus::Succeeded, + ) + } + #[tokio::test] async fn evicted_payments_survive_a_round_trip_through_the_store() { // A bounded store hands back objects it deserialized rather than ones it kept, so every @@ -1617,6 +1647,10 @@ mod bounded_cache_tests { let data_store = new_bounded_payment_store(1); let mut stored = bolt11_payment(1); + stored.status = PaymentStatus::Pending; + if let PaymentKind::Bolt11 { ref mut preimage, .. } = stored.kind { + *preimage = None; + } stored.fee_paid_msat = Some(4_242); data_store.insert(stored.clone()).await.unwrap(); @@ -1634,6 +1668,25 @@ mod bounded_cache_tests { assert_eq!(stored.amount_msat, updated.amount_msat); } + #[tokio::test] + async fn late_failure_does_not_downgrade_succeeded_outbound_lightning_payment() { + let data_store = new_bounded_payment_store(1); + + for succeeded in [bolt11_payment(1), bolt12_payment(2)] { + data_store.insert(succeeded.clone()).await.unwrap(); + + // Exercise the persistence-backed update path rather than relying on the cache. + data_store.insert(bolt11_payment(3)).await.unwrap(); + + let mut update = PaymentDetailsUpdate::new(succeeded.id); + update.status = Some(PaymentStatus::Failed); + assert_eq!(Ok(DataStoreUpdateResult::Unchanged), data_store.update(update).await); + + let stored = data_store.get(&succeeded.id).await.unwrap().unwrap(); + assert_eq!(PaymentStatus::Succeeded, stored.status); + } + } + #[tokio::test] async fn listing_covers_payments_the_cache_cannot_hold() { let data_store = new_bounded_payment_store(3); From 8cf15f2aae99e26c1475a3e64701ed14b3fbbfdc Mon Sep 17 00:00:00 2001 From: tsk Date: Fri, 18 Sep 2026 11:36:02 +0000 Subject: [PATCH 2/3] fix(payments): ignore late failures atomically in the event handler Move the late-failure guard from payment-store updates into the event handler. Use mutate to preserve the entire succeeded Lightning payment and suppress its failure event without a second store lookup. Keep failure notifications for unchanged or missing records so replay still delivers the event. Cover BOLT11 and BOLT12 metadata preservation and notifications for pending, already-failed, and missing payments. Developed with assistance from OpenAI Codex. --- src/event.rs | 219 +++++++++++++++++++++++++++++++++---------- src/payment/store.rs | 51 +--------- 2 files changed, 173 insertions(+), 97 deletions(-) diff --git a/src/event.rs b/src/event.rs index 4e383b0f18..ddd566b65d 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; @@ -695,6 +695,57 @@ where } } + async fn handle_payment_failed( + payment_store: &PaymentStore, event_queue: &EventQueue, logger: &L, + payment_id: PaymentId, payment_hash: Option, + reason: Option, + ) -> Result<(), ReplayEvent> { + let mut ignore_late_failure = false; + 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 + && !matches!(current.kind, PaymentKind::Onchain { .. }) + && 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!(logger, "Failed to access payment store: {}", e); + ReplayEvent() + })?; + + if ignore_late_failure { + log_info!( + 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!(logger, "Failed to send payment with ID {} due to {:?}.", payment_id, reason); + let event = Event::PaymentFailed { payment_id, payment_hash, reason }; + event_queue.add_event(event).await.map_err(|e| { + log_error!(logger, "Failed to push to event queue: {}", e); + ReplayEvent() + }) + } + fn lsps2_max_total_opening_fee_msat(payment_metadata: &[u8], amount_msat: u64) -> Option { let metadata = PaymentMetadata::read(&mut &payment_metadata[..]).ok()?; let lsps2_parameters = metadata.lsps2_parameters?; @@ -1472,53 +1523,15 @@ where }; }, LdkEvent::PaymentFailed { payment_id, payment_hash, 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 {:?}.", + return Self::handle_payment_failed( + &self.payment_store, + &self.event_queue, + &self.logger, payment_id, - reason - ); - - let event = Event::PaymentFailed { payment_id, payment_hash, reason }; - match self.event_queue.add_event(event).await { - Ok(_) => return Ok(()), - Err(e) => { - log_error!(self.logger, "Failed to push to event queue: {}", e); - return Err(ReplayEvent()); - }, - }; + payment_hash, + reason, + ) + .await; }, LdkEvent::PaymentPathSuccessful { .. } => {}, @@ -2283,6 +2296,118 @@ mod tests { use crate::payment::store::LSPS2Parameters; use crate::types::DynStoreWrapper; + fn payment_failure_test_store() -> (PaymentStore, EventQueue>, Arc) + { + let kv_store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); + let logger = Arc::new(TestLogger::new()); + let store = PaymentStore::new( + Vec::new(), + crate::data_store::KeepLeastRecentlyUsed::new(std::num::NonZeroUsize::new(1).unwrap()), + crate::io::PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + crate::io::PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::new(Logger::new_log_facade()), + ); + (store, EventQueue::new(kv_store, Arc::clone(&logger)), logger) + } + + fn outbound_test_payment(kind: PaymentKind, status: PaymentStatus) -> PaymentDetails { + PaymentDetails::new( + PaymentId([1; 32]), + kind, + Some(1_000), + Some(10), + PaymentDirection::Outbound, + status, + ) + } + + #[tokio::test] + async fn payment_failed_ignores_entire_late_failure() { + let hash = PaymentHash([2; 32]); + let preimage = Some(PaymentPreimage([3; 32])); + for kind in [ + PaymentKind::Bolt11 { + hash, + preimage, + secret: None, + counterparty_skimmed_fee_msat: None, + }, + PaymentKind::Bolt12Offer { + hash: Some(hash), + preimage, + secret: None, + offer_id: lightning::offers::offer::OfferId([4; 32]), + payer_note: None, + quantity: None, + }, + PaymentKind::Bolt12Refund { + hash: Some(hash), + preimage, + secret: None, + payer_note: None, + quantity: None, + }, + ] { + let (store, queue, logger) = payment_failure_test_store(); + let succeeded = outbound_test_payment(kind, PaymentStatus::Succeeded); + store.insert(succeeded.clone()).await.unwrap(); + // Evict the successful payment to exercise the persistence-backed path. + let mut other = succeeded.clone(); + other.id = PaymentId([9; 32]); + store.insert(other).await.unwrap(); + + for failure_hash in [None, Some(hash)] { + EventHandler::handle_payment_failed( + &store, + &queue, + &logger, + succeeded.id, + failure_hash, + None, + ) + .await + .unwrap(); + assert_eq!(store.get(&succeeded.id).await.unwrap(), Some(succeeded.clone())); + assert_eq!(queue.next_event(), None); + } + } + } + + #[tokio::test] + async fn payment_failed_emits_for_pending_replayed_and_missing_payments() { + let (store, queue, logger) = payment_failure_test_store(); + let hash = PaymentHash([2; 32]); + let pending = outbound_test_payment( + PaymentKind::Bolt11 { + hash, + preimage: None, + secret: None, + counterparty_skimmed_fee_msat: None, + }, + PaymentStatus::Pending, + ); + store.insert(pending.clone()).await.unwrap(); + let missing_id = PaymentId([9; 32]); + // The second call exercises replay after the store has already recorded failure. + for id in [pending.id, pending.id, missing_id] { + EventHandler::handle_payment_failed(&store, &queue, &logger, id, Some(hash), None) + .await + .unwrap(); + assert_eq!( + queue.next_event(), + Some(Event::PaymentFailed { + payment_id: id, + payment_hash: Some(hash), + reason: None, + }) + ); + queue.event_handled().await.unwrap(); + } + assert_eq!(store.get(&pending.id).await.unwrap().unwrap().status, PaymentStatus::Failed); + assert_eq!(store.get(&missing_id).await.unwrap(), None); + } + fn htlc_locator(channel_byte: u8) -> HTLCLocator { HTLCLocator { channel_id: ChannelId([channel_byte; 32]), diff --git a/src/payment/store.rs b/src/payment/store.rs index 81421dfcfa..44120959ff 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -320,20 +320,7 @@ impl UpdatableObject for PaymentDetails { } if let Some(status) = update.status { - // LDK may, in exceedingly rare cases, emit `PaymentFailed` after - // `PaymentSent` for the same outbound payment. In that case the - // failure must be ignored and the payment must remain succeeded. - // - // Keep this invariant scoped to outbound Lightning payments as - // on-chain payment state may legitimately be revised after a reorg. - let is_outbound_lightning_payment = self.direction == PaymentDirection::Outbound - && !matches!(self.kind, PaymentKind::Onchain { .. }); - let is_late_failure = is_outbound_lightning_payment - && self.status == PaymentStatus::Succeeded - && status == PaymentStatus::Failed; - if !is_late_failure { - update_if_necessary!(self.status, status); - } + update_if_necessary!(self.status, status); } if let Some(confirmation_status) = update.confirmation_status { @@ -1605,23 +1592,6 @@ mod bounded_cache_tests { ) } - fn bolt12_payment(seed: u8) -> PaymentDetails { - PaymentDetails::new( - PaymentId([seed; 32]), - PaymentKind::Bolt12Refund { - hash: Some(PaymentHash([seed; 32])), - preimage: Some(PaymentPreimage([seed.wrapping_add(1); 32])), - secret: Some(PaymentSecret([seed.wrapping_add(2); 32])), - payer_note: None, - quantity: None, - }, - Some(seed as u64 * 1_000), - Some(seed as u64 * 3), - PaymentDirection::Outbound, - PaymentStatus::Succeeded, - ) - } - #[tokio::test] async fn evicted_payments_survive_a_round_trip_through_the_store() { // A bounded store hands back objects it deserialized rather than ones it kept, so every @@ -1668,25 +1638,6 @@ mod bounded_cache_tests { assert_eq!(stored.amount_msat, updated.amount_msat); } - #[tokio::test] - async fn late_failure_does_not_downgrade_succeeded_outbound_lightning_payment() { - let data_store = new_bounded_payment_store(1); - - for succeeded in [bolt11_payment(1), bolt12_payment(2)] { - data_store.insert(succeeded.clone()).await.unwrap(); - - // Exercise the persistence-backed update path rather than relying on the cache. - data_store.insert(bolt11_payment(3)).await.unwrap(); - - let mut update = PaymentDetailsUpdate::new(succeeded.id); - update.status = Some(PaymentStatus::Failed); - assert_eq!(Ok(DataStoreUpdateResult::Unchanged), data_store.update(update).await); - - let stored = data_store.get(&succeeded.id).await.unwrap().unwrap(); - assert_eq!(PaymentStatus::Succeeded, stored.status); - } - } - #[tokio::test] async fn listing_covers_payments_the_cache_cannot_hold() { let data_store = new_bounded_payment_store(3); From 1accb18802bcd24657a37d81db4171b29ffde793 Mon Sep 17 00:00:00 2001 From: tsk Date: Fri, 18 Sep 2026 12:12:07 +0000 Subject: [PATCH 3/3] refactor(payments): keep late-failure handling inline Move late-failure handling into the PaymentFailed match arm to match the existing event-handler structure. Restore the original event-queue error handling and remove the helper-dependent tests as requested in review. Remove leftover status and preimage changes from the payment-store test. Developed with assistance from OpenAI Codex. --- src/event.rs | 222 ++++++++++--------------------------------- src/payment/store.rs | 4 - 2 files changed, 51 insertions(+), 175 deletions(-) diff --git a/src/event.rs b/src/event.rs index ddd566b65d..d0053609b5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -695,57 +695,6 @@ where } } - async fn handle_payment_failed( - payment_store: &PaymentStore, event_queue: &EventQueue, logger: &L, - payment_id: PaymentId, payment_hash: Option, - reason: Option, - ) -> Result<(), ReplayEvent> { - let mut ignore_late_failure = false; - 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 - && !matches!(current.kind, PaymentKind::Onchain { .. }) - && 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!(logger, "Failed to access payment store: {}", e); - ReplayEvent() - })?; - - if ignore_late_failure { - log_info!( - 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!(logger, "Failed to send payment with ID {} due to {:?}.", payment_id, reason); - let event = Event::PaymentFailed { payment_id, payment_hash, reason }; - event_queue.add_event(event).await.map_err(|e| { - log_error!(logger, "Failed to push to event queue: {}", e); - ReplayEvent() - }) - } - fn lsps2_max_total_opening_fee_msat(payment_metadata: &[u8], amount_msat: u64) -> Option { let metadata = PaymentMetadata::read(&mut &payment_metadata[..]).ok()?; let lsps2_parameters = metadata.lsps2_parameters?; @@ -1523,15 +1472,58 @@ where }; }, LdkEvent::PaymentFailed { payment_id, payment_hash, reason, .. } => { - return Self::handle_payment_failed( - &self.payment_store, - &self.event_queue, - &self.logger, + 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 + && !matches!(current.kind, PaymentKind::Onchain { .. }) + && 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, - payment_hash, - reason, - ) - .await; + reason + ); + let event = Event::PaymentFailed { payment_id, payment_hash, reason }; + match self.event_queue.add_event(event).await { + Ok(_) => return Ok(()), + Err(e) => { + log_error!(self.logger, "Failed to push to event queue: {}", e); + return Err(ReplayEvent()); + }, + }; }, LdkEvent::PaymentPathSuccessful { .. } => {}, @@ -2296,118 +2288,6 @@ mod tests { use crate::payment::store::LSPS2Parameters; use crate::types::DynStoreWrapper; - fn payment_failure_test_store() -> (PaymentStore, EventQueue>, Arc) - { - let kv_store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); - let logger = Arc::new(TestLogger::new()); - let store = PaymentStore::new( - Vec::new(), - crate::data_store::KeepLeastRecentlyUsed::new(std::num::NonZeroUsize::new(1).unwrap()), - crate::io::PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), - crate::io::PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), - Arc::clone(&kv_store), - Arc::new(Logger::new_log_facade()), - ); - (store, EventQueue::new(kv_store, Arc::clone(&logger)), logger) - } - - fn outbound_test_payment(kind: PaymentKind, status: PaymentStatus) -> PaymentDetails { - PaymentDetails::new( - PaymentId([1; 32]), - kind, - Some(1_000), - Some(10), - PaymentDirection::Outbound, - status, - ) - } - - #[tokio::test] - async fn payment_failed_ignores_entire_late_failure() { - let hash = PaymentHash([2; 32]); - let preimage = Some(PaymentPreimage([3; 32])); - for kind in [ - PaymentKind::Bolt11 { - hash, - preimage, - secret: None, - counterparty_skimmed_fee_msat: None, - }, - PaymentKind::Bolt12Offer { - hash: Some(hash), - preimage, - secret: None, - offer_id: lightning::offers::offer::OfferId([4; 32]), - payer_note: None, - quantity: None, - }, - PaymentKind::Bolt12Refund { - hash: Some(hash), - preimage, - secret: None, - payer_note: None, - quantity: None, - }, - ] { - let (store, queue, logger) = payment_failure_test_store(); - let succeeded = outbound_test_payment(kind, PaymentStatus::Succeeded); - store.insert(succeeded.clone()).await.unwrap(); - // Evict the successful payment to exercise the persistence-backed path. - let mut other = succeeded.clone(); - other.id = PaymentId([9; 32]); - store.insert(other).await.unwrap(); - - for failure_hash in [None, Some(hash)] { - EventHandler::handle_payment_failed( - &store, - &queue, - &logger, - succeeded.id, - failure_hash, - None, - ) - .await - .unwrap(); - assert_eq!(store.get(&succeeded.id).await.unwrap(), Some(succeeded.clone())); - assert_eq!(queue.next_event(), None); - } - } - } - - #[tokio::test] - async fn payment_failed_emits_for_pending_replayed_and_missing_payments() { - let (store, queue, logger) = payment_failure_test_store(); - let hash = PaymentHash([2; 32]); - let pending = outbound_test_payment( - PaymentKind::Bolt11 { - hash, - preimage: None, - secret: None, - counterparty_skimmed_fee_msat: None, - }, - PaymentStatus::Pending, - ); - store.insert(pending.clone()).await.unwrap(); - let missing_id = PaymentId([9; 32]); - // The second call exercises replay after the store has already recorded failure. - for id in [pending.id, pending.id, missing_id] { - EventHandler::handle_payment_failed(&store, &queue, &logger, id, Some(hash), None) - .await - .unwrap(); - assert_eq!( - queue.next_event(), - Some(Event::PaymentFailed { - payment_id: id, - payment_hash: Some(hash), - reason: None, - }) - ); - queue.event_handled().await.unwrap(); - } - assert_eq!(store.get(&pending.id).await.unwrap().unwrap().status, PaymentStatus::Failed); - assert_eq!(store.get(&missing_id).await.unwrap(), None); - } - fn htlc_locator(channel_byte: u8) -> HTLCLocator { HTLCLocator { channel_id: ChannelId([channel_byte; 32]), diff --git a/src/payment/store.rs b/src/payment/store.rs index 44120959ff..41c39045f8 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -1617,10 +1617,6 @@ mod bounded_cache_tests { let data_store = new_bounded_payment_store(1); let mut stored = bolt11_payment(1); - stored.status = PaymentStatus::Pending; - if let PaymentKind::Bolt11 { ref mut preimage, .. } = stored.kind { - *preimage = None; - } stored.fee_paid_msat = Some(4_242); data_store.insert(stored.clone()).await.unwrap();