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
- Is this the right abstraction level? We expose raw commitment data rather than implementing a specific watchtower protocol in ldk-node itself
- Should
WatchtowerPersister be opt-in via a builder flag, or always-on with zero overhead when unused?
- 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)
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: wrapsMonitorUpdatingPersister, interceptspersist_new_channelandupdate_persisted_channelcallbacks to capture commitment data in real timeNode:watchtower_drain_justice_blobs()-- returns pending commitment data, clears queuewatchtower_set_sweep_address(bytes)-- sets the address for justice tx outputswatchtower_list_monitors()-- lists monitored channel pointsPersistcalls untouchedChanges are 2 files + 1 new file:
src/watchtower.rs(new) -- WatchtowerPersister + typessrc/types.rs-- type alias update (2 lines)src/builder.rs-- wrap persister (3 lines)Separate crate (not proposed for upstream):
Proven in production
Questions for maintainers
WatchtowerPersisterbe opt-in via a builder flag, or always-on with zero overhead when unused?MonitorUpdatingPersister?Happy to adjust the approach based on feedback. Fork with implementation: FreeOnlineUser/ldk-node (6 commits, all tests pass)