This issue tracks crash and replay boundaries between LDK's persisted state and ldk-node's app-level payment records. It focuses on Lightning payments from both the sending and receiving node perspectives.
LDK persists ChannelManager, ChannelMonitors, and replayable LDK events. ldk-node separately persists PaymentDetails in PaymentStore and user-facing events in EventQueue. Because these are independent persistence streams, a crash or store error can leave the two views temporarily or permanently out of sync unless the event handling path or startup logic reconciles them.
Current main basis: 73be359ec5e8cddfc2463b8ed04bf2de038cdcd1.
LDK events are at-least-once: until the LDK event source durably records an event as handled, ldk-node may receive the same event again after restart.
Outbound
| ID |
Scenario |
Consequence |
Status |
Issue |
| O1 |
LDK accepts or starts outbound payment, crash before PaymentStore::insert(Pending) |
Payment may be in flight but absent from ldk-node APIs. |
GAP |
|
| O2 |
PaymentStore::insert(Pending) succeeds, crash before LDK manager or monitor persistence |
ldk-node may show a stuck pending payment that LDK no longer knows. |
GAP |
|
| O3 |
Restart sees LDK recent payment but no ldk-node record |
No automatic backfill into PaymentStore. |
GAP |
#171 |
| O4 |
Restart sees ldk-node Pending, but LDK no longer has the payment |
Duplicate protection may block retry incorrectly. |
GAP |
#171 |
| O5 |
PaymentSent or PaymentFailed update returns NotFound |
Success or failure event may be queued, but payment record stays missing. |
GAP |
|
| O6 |
PaymentSent or PaymentFailed update has real persistence error |
LDK event is replayed later. |
SAFE |
|
| O7 |
Payment update succeeds, app event queue write fails |
Event is replayed, payment update is idempotent. |
SAFE |
|
| O8 |
App event queued, crash before LDK event source durably records the LDK event as handled |
User may see duplicate success or failure events on replay. |
GAP |
|
| O9 |
Missing completed payment record weakens duplicate protection after LDK stops tracking it |
Same invoice or payment hash could be paid again. |
GAP |
|
Inbound
| ID |
Scenario |
Consequence |
Status |
Issue |
| I1 |
BOLT11 invoice created, crash before ldk-node payment record write, invoice never paid |
Invoice may remain payable but invisible to ldk-node. |
GAP |
#425, #51 |
| I2 |
BOLT11 auto-claim invoice record missing, later PaymentClaimable arrives |
Current main claims without backfilling a durable payment record; #948 backfills before claim handling. |
#948 |
#298 |
| I3 |
BOLT11 auto-claim record missing, later PaymentClaimed arrives without prior app-visible claimable event |
Current main queues a receive event without payment history; #948 reconstructs the claimed record. |
#948 |
#298 |
| I4 |
Manual-claim BOLT11 pending registration exists, promoted on PaymentClaimable |
User can still get a claimable event after restart if the pending record is durable. |
SAFE |
#280 |
| I5 |
Manual-claim BOLT11 pending registration lost before durable write |
HTLC may not be surfaced for manual claim. |
GAP |
#425, #51, #280 |
| I6 |
BOLT12 or spontaneous inbound PaymentClaimable insert fails |
Debug builds panic at debug_assert!(false); non-debug builds log but do not request LDK replay, so funds may be claimed without a durable payment record. |
#948 |
#298, #381 |
| I7 |
PaymentClaimed update returns NotFound, current code only logs and still queues event |
User sees receive event, but payment history is missing. |
#948 |
#298 |
| I8 |
PaymentClaimed update has real persistence error |
LDK event is replayed later. |
SAFE |
|
| I9 |
Manual PaymentClaimable app event queue write fails before claim |
LDK event is replayed, user is prompted later. |
SAFE |
|
| I10 |
PaymentReceived app event queued, crash before LDK event source durably records the LDK event as handled |
User may see duplicate receive events on replay. |
GAP |
|
| I11 |
Repeated inbound payments sharing payment hash need LDK event payment_id tracking |
Distinct receives can overwrite or confuse each other. |
#948 |
#298, #472 |
| I12 |
Canceled invoice enforcement depends on durable payment record, lost record cannot enforce cancel |
Current main has no cancel-invoice API in src/payment, so this is not an executable current-main scenario. |
GAP |
|
| I13 |
Restart/replay should reconstruct missing inbound payment records from replayed LDK payment events |
Current main has no eager startup reconciliation; missing records only surface when PaymentClaimable or PaymentClaimed is replayed. |
GAP |
#298 |
Cross-Cutting
| ID |
Scenario |
Consequence |
Status |
Issue |
| C1 |
Startup restores PaymentStore by listing persisted keys, so stores that return only one page of keys require pagination to load every payment record. |
Records beyond the first page can remain persisted but absent from the in-memory PaymentStore after startup. |
#960 |
|
| C2 |
Payment store update must not mutate in-memory state before durable write, otherwise replay can become falsely idempotent |
Failed writes could be acknowledged on replay without being durable. |
SAFE |
|
| C3 |
LDK events are at-least-once, so ldk-node app events need dedupe or event IDs to avoid duplicate user events |
Apps may process duplicate payment events. |
GAP |
|
Notes
The largest unaddressed area is outbound reconciliation. LDK exposes ChannelManager::list_recent_payments() for crash recovery, but current ldk-node startup does not appear to compare that state with PaymentStore.
The second unaddressed area is user-facing event deduplication. Because LDK events are at-least-once, an app event can be queued before the LDK event source durably records the underlying LDK event as handled. If the node restarts in that window, the same LDK event can be replayed and queued again.
This issue tracks crash and replay boundaries between LDK's persisted state and ldk-node's app-level payment records. It focuses on Lightning payments from both the sending and receiving node perspectives.
LDK persists
ChannelManager,ChannelMonitors, and replayable LDK events. ldk-node separately persistsPaymentDetailsinPaymentStoreand user-facing events inEventQueue. Because these are independent persistence streams, a crash or store error can leave the two views temporarily or permanently out of sync unless the event handling path or startup logic reconciles them.Current main basis:
73be359ec5e8cddfc2463b8ed04bf2de038cdcd1.LDK events are at-least-once: until the LDK event source durably records an event as handled, ldk-node may receive the same event again after restart.
Outbound
PaymentStore::insert(Pending)PaymentStore::insert(Pending)succeeds, crash before LDK manager or monitor persistencePaymentStore.Pending, but LDK no longer has the paymentPaymentSentorPaymentFailedupdate returnsNotFoundPaymentSentorPaymentFailedupdate has real persistence errorInbound
PaymentClaimablearrivesPaymentClaimedarrives without prior app-visible claimable eventPaymentClaimablePaymentClaimableinsert failsdebug_assert!(false); non-debug builds log but do not request LDK replay, so funds may be claimed without a durable payment record.PaymentClaimedupdate returnsNotFound, current code only logs and still queues eventPaymentClaimedupdate has real persistence errorPaymentClaimableapp event queue write fails before claimPaymentReceivedapp event queued, crash before LDK event source durably records the LDK event as handledpayment_idtrackingsrc/payment, so this is not an executable current-main scenario.PaymentClaimableorPaymentClaimedis replayed.Cross-Cutting
PaymentStoreby listing persisted keys, so stores that return only one page of keys require pagination to load every payment record.PaymentStoreafter startup.Notes
The largest unaddressed area is outbound reconciliation. LDK exposes
ChannelManager::list_recent_payments()for crash recovery, but current ldk-node startup does not appear to compare that state withPaymentStore.The second unaddressed area is user-facing event deduplication. Because LDK events are at-least-once, an app event can be queued before the LDK event source durably records the underlying LDK event as handled. If the node restarts in that window, the same LDK event can be replayed and queued again.