lnd.go: sync headers before chain notifier startup to prevent premature rescan
What changed, and why it matters
This patch fixes a startup-order bug in the Lightning Network Daemon (LND). Because a previous change started the chain notifier before the wallet's block headers were fully synced, rescans could begin from an old block height roughly 100,000 blocks behind the current chain tip. The fix moves chain notifier startup to after header synchronization and delays the 'starting beat' block reference until after the notifier is running. The main risk is operational/money-loss: a node that rescans from a stale height can miss recent on-chain events such as channel closes, force-closes, or swept funds, which may lead to incorrect balances or delayed detection of transactions. It is not a remote code execution or direct theft vulnerability.
Nodes running affected LND versions with Neutrino or similar SPV backends should upgrade to a version containing this fix. Operators should verify that rescan heights are current after restart and monitor for missed on-chain events if they previously ran the regressed code. No immediate emergency response is required, but the fix should be included in the next maintenance release.
Security signals we found
Startup-order race between chain notifier and header sync
Rescan from stale block height (~100k blocks behind)
Potential missed on-chain events (channel closes, sweeps, force closes)
Operational integrity / balance accuracy impact
No input validation, authentication, or cryptography change
Evidence from the diff
Commit c6f458e478f9ef2cf1d394972bfbc512862c6707 reorders LND startup so that s.cc.ChainNotifier.Start() happens after wallet/header sync completes and after the RPC server starts, instead of inside newServer(). It also moves getStartingBeat() to after the chain notifier has started, ensuring the block beat used by txPublisher and related subsystems reflects the synced height rather than the height captured during BuildChainControl. The regression was introduced in commit 16a8b623b, which moved chain notifier startup before sync. The patch removes the startLowLevelServices() helper and inlines the notifier start into the main Start() goroutine, with cleanup registration for Stop().
Changed components
lnd/server.goChainNotifier startupBestBlockTracker startuptxPublisher startupNeutrino/wallet rescan logicInspect captured patch +12 / −40
diff --git a/server.go b/server.go
index a2d36eb..6d0b28f 100644
--- a/server.go
+++ b/server.go
@@ -733,17 +733,6 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
quit: make(chan struct{}),
}
- // Start the low-level services once they are initialized.
- //
- // TODO(yy): break the server startup into four steps,
- // 1. init the low-level services.
- // 2. start the low-level services.
- // 3. init the high-level services.
- // 4. start the high-level services.
- if err := s.startLowLevelServices(); err != nil {
- return nil, err
- }
-
currentHash, currentHeight, err := s.cc.ChainIO.GetBestBlock()
if err != nil {
return nil, err
@@ -2125,41 +2114,12 @@ func (c cleaner) run() {
}
}
-// startLowLevelServices starts the low-level services of the server. These
-// services must be started successfully before running the main server. The
-// services are,
-// 1. the chain notifier.
-//
-// TODO(yy): identify and add more low-level services here.
-func (s *server) startLowLevelServices() error {
- var startErr error
-
- cleanup := cleaner{}
-
- cleanup = cleanup.add(s.cc.ChainNotifier.Stop)
- if err := s.cc.ChainNotifier.Start(); err != nil {
- startErr = err
- }
-
- if startErr != nil {
- cleanup.run()
- }
-
- return startErr
-}
-
// Start starts the main daemon server, all requested listeners, and any helper
// goroutines.
// NOTE: This function is safe for concurrent access.
//
//nolint:funlen
func (s *server) Start(ctx context.Context) error {
- // Get the current blockbeat.
- beat, err := s.getStartingBeat()
- if err != nil {
- return err
- }
-
var startErr error
// If one sub system fails to start, the following code ensures that the
@@ -2213,6 +2173,12 @@ func (s *server) Start(ctx context.Context) error {
return
}
+ cleanup = cleanup.add(s.cc.ChainNotifier.Stop)
+ if err := s.cc.ChainNotifier.Start(); err != nil {
+ startErr = err
+ return
+ }
+
cleanup = cleanup.add(s.cc.BestBlockTracker.Stop)
if err := s.cc.BestBlockTracker.Start(); err != nil {
startErr = err
@@ -2247,6 +2213,12 @@ func (s *server) Start(ctx context.Context) error {
}
}
+ beat, err := s.getStartingBeat()
+ if err != nil {
+ startErr = err
+ return
+ }
+
cleanup = cleanup.add(s.txPublisher.Stop)
if err := s.txPublisher.Start(beat); err != nil {
startErr = err
Why this scored 43/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.