Repository navigation
loopd: track lnd connection ownership, don't close borrowed conns - #1272
Open
litbot-9000 wants to merge 2 commits into
Open
litbot-9000 wants to merge 2 commits into
litbot-9000 wants to merge 2 commits into
Conversation
When running as a subserver inside lightning-terminal, the loop daemon receives an lnd connection that is created and owned by the parent process and shared among all subservers. On shutdown, both graceful and after a runtime error, the daemon unconditionally closed that connection, tearing down lnd access for LiT itself and all other integrated subservers (pool, faraday, tapd, accounts and the LiT macaroon service). Track the connection ownership like faraday already does: the standalone Start path marks the connection as owned, StartAsSubserver marks it as borrowed and stop only closes owned connections. Add a regression test that starts an embedded daemon against a mock lnd with a real injected grpc.ClientConn and asserts the connection survives both a graceful Stop and a runtime-error shutdown, plus a test that an owned connection is still closed on stop. Refs lightninglabs/lightning-terminal#1381 (comment 6530026672).
litbot-9000
force-pushed
the
fix/lnd-conn-ownership
branch
from
October 30, 2026 19:27
ee36cc5 to
5b570d8
Compare
Strengthen the ownership fix from a runtime flag to type-level prevention: StartAsSubserver now accepts the non-closable *lndclient.LndServices view instead of the *lndclient.GrpcLndServices wrapper, so a subserver cannot call Close on the shared connection even if it tries. The closable wrapper is only held in the new lndOwner field, which is only set when the daemon creates the connection itself (standalone Start and the view command). Add a mechanically enforced guard test that fails CI if any code in the package closes an lnd connection handle outside the whitelisted owner sites, or if StartAsSubserver ever accepts the closable wrapper again. The guard was verified to fail against master (flagging the original d.lnd.Close() bug site and the wrapper-typed signature) and to pass with this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
When the loop daemon runs as an integrated subserver of lightning-terminal (LiT), it receives lnd services backed by a connection that LiT creates once and shares among all its subservers (loop, pool, faraday, tapd) via
StartAsSubserver.On shutdown the daemon unconditionally closed that borrowed connection:
d.stop()(both the gracefulStop()path and the runtime-error path viainternalErrChan) calledd.lnd.Close()on the shared connection.Once that borrowed connection is closed, everything else in the LiT process that uses it fails with
grpc: the client connection is closing— the LiT macaroon service (signrpc.DeriveSharedKey), tapd, faraday and accounts — while the remote lnd stays perfectly healthy. This is the root cause behind the remote-lnd startup failure reported in lightninglabs/lightning-terminal#1381 (comment) (litd 0.17.8 + remote lnd 0.21.4-beta failing 12ms after "Starting LiT macaroon service"), as directed in lightninglabs/lightning-terminal#1329 (comment).Pool had the identical bug, fixed in the sister PR lightninglabs/pool#528. Faraday already tracked connection ownership correctly (
lndOwned), and tapd never receives theGrpcLndServiceswrapper, so neither needed a fix. LiT itself already closes the shared client exactly once inshutdownSubServers(terminal.go), after stopping all subservers, so no lightning-terminal change is needed for the ownership model. The ownership model is now documented (with diagrams) in lightninglabs/lightning-terminal#1458.The fix (two layers)
Layer 1 — ownership tracking (commit 1). Mirror faraday's model:
Start()(standalone) marks the connection as owned,StartAsSubserver()marks it borrowed, andstop()only closes owned connections.Layer 2 — type-level prevention (commit 2). Make borrowed-ness unrepresentable.
StartAsSubservernow accepts the non-closable*lndclient.LndServicesview instead of the*lndclient.GrpcLndServiceswrapper, so a subserver cannot callCloseon the shared connection even if it tries. The closable wrapper is only held in the newlndOwnerfield, which is set only when the daemon creates the connection itself (standaloneStart, and theviewcommand whose local was renamed toownedLndfor greppability). Judge: the type checker.Note for integrators: this changes the exported
StartAsSubserversignature. LiT's call site (subservers/loop.go) must pass&g.lndClient.LndServicesinstead ofg.lndClientwhen bumping the loop dependency.Verification (each layer demonstrated to fail pre-fix)
loopd/daemon_ownership_test.gostarts a real (bufconn, TLS) mock lnd, builds a realGrpcLndServiceswith a real injectedgrpc.ClientConnexactly like LiT does, starts an embedded daemon with the borrowed services, and asserts the injected conn does NOT enterconnectivity.Shutdown— for both a gracefulStop()and a runtime-error stop (internalErrChan).TestOwnedLndConnClosedOnStopasserts an owned connection is still closed (no standalone leak). Both subserver subtests fail on master ("subserver closed the shared lnd connection") and pass with the fix.loopd/ownership_guard_test.gois a static guard (AST scan of the package, stdlib-only) that fails CI if any.Close()targets an lnd connection handle outside the whitelisted owner sites, or ifStartAsSubserverever accepts the closable wrapper again. Demonstrated on master: it flags the original bug site (daemon.go:1173 d.lnd.Close()), the wrapper-typed signature, and theview.goclose.loopdpackage + root package tests pass; golangci-lint clean.Skipped as low-value-after-type-level: a formal TLA+-style spec — the lifecycle has four states and one invariant ("a subserver stopping never closes the shared conn"), which is now enforced by the type checker, the regression tests and the static guard; the state machine is sketched in the litd docs PR instead.
The reconnect/restart work from lightning-terminal#644 is intentionally kept separate and out of scope.
View the OpenClaw team session