Skip to content

Payment Persistence Gap Matrix #966

Description

@joostjager

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions