Problem
ldk-node exposes user-facing events through EventQueue in src/event.rs. The queue is separate from LDK's own event persistence and acknowledgement. LDK events are at-least-once, so ldk-node may see the same underlying LDK event again after restart if that LDK event was not durably recorded as handled.
EventQueue::add_event currently:
- Locks the in-memory
VecDeque<Event>.
- Pushes the new
Event with push_back.
- Serializes the entire queue with
EventQueueSerWrapper.
- Persists the queue with
KVStore::write under the event queue persistence key.
There is no stable app-event ID, no event fingerprint, and no queue-level deduplication. Node::next_event and Node::next_event_async return the front of this queue, and Node::event_handled removes the front event and persists the shorter queue. That API makes delivery persistent until the application acknowledges an event, but it does not protect against duplicate events that were appended because LDK replayed the underlying event.
This is not specific to payments. Any user-facing event derived from a replayed LDK event can be appended again if the ldk-node event was already persisted but the underlying LDK event was not yet durably recorded as handled.
Payment events do carry useful dedupe material. Current PaymentSuccessful, PaymentFailed, and PaymentReceived events are emitted with payment_id: Some(...), and they also carry payment hashes where available. That means applications that already treat payment events as idempotent by payment ID or payment hash can avoid the worst outcomes. The remaining gap is that ldk-node can still deliver duplicate app events, the API does not expose a stable event ID, and the idempotency contract is implicit rather than documented or enforced by the queue. There are also compatibility and edge-case wrinkles: older serialized events can have payment_id: None, PaymentFailed can have payment_hash: None for some BOLT12 failures, and current-main inbound PaymentReceived uses a hash-derived PaymentId rather than the LDK event payment_id.
Consequence
Applications may receive duplicate user-facing events. Payment events include identifiers that let careful applications dedupe most payment-side effects, so the channel-fund-safety risk is not the concern here. The risk is API-level ambiguity: applications that assume exactly-once event delivery may create duplicate accounting entries, notifications, fulfillment actions, or operational work. For non-payment events, the consequence depends on what the application does in response and whether the event exposes enough stable fields to dedupe reliably.
Possible Fix
ldk-node could expose stable event IDs, deduplicate queued events using an event-specific fingerprint, or document and enforce an app-facing idempotency contract. If the queue can recognize that a replayed LDK event would produce the same user-facing event again, it can avoid appending a duplicate PaymentSuccessful, PaymentFailed, or PaymentReceived event. That would address the duplicate outbound and inbound payment-event cases without changing LDK's at-least-once event delivery model.
Problem
ldk-node exposes user-facing events through
EventQueueinsrc/event.rs. The queue is separate from LDK's own event persistence and acknowledgement. LDK events are at-least-once, so ldk-node may see the same underlying LDK event again after restart if that LDK event was not durably recorded as handled.EventQueue::add_eventcurrently:VecDeque<Event>.Eventwithpush_back.EventQueueSerWrapper.KVStore::writeunder the event queue persistence key.There is no stable app-event ID, no event fingerprint, and no queue-level deduplication.
Node::next_eventandNode::next_event_asyncreturn the front of this queue, andNode::event_handledremoves the front event and persists the shorter queue. That API makes delivery persistent until the application acknowledges an event, but it does not protect against duplicate events that were appended because LDK replayed the underlying event.This is not specific to payments. Any user-facing event derived from a replayed LDK event can be appended again if the ldk-node event was already persisted but the underlying LDK event was not yet durably recorded as handled.
Payment events do carry useful dedupe material. Current
PaymentSuccessful,PaymentFailed, andPaymentReceivedevents are emitted withpayment_id: Some(...), and they also carry payment hashes where available. That means applications that already treat payment events as idempotent by payment ID or payment hash can avoid the worst outcomes. The remaining gap is that ldk-node can still deliver duplicate app events, the API does not expose a stable event ID, and the idempotency contract is implicit rather than documented or enforced by the queue. There are also compatibility and edge-case wrinkles: older serialized events can havepayment_id: None,PaymentFailedcan havepayment_hash: Nonefor some BOLT12 failures, and current-main inboundPaymentReceiveduses a hash-derivedPaymentIdrather than the LDK eventpayment_id.Consequence
Applications may receive duplicate user-facing events. Payment events include identifiers that let careful applications dedupe most payment-side effects, so the channel-fund-safety risk is not the concern here. The risk is API-level ambiguity: applications that assume exactly-once event delivery may create duplicate accounting entries, notifications, fulfillment actions, or operational work. For non-payment events, the consequence depends on what the application does in response and whether the event exposes enough stable fields to dedupe reliably.
Possible Fix
ldk-node could expose stable event IDs, deduplicate queued events using an event-specific fingerprint, or document and enforce an app-facing idempotency contract. If the queue can recognize that a replayed LDK event would produce the same user-facing event again, it can avoid appending a duplicate
PaymentSuccessful,PaymentFailed, orPaymentReceivedevent. That would address the duplicate outbound and inbound payment-event cases without changing LDK's at-least-once event delivery model.