Skip to content

Commit 4df77f4

Browse files
committed
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.
1 parent be1d4e6 commit 4df77f4

2 files changed

Lines changed: 80 additions & 8 deletions

File tree

src/event.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,13 +1457,6 @@ where
14571457
};
14581458
},
14591459
LdkEvent::PaymentFailed { payment_id, payment_hash, reason, .. } => {
1460-
log_info!(
1461-
self.logger,
1462-
"Failed to send payment with ID {} due to {:?}.",
1463-
payment_id,
1464-
reason
1465-
);
1466-
14671460
let update = PaymentDetailsUpdate {
14681461
hash: Some(payment_hash),
14691462
status: Some(PaymentStatus::Failed),
@@ -1477,6 +1470,32 @@ where
14771470
},
14781471
};
14791472

1473+
// LDK may emit `PaymentFailed` after `PaymentSent` in exceedingly rare cases.
1474+
// The payment-store update above preserves success in that case; re-read the
1475+
// resulting state so we also avoid surfacing a contradictory public event.
1476+
match self.payment_store.get(&payment_id).await {
1477+
Ok(Some(payment)) if payment.status == PaymentStatus::Succeeded => {
1478+
log_info!(
1479+
self.logger,
1480+
"Ignoring late payment failure for already-succeeded payment with ID {}.",
1481+
payment_id
1482+
);
1483+
return Ok(());
1484+
},
1485+
Ok(_) => {},
1486+
Err(e) => {
1487+
log_error!(self.logger, "Failed to access payment store: {}", e);
1488+
return Err(ReplayEvent());
1489+
},
1490+
}
1491+
1492+
log_info!(
1493+
self.logger,
1494+
"Failed to send payment with ID {} due to {:?}.",
1495+
payment_id,
1496+
reason
1497+
);
1498+
14801499
let event = Event::PaymentFailed { payment_id, payment_hash, reason };
14811500
match self.event_queue.add_event(event).await {
14821501
Ok(_) => return Ok(()),

src/payment/store.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,20 @@ impl StorableObject for PaymentDetails {
317317
}
318318

319319
if let Some(status) = update.status {
320-
update_if_necessary!(self.status, status);
320+
// LDK may, in exceedingly rare cases, emit `PaymentFailed` after
321+
// `PaymentSent` for the same outbound payment. In that case the
322+
// failure must be ignored and the payment must remain succeeded.
323+
//
324+
// Keep this invariant scoped to outbound Lightning payments as
325+
// on-chain payment state may legitimately be revised after a reorg.
326+
let is_outbound_lightning_payment = self.direction == PaymentDirection::Outbound
327+
&& !matches!(self.kind, PaymentKind::Onchain { .. });
328+
let is_late_failure = is_outbound_lightning_payment
329+
&& self.status == PaymentStatus::Succeeded
330+
&& status == PaymentStatus::Failed;
331+
if !is_late_failure {
332+
update_if_necessary!(self.status, status);
333+
}
321334
}
322335

323336
if let Some(confirmation_status) = update.confirmation_status {
@@ -1589,6 +1602,23 @@ mod bounded_cache_tests {
15891602
)
15901603
}
15911604

1605+
fn bolt12_payment(seed: u8) -> PaymentDetails {
1606+
PaymentDetails::new(
1607+
PaymentId([seed; 32]),
1608+
PaymentKind::Bolt12Refund {
1609+
hash: Some(PaymentHash([seed; 32])),
1610+
preimage: Some(PaymentPreimage([seed.wrapping_add(1); 32])),
1611+
secret: Some(PaymentSecret([seed.wrapping_add(2); 32])),
1612+
payer_note: None,
1613+
quantity: None,
1614+
},
1615+
Some(seed as u64 * 1_000),
1616+
Some(seed as u64 * 3),
1617+
PaymentDirection::Outbound,
1618+
PaymentStatus::Succeeded,
1619+
)
1620+
}
1621+
15921622
#[tokio::test]
15931623
async fn evicted_payments_survive_a_round_trip_through_the_store() {
15941624
// A bounded store hands back objects it deserialized rather than ones it kept, so every
@@ -1614,6 +1644,10 @@ mod bounded_cache_tests {
16141644
let data_store = new_bounded_payment_store(1);
16151645

16161646
let mut stored = bolt11_payment(1);
1647+
stored.status = PaymentStatus::Pending;
1648+
if let PaymentKind::Bolt11 { ref mut preimage, .. } = stored.kind {
1649+
*preimage = None;
1650+
}
16171651
stored.fee_paid_msat = Some(4_242);
16181652
data_store.insert(stored.clone()).await.unwrap();
16191653

@@ -1631,6 +1665,25 @@ mod bounded_cache_tests {
16311665
assert_eq!(stored.amount_msat, updated.amount_msat);
16321666
}
16331667

1668+
#[tokio::test]
1669+
async fn late_failure_does_not_downgrade_succeeded_outbound_lightning_payment() {
1670+
let data_store = new_bounded_payment_store(1);
1671+
1672+
for succeeded in [bolt11_payment(1), bolt12_payment(2)] {
1673+
data_store.insert(succeeded.clone()).await.unwrap();
1674+
1675+
// Exercise the persistence-backed update path rather than relying on the cache.
1676+
data_store.insert(bolt11_payment(3)).await.unwrap();
1677+
1678+
let mut update = PaymentDetailsUpdate::new(succeeded.id);
1679+
update.status = Some(PaymentStatus::Failed);
1680+
assert_eq!(Ok(DataStoreUpdateResult::Unchanged), data_store.update(update).await);
1681+
1682+
let stored = data_store.get(&succeeded.id).await.unwrap().unwrap();
1683+
assert_eq!(PaymentStatus::Succeeded, stored.status);
1684+
}
1685+
}
1686+
16341687
#[tokio::test]
16351688
async fn listing_covers_payments_the_cache_cannot_hold() {
16361689
let data_store = new_bounded_payment_store(3);

0 commit comments

Comments
 (0)