Skip to content

Commit 6168bb0

Browse files
jkczyzclaude
andcommitted
f - Recreate a missing pending index while the payment is still Pending
Classification treated an absent pending entry as proof the payment had graduated and only merged into existing entries. But a crash or failed write between the payment-store and pending-store writes leaves a Pending record with no index entry, and that state was never repaired: the payment could no longer graduate (graduation iterates the pending store) and its candidate txids could no longer be mapped back to the record, which for an RBF splice invites a duplicate generic payment. Decide by the post-write payment status instead: while the record is still Pending, insert the missing entry (embedding the post-write record, so a confirmation wallet sync already mirrored keeps driving graduation); once it advanced beyond Pending, keep treating absence as graduated. A graduated payment is never Pending, so the no-reindex rule is preserved by the status gate itself. The repaired state is not constructible in a test: it requires a failure injected between the two store writes, and no such seam exists. The store primitives the decision rests on are unit-tested. Generated with assistance from Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 435caa6 commit 6168bb0

1 file changed

Lines changed: 21 additions & 19 deletions

File tree

src/wallet/mod.rs

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ use lightning_invoice::RawBolt11Invoice;
5454
use persist::KVStoreWalletPersister;
5555

5656
use crate::config::Config;
57-
use crate::data_store::DataStoreUpdateOrInsertResult;
5857
use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator};
5958
use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
6059
use crate::payment::pending_payment_store::PendingPaymentDetailsUpdate;
@@ -1419,24 +1418,27 @@ impl Wallet {
14191418
conflicting_txids: None,
14201419
candidates: candidates.clone(),
14211420
};
1422-
match self.payment_store.update_or_insert(update, details.clone()).await? {
1423-
DataStoreUpdateOrInsertResult::Inserted => {
1424-
// First time we record this funding payment: index it for graduation. Wallet sync
1425-
// can still land between the payment-store write above and this one and mirror an
1426-
// advanced confirmation into the pending store, so this write makes the same
1427-
// atomic decision: merge narrowly into an entry that appeared, insert the fresh
1428-
// one otherwise.
1429-
let pending = PendingPaymentDetails::new(details, Vec::new(), candidates);
1430-
self.pending_payment_store.update_or_insert(pending_update, pending).await?;
1431-
},
1432-
DataStoreUpdateOrInsertResult::Updated | DataStoreUpdateOrInsertResult::Unchanged => {
1433-
// An earlier candidate or a racing wallet sync already recorded this payment.
1434-
// `update` is a no-op when the pending entry is absent, so the index is not
1435-
// re-created for a payment the graduation path already removed. (A graduated
1436-
// payment always has a payment-store record, so it cannot take the `Inserted`
1437-
// branch above and be re-indexed.)
1438-
self.pending_payment_store.update(pending_update).await?;
1439-
},
1421+
self.payment_store.update_or_insert(update, details.clone()).await?;
1422+
1423+
// The pending index must exist exactly while the authoritative record is Pending:
1424+
// graduation and rebroadcast read it, and a graduated payment must not be re-indexed.
1425+
// Deciding by the post-write status rather than by whether the write inserted also
1426+
// repairs a missing index — a crash or failed write between the two stores leaves a
1427+
// Pending record with no entry, and a merge alone would never recreate it, leaving the
1428+
// payment unable to graduate and its txids unmapped.
1429+
let recorded = self.payment_store.get(&details.id).unwrap_or(details);
1430+
if recorded.status == PaymentStatus::Pending {
1431+
// Wallet sync can still land between the payment-store write above and this one and
1432+
// mirror an advanced confirmation into the pending store, so this write makes the
1433+
// same atomic decision: merge narrowly into an entry that appeared, insert otherwise.
1434+
// The inserted entry embeds the post-write record rather than the fresh details, so a
1435+
// confirmation wallet sync already recorded keeps driving graduation.
1436+
let pending = PendingPaymentDetails::new(recorded, Vec::new(), candidates);
1437+
self.pending_payment_store.update_or_insert(pending_update, pending).await?;
1438+
} else {
1439+
// The payment already advanced beyond Pending: the graduation path removed the
1440+
// entry, and `update`'s no-op on absence must not re-create it.
1441+
self.pending_payment_store.update(pending_update).await?;
14401442
}
14411443
Ok(())
14421444
}

0 commit comments

Comments
 (0)