This document records the architectural assessment and upstream status for
in-process Lightning integration in bitcoin-rs, covering issue #1631
(ENG-83 and ENG-96).
In-process integration divides into two distinct phases, each blocked on an independent upstream component:
| Phase | Scope | Upstream blocker |
|---|---|---|
Phase A (ENG-83) |
In-process LDK Server daemon hosting | lightningdevkit/ldk-server at c17dc11 |
Phase B (ENG-96) |
Direct in-memory LDK Node chain backend | lightningdevkit/ldk-node at 179b4b2 and v0.7.1 (peeled commit edc8916, annotated tag 3534a3d) |
The existing tools/ldk adapter was authored for
issue #815. It proves that
the production Node embedding contract (crates/node/src/embed.rs,
docs/contracts/embedding.md) can satisfy the
low-level trait requirements of rust-lightning. Those traits are
BroadcasterInterface, FeeEstimator, and BlockSource from
lightning = "=0.2.7" and lightning-block-sync = "=0.2.0".
tools/ldk lives in an independent Cargo workspace with its own Cargo.lock,
keeping LDK dependencies entirely out of the authoritative bitcoin-rs workspace
dependency graph and default CI.
tools/ldk serves strictly as an L1 adapter proof:
- It does not manage Lightning channels, peer sessions, or
ChannelMonitorstate. - It does not host an LDK Server daemon, gRPC service, or macaroon store.
- Its pinned
lightning = "=0.2.7"dependency from crates.io cannot be assumed type-compatible with upstreamldk-nodeorldk-servergit revisions.
External black-box verification of Lightning implementations (LDK Server,
LND, Core Lightning, Lampo, Eclair) over JSON-RPC, REST, and ZMQ belongs
exclusively to issue #1488.
That issue owns the external verification evidence to be recorded in
docs/lightning-interop-matrix.md and exercised via scripts/verify-lightning-interop.sh.
Pin and provenance qualification of the external binaries runs without starting
any node (--verify-pins-only). That route and the runner's exit-status
contract are specified in
docs/lightning-interop-matrix.md section 4.
Issue #1631 addresses only in-process, native Rust embedding.
| Field | Value |
|---|---|
| Repository | lightningdevkit/ldk-server |
| Pinned revision | Commit c17dc11dde38f88c53457039ddf30c6b90a63ba2 |
| Upstream status | BLOCKED |
The manifest at ldk-server/Cargo.toml
defines a standalone binary crate. It contains no [lib] section, and the source
tree contains src/main.rs but no src/lib.rs.
The repository root manifest at Cargo.toml
defines workspace members ["ldk-server-cli", "ldk-server-client", "ldk-server-grpc", "ldk-server", "ldk-server-mcp", "ldk-server-macaroons"].
None of these provides an embeddable server library capable of driving the LDK
Server daemon lifecycle in-process.
The absence of a [lib] section is only a surface obstacle. Even if a
library target were declared, the codebase lacks any public serving API.
NodeService is private. Defined in ldk-server/src/service.rs,
NodeService, its constructor NodeService::new, and its inner Context
(which holds Arc<Node>) are declared pub(crate). Neither can be constructed
or accessed outside the ldk-server binary crate.
In ldk-server/src/main.rs,
the complete runtime, networking, and event lifecycle is hardcoded within private
functions:
- Node construction is private:
fn build_node(...) -> Result<Node, BuildError>(main.rs). - Node startup is called directly inside
main():node.start()(main.rs). - Service instantiation happens exclusively inside the private socket accept loop:
NodeService::new(...)(main.rs). - The serving loop is a private inline Tokio event loop (
main.rs) drivinggrpc_listener.accept()on a localTcpListener(main.rs). It performs the TLS handshake in line viaacceptor.accept(stream)(main.rs) and serves HTTP/2 viahttp2::Builder::new(TokioExecutor::new()).serve_connection(io_stream, node_service)(main.rs). LDK events are polled viaevent_node.next_event_async()(main.rs). Events are processed internally and emitted via a private broadcast channel (event_sender: broadcast::Sender<EventEnvelope>atservice.rs). No public API lets external consumers subscribe to, poll, or process these events directly. - Shutdown handling is coupled to system signals (
SIGTERM/Ctrl-C), systemd notification (systemd::notify_stopping()), and immediatenode.stop()(main.rs). There is no public interface for an orderly shutdown sequence that stops incoming connections, drains active gRPC calls and event dispatch queues, and then closes the node safely.
There is no public type or function that accepts an externally managed
Arc<ldk_node::Node> and provides programmatic control over the gRPC listener,
TLS configuration, macaroon authentication, event polling and dispatch, and
orderly shutdown sequence.
The tracking issue is lightningdevkit/ldk-server#310
("Expose LDK Server as an embeddable library for bitcoin-rs in-process integration"),
opened 2026-10-10 by @gosunuts. Its status is open.
Maintainers questioned the direction, proposing instead that bitcoin-rs
be imported into ldk-server behind a feature flag. As of 2026-10-24, the
inspected issue does not establish an accepted interface or design agreement
for in-process embedding.
Phase A is blocked on an accepted upstream ldk-server serving interface that:
- Exposes an embeddable library interface accepting an externally managed
Arc<ldk_node::Node>. - Provides programmatic control over gRPC socket binding, TLS configuration, macaroon storage, and event dispatch.
- Exposes public event subscription and an orderly shutdown sequence that drains active gRPC requests and events before node teardown.
- Avoids taking ownership of OS signal handling, systemd notifications, or the caller's Tokio runtime.
Maintaining a downstream fork of ldk-server, copying its private gRPC handlers
into bitcoin-rs, or substituting a loopback HTTP probe is explicitly rejected.
| Field | Value |
|---|---|
| Repository | lightningdevkit/ldk-node |
| Pinned commit | Commit 179b4b263762155a7ff515ddad7e757a80807fd5 |
| Pinned release tag | Tag v0.7.1 (peeled commit edc8916e289d39a4135b6eda66978a9f765396bb, annotated tag object 3534a3d2072c... via git/refs/tags/v0.7.1) |
| Upstream status | BLOCKED |
The builder in ldk-node exposes only concrete, remote chain-source setters, with signatures that differ across the two baselines.
At tag v0.7.1 (peeled commit edc8916e, src/builder.rs), set_chain_source_bitcoind_rpc is a four-parameter setter:
pub fn set_chain_source_esplora(&mut self, server_url: String, sync_config: Option<EsploraSyncConfig>) -> &mut Self;
pub fn set_chain_source_esplora_with_headers(&mut self, server_url: String, headers: HashMap<String, String>, sync_config: Option<EsploraSyncConfig>) -> &mut Self;
pub fn set_chain_source_electrum(&mut self, server_url: String, sync_config: Option<ElectrumSyncConfig>) -> &mut Self;
pub fn set_chain_source_bitcoind_rpc(&mut self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String) -> &mut Self;
pub fn set_chain_source_bitcoind_rest(&mut self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String) -> &mut Self;At commit 179b4b2 (src/builder.rs), wallet_rescan_from_height: Option<u32> was added, yielding a five-parameter RPC setter (and seven-parameter REST setter):
pub fn set_chain_source_esplora(&mut self, server_url: String, sync_config: Option<EsploraSyncConfig>) -> &mut Self;
pub fn set_chain_source_esplora_with_headers(&mut self, server_url: String, headers: HashMap<String, String>, sync_config: Option<EsploraSyncConfig>) -> &mut Self;
pub fn set_chain_source_electrum(&mut self, server_url: String, sync_config: Option<ElectrumSyncConfig>) -> &mut Self;
pub fn set_chain_source_bitcoind_rpc(&mut self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option<u32>) -> &mut Self;
pub fn set_chain_source_bitcoind_rest(&mut self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option<u32>) -> &mut Self;In both revisions, there is no generic setter, no trait object parameter (Box<dyn ChainSource>), and no custom backend registration hook.
Internal dispatch is mediated by a private enum at 179b4b2
(src/chain/mod.rs)
and v0.7.1 (src/chain/mod.rs):
enum ChainSourceKind {
#[cfg(feature = "chain-esplora")]
Esplora(EsploraChainSource),
#[cfg(feature = "chain-electrum")]
Electrum(ElectrumChainSource),
#[cfg(feature = "chain-bitcoind")]
Bitcoind(BitcoindChainSource),
}ChainSourceKind is private (unadorned enum ChainSourceKind), and contains
only three variants corresponding to external HTTP and TCP network clients.
The internal configuration enum ChainDataSourceConfig (src/builder.rs) is
equally private and closed.
Across both revisions, there are zero extension points for an in-process caller to supply chain services:
| Chain capability | Implementation in ldk-node |
Extension status |
|---|---|---|
| Full-block and wallet sync | sync_onchain_wallet and sync_lightning_wallet (src/chain/mod.rs) dispatch via match &self.kind to concrete clients. Bitcoind mode delegates to internal lightning-block-sync::SpvClient / ChainPoller polling over HTTP REST/RPC (src/chain/bitcoind.rs). |
Closed |
| Fee estimation | OnchainFeeEstimator (src/fee_estimator.rs) is an internal concrete struct, not an open trait. Concrete clients poll remote endpoints and mutate an internal fee rate cache. |
Closed |
| Transaction broadcast | TransactionBroadcaster (src/tx_broadcaster.rs) queues packages and broadcasts directly through concrete clients via bitcoind RPC sendrawtransaction/submitpackage, Esplora HTTP, or Electrum TCP. |
Closed |
| Tip and filter observation | Driven strictly within internal SpvClient and ChainPoller polling loops. External listeners cannot attach to the node native chain feed. |
Closed |
In Rust, type identity is nominal and bound to the specific crate instance and source: types defined across distinct crate sources, git revisions, or disparate builds cannot be unified.
| Target | Declared lightning dependency |
Resolved version | Compatibility with tools/ldk |
|---|---|---|---|
Git commit 179b4b2 |
rev = "d7a980641c6325ddda5072a0f470ef8392676cf5" in ldk-node/Cargo.toml:86-97 |
Resolves to 0.3.0-rc3 in ldk-server lockfile |
Incompatible with crates.io 0.2.7 |
Release tag v0.7.1 |
version = "0.2.0" in ldk-node/Cargo.toml:34 |
SemVer range >= 0.2.0, < 0.3.0 (capable of resolving 0.2.7) |
Nominal identity unproven without compiling consumer proof |
Local tools/ldk |
version = "=0.2.7" in tools/ldk/Cargo.toml:12 |
Exact crates.io 0.2.7 |
Self-contained |
Under Cargo SemVer rules, the declaration version = "0.2.0" in v0.7.1 can resolve to crates.io 0.2.7. However, SemVer compatibility alone does not establish nominal type identity: in Rust, types from two independently resolved crates or different crate sources cannot be unified without a compiling consumer proof. For commit 179b4b2, the explicit git pin resolves to 0.3.0-rc3, which is definitively incompatible with tools/ldk's crates.io 0.2.7. In accordance with plan Step 29, tools/ldk must not be assumed identical to ldk-node's source until an actual compiling consumer proof is established.
Phase B is blocked on an accepted upstream ldk-node custom chain source
interface that:
- Defines an accepted public extension point (such as an open
ChainSourcetrait or builder setter) accepting custom in-process implementations. - Supplies on-chain and Lightning wallet sync, fee estimation, transaction and package broadcast, and tip or filter observation.
- Decouples wallet synchronization from remote REST or RPC polling loops.
- Uses a stable
lightningdependency version compatible with the embedding application without git patch overrides.
Shipping a downstream fork of ldk-node, maintaining patched dependency tables,
or routing requests through a local HTTP loopback server is explicitly rejected.
Every criterion from issue #1631 and the approved plan is mapped below to its exact upstream prerequisite and current verdict:
| Criterion | Target scope | Current status | Blocking upstream prerequisite |
|---|---|---|---|
| Independent compilation | Build with unmodified upstream dependencies | BLOCKED |
Phase A requires ldk-server to export a public library target ([lib]). Phase B requires ldk-node to align on compatible lightning dependency pins without requiring git patch overrides. |
| gRPC, TLS, macaroon, and events lifecycle | Programmatic TLS, macaroon auth, gRPC listener, event polling and dispatch, and coordinated shutdown draining | BLOCKED |
Phase A requires ldk-server to export a public serving interface (NodeService or server runner) accepting an external Arc<ldk_node::Node>, with public event subscription and deterministic shutdown ordering before node teardown. |
| Channel lifecycle | Open, pay, close, and persisted restart | BLOCKED |
Blocked on Phase A (cannot drive LDK Server channels via programmatic gRPC) and Phase B (cannot drive channel monitoring or wallet state via in-process chain feed). |
| Reorg and missed-notification recovery | Positional chain replay without dropped blocks | BLOCKED |
Blocked on Phase B. ldk-node internal sync_lightning_wallet and SpvClient cannot be bound to Node::subscribe_chain / replay_chain. |
| Zero-loopback native chain backend | In-memory block, fee, mempool, and broadcast queries | BLOCKED |
Phase B requires ldk-node to provide a public custom chain-source extension (such as an open ChainSource trait or builder setter) accepting in-process implementations. |
| Matched native versus transport performance | Compare in-process memory calls against RPC loopback | BLOCKED |
Cannot perform comparative benchmark until native Phase B integration exists without loopback network hops. |
A test scaffold or mock server that simulates LDK Server responses over loopback TCP does not constitute an in-process integration. No acceptance item may be marked as passed based on stubbed or simulated components.
Neither tools/ldk-server nor any new LDK dependency may be added to the
authoritative bitcoin-rs workspace. The only permitted LDK consumer remains
the isolated tools/ldk adapter crate for issue #815.
When upstream components eventually provide the required extension points, native integration must comply with:
SYNC-03: In-memory adapter calls must be bounded, non-blocking, and unable to stall node progress.SYNC-05: Adapter queries must honestly reflect consensus and storage state without masking persistent errors.EMB-01throughEMB-04: Embedders must interact solely through the publicNodeinterface, without bypassing module fences or accessing internal storage locks.