Skip to content

Watchtower support: expose channel monitor data for external watchtower implementations #813

Open

Description

@FreeOnlineUser

Summary

We've implemented an LDK-to-LND watchtower bridge that lets ldk-node mobile clients use existing LND watchtower infrastructure for channel protection. This is running in production on Android (GrapheneOS) with verified blob pushes to a live LND tower.

We'd like to upstream the ldk-node side of this, which is minimal and opt-in.

Problem

Mobile Lightning nodes are intermittently offline. LND has mature watchtower infrastructure deployed on thousands of home nodes (Umbrel, Start9, RaspiBlitz), but there's no way for ldk-node clients to use it.

What we built

In ldk-node (proposed upstream, ~300 lines):

  • WatchtowerPersister: wraps MonitorUpdatingPersister, intercepts persist_new_channel and update_persisted_channel callbacks to capture commitment data in real time
  • 3 new methods on Node:
    • watchtower_drain_justice_blobs() -- returns pending commitment data, clears queue
    • watchtower_set_sweep_address(bytes) -- sets the address for justice tx outputs
    • watchtower_list_monitors() -- lists monitored channel points
  • UniFFI bindings (Kotlin/Swift)
  • Zero behavior change when not used. Wrapper delegates all existing Persist calls untouched

Changes are 2 files + 1 new file:

  • src/watchtower.rs (new) -- WatchtowerPersister + types
  • src/types.rs -- type alias update (2 lines)
  • src/builder.rs -- wrap persister (3 lines)

Separate crate (not proposed for upstream):

  • ldk-watchtower-client: LND wire protocol (Brontide/Noise_XK with secp256k1), justice blob encryption, embedded Tor via Arti for direct .onion connections

Proven in production

  • Bitcoin Pocket Node: Android app running full bitcoind + ldk-node
  • Justice blobs successfully pushed to live LND 0.20.0-beta tower on Umbrel
  • Tor connection verified on Pixel 9 (GrapheneOS) -- no SSH tunnel, no Orbot
  • Custom Brontide implementation verified against BOLT 8 test vectors

Questions for maintainers

  1. Is this the right abstraction level? We expose raw commitment data rather than implementing a specific watchtower protocol in ldk-node itself
  2. Should WatchtowerPersister be opt-in via a builder flag, or always-on with zero overhead when unused?
  3. Any concerns about the wrapper pattern around MonitorUpdatingPersister?

Happy to adjust the approach based on feedback. Fork with implementation: FreeOnlineUser/ldk-node (6 commits, all tests pass)

Activity

  1. TheBlueMatt commented on Mar 1, 2026

    @TheBlueMatt
    Contributor

    Cool!

    I'm not 100% sure if we want to add support for the LND-proprietary watchtower model (at a minimum there are better ways to do watchtowers - its weird that the LND watchtower model both breaks your privacy and doesn't enforce HTLCs, if its not gonna enforce HTLCs it really should offer privacy!), but we definitely want to improve the watchtower API in LDK so that most of your code can go upstream and you can have a much, much, much simpler patchset to maintain.

    As you discovered, using the ChannelMonitor "watchtower" API from lightningdevkit/rust-lightning#2337 requires tracking a bunch of state outside the ChannelMonitor and firuging out how to track (and persist!) that state.

    There was a followup attempted at lightningdevkit/rust-lightning#2552 to clean up that API so that that tracking can be done inside the ChannelMonitor and fetching the justice data is a simple call into the updated ChannelMonitor but sadly the original author moved on to other projects before finishing it. Do you have any interest in picking that back up?

  2. FreeOnlineUser commented on Mar 1, 2026

    @FreeOnlineUser
    Author

    Thanks Matt. Appreciate the detailed response.

    Agreed on the LND tower model's privacy trade-offs. The reason I went with it is purely pragmatic: it's what's deployed on most home nodes today, and the target user for this project already has an Umbrel/Start9 running LND. Meeting users where their infrastructure is.

    I'll review #2552 and see where it left off. The external state tracking was the most painful part of the implementation, so having that handled inside ChannelMonitor would be a big improvement. I've been through the persist callbacks and the signing flow enough to have opinions on where the abstractions should sit.

    I should be upfront that I'm currently unfunded on this project, so my bandwidth is limited. But I'm definitely interested in contributing what I can toward cleaning up the watchtower API. Happy to share what I learned from building the bridge if that's useful context for the design.

  3. FreeOnlineUser commented on Mar 1, 2026

    @FreeOnlineUser
    Author

    Reviewed #2552 and the review thread. The PR takes an external utility approach (JusticeTxTracker alongside the Persist impl), but your feedback suggested moving the state tracking inside ChannelMonitor itself so extracting justice data becomes a single call with no external storage. That aligns with my experience: the external state tracking was the most complex part of my implementation. I'll look at what it would take to store the latest counterparty commitment inside ChannelMonitor and expose a method to extract signed justice txs directly.

  4. FreeOnlineUser commented on Mar 1, 2026

    @FreeOnlineUser
    Author

    Following up on @TheBlueMatt's suggestion to move justice tx state tracking inside ChannelMonitor rather than using an external utility.

    I've prototyped this approach against current rust-lightning main. Draft PR here: lightningdevkit/rust-lightning#4453

    Short version: adds ChannelMonitor::get_justice_txs(feerate_per_kw, destination_script) returning signed justice transactions for all revoked counterparty commitments in a single call. No external queue or state tracking needed. The test WatchtowerPersister drops from ~80 lines of queue management to ~10.

    A few design questions in the PR description where I'd appreciate input before finalizing.

  5. tnull commented on Mar 2, 2026

    @tnull
    Collaborator

    Cool!

    I'm not 100% sure if we want to add support for the LND-proprietary watchtower model (at a minimum there are better ways to do watchtowers - its weird that the LND watchtower model both breaks your privacy and doesn't enforce HTLCs, if its not gonna enforce HTLCs it really should offer privacy!), but we definitely want to improve the watchtower API in LDK so that most of your code can go upstream and you can have a much, much, much simpler patchset to maintain.

    As you discovered, using the ChannelMonitor "watchtower" API from lightningdevkit/rust-lightning#2337 requires tracking a bunch of state outside the ChannelMonitor and firuging out how to track (and persist!) that state.

    There was a followup attempted at lightningdevkit/rust-lightning#2552 to clean up that API so that that tracking can be done inside the ChannelMonitor and fetching the justice data is a simple call into the updated ChannelMonitor but sadly the original author moved on to other projects before finishing it. Do you have any interest in picking that back up?

    I agree here - thank you for the interest, watchtowers are definitely something we want to support (finally).

    However, @FreeOnlineUser if you intend to pick some of this up, please make sure to coordinate with @enigbe who also intends to work on finishing watchtower support for LDK/LDK Node in the coming months.

  6. FreeOnlineUser commented on Mar 2, 2026

    @FreeOnlineUser
    Author

    Thanks @tnull. I put up a draft at lightningdevkit/rust-lightning#4453 as a starting point, but happy to defer to @enigbe on direction here. My watchtower integration already works via an ldk-node fork, so this is more about reducing the downstream patchset than anything urgent. If the approach or API needs to change, no problem. Happy to help however's useful.

    Really enjoying building on ldk-node, great work.

  7. 1 remaining item

  8. kaloudis commented on Sep 25, 2026

    @kaloudis
    Contributor

    Zeus (mobile, embedded ldk-node) is interested in this too. Our users are offline most of the time, so justice tx coverage is the main thing we lack compared to our LND-backed mode.

    Right now ChainMonitor in src/types.rs is fixed to MonitorUpdatingPersister, so an app can't wrap the persister without forking. Even before a tower protocol is chosen, would you take a small change that lets the builder accept a Persist wrapper, or some other hook that exposes counterparty commitment data from each monitor update?

    I saw rust-lightning#4453 was closed. @enigbe, is there a current branch or design for the LDK-side API that we could test against or help with?

  9. FreeOnlineUser commented on Sep 26, 2026

    @FreeOnlineUser
    Author

    #4453 isn't dead, it moved. rust-lightning PR development went to the project's self-hosted forge in July and the GitHub PRs were auto-closed in the migration (GitHub is a mirror now). It's live at https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/pulls/4453 with review ongoing: the monitor retains revoked counterparty commitments internally and produces signed justice transactions on demand, and ChainMonitor takes a WatchtowerPersist (sync or async) that gets offered the pending set after each revocation, crash-safe across restarts. All review rounds to date are addressed and it's queued for another maintainer pass.

    The ldk-node side is then mostly builder plumbing to register a persister; I run a working version of that in my fork today. Once the rust-lightning API settles I plan to bring that up here as a follow-up.

  10. enigbe commented on Sep 28, 2026

    @enigbe
    Contributor

    @kaloudis: I see @FreeOnlineUser, who has been driving all the work on this, has already responded.

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