镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Revisit ldk-node's persistence architecture #1003

Open

Description

@joostjager

During the July 23 developer call, we discussed that persistence in and around ldk-node has become difficult to reason about. State is managed across LDK, ldk-node, and BDK, while storage implementations and persistence abstractions are split across multiple layers.

Even when these components use the same underlying store, related updates are generally written independently rather than committed atomically. A crash or persistence error can therefore leave protocol state, payment records, wallet state, and user-facing events out of sync. Replay and startup reconciliation repair some cases, but can also produce missing records, stale state, or duplicate events.

Addressing these problems individually is adding more special-case recovery logic without giving us a clear overall persistence contract.

We would like to collect ideas for simplifying this architecture and making its consistency and recovery guarantees easier to understand. Please share possible approaches, tradeoffs, or relevant experience with local, remote, and custom storage backends.

Activity

  1. joostjager commented on Jul 24, 2026

    @joostjager
    ContributorAuthor

    I think we need atomic transactions, as proposed in #970. Ideally LDK would let us commit its writes together with the related ldk-node state.

    Otherwise, ldk-node could buffer the writes made through the store passed to LDK, then commit them atomically with its own writes before completing LDK's pending futures. This would only provide stronger guarantees for backends that support atomic transactions.

  2. MaxFangX commented on Jul 28, 2026

    @MaxFangX
    Contributor

    Maybe you can find some inspiration from Lexe's PaymentsManager: https://github.com/lexe-app/lexe-public/blob/master/lexe-ln/src/payments/manager.rs, which owns idempotency and consistency, and is safe across crashes, restarts, etc.

    It's not perfect, as to reason about it you still need to remember how the background processor drives the event handler which then calls into the PaymentsManager. But once you get into the PaymentsManager, we have independent state machines for every payment flow that Lexe supports, and each state machine is easily audited. For example: our state machine for onchain sends: https://github.com/lexe-app/lexe-public/blob/7b00c7ba01890f98322e846c6f82807e24d6f0ce/lexe-ln/src/payments/onchain.rs#L32-L104

    We don't flatten all fields into a single struct for persistence; instead, we use an enum Payment which contains variants for each of our flows. This lets us remove Option in cases where the field always exists for a particular type of payment.

  3. joostjager commented on Jul 28, 2026

    @joostjager
    ContributorAuthor

    Thanks for the pointer. I think encapsulating the payment logic and defining strict states could indeed be helpful in ldk-node too.

    That said, when zooming out, payments are intrinsically part of Lightning. To me, much of the complexity still stems from the persistence boundary between ldk and ldk-node, combined with the lack of atomic batch writes.

  4. TheBlueMatt commented on Jul 28, 2026

    @TheBlueMatt
    Contributor

    Irrespective of what we can blame for the issues, I think the next step here is to brainstorm what the answer should be - ignoring whether ldk-node and LDK are separate, what kind of (outbound) payment store would we design in a monolithic app? Its not entirely clear to me what the answer to that is. A few ideas:

    • move the concept of a pending payment outside of ldk-node entirely - don't worry about persisting an outbound payments until they complete/fail, providing storage for any additional metadata related to them as a part of lightning. ldk-node APIs which list payments would have to query ChannelManager::list_recent_payments before returning to merge the lists. Gets a bit complicated with pagination, but nothing too wild. Basically, this means we split responsibility for outbound payments between "still in channels, query the channel logic to figure out what payments are there" and "not in channels anymore, at rest in the DB".
    • add an outbound payment store API which both receives updates (ala current Events or the Lexe state machine update methods) but also can be queried so LDK is responsible for replaying any missing payments. We could flip which write we require first from the payment store to LDK if we're willing to store payment metadata in lightning. If we did want to go in the direction of batched writes this would ultimately be the first step, just dropping the replay logic in favor of a write batch would require having some interface where we can pass around write batches and fill them up with montior + payment data across subsystems.

    I think we should scope this specifically to the outbound payment store as we don't have anywhere close to the same kind of issues on the inbound side (there's no replay to handle - you just process events and if you dont finish before a crash the event gets replayed). Excluding things that are just in-progress and unrelated to the specific architectural question, the only linked issues that are relevant are: #171, parts of #966 and #969. Most of the linked issues seem to point to totally unrelated things.

  5. joostjager commented on Jul 29, 2026

    @joostjager
    ContributorAuthor

    Agreed that we should think about this as if we were designing a monolithic app. But I'm not sure if your proposed options follow that line. They are framed mostly around moving things between lightning crate, ldk, ldk-node or entirely outside ldk-node?

    I think the first question to answer is whether we'd atomically commit channel state and payment state together, and go from there.

    Regarding the linked issues: I'd say that for example also #968 and #970 are not 'totally unrelated'. I thought a list of directly or indirectly related issues can be useful for context, but I removed the list now, because the level of relevance isn't what we should be discussing.

  6. joostjager commented on Jul 31, 2026

    @joostjager
    ContributorAuthor

    We discussed this issue in the team meeting. For the short term, the pragmatic direction is to avoid changing LDK and continue making ldk-node’s persistence handling idempotent, with reconciliation where the persisted views can diverge.

    I do want to re-highlight the StagingStore option discussed in #970 as a possible intermediate step. It would not require an LDK change, but could allow ldk-node to buffer writes made through the store passed to LDK and commit them atomically with related ldk-node state when the backing store supports atomic batches.

  7. joostjager commented on Sep 3, 2026

    @joostjager
    ContributorAuthor

    Following up on the question here of whether outbound payment state should be committed atomically with channel state, I put together a concrete cross-repository sketch and described it in the one-shot persistence issue.

    In ldk-node, an operation such as send_payment holds an internal guard while the ChannelManager and payment store are updated, then waits for the background processor to commit those writes together with the manager and monitor state. Corresponding messages and events remain gated until the commit succeeds. Transaction coordination stays in the store and background processor, without passing transaction objects through the call graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions