From 4fdd72489b800da1271a512bd968d1a89a4eb159 Mon Sep 17 00:00:00 2001 From: Frank Braun Date: Wed, 10 Jun 2026 01:05:27 +0200 Subject: [PATCH] Detect reorgs that occur while the server is down NewBlockCache did not initialize latestHash from the last block in the disk cache, so HashMatch vacuously accepted the first block ingested after every restart. If the backing node reorged across a lightwalletd restart, the new chain was appended on top of the orphaned blocks, which then remained in the compact-block cache permanently, serving wallets an internally inconsistent block stream. Initialize latestHash in NewBlockCache (using the existing setLatestHash helper, previously only called from Reorg) so the ingestor walks back orphaned blocks on the first ingest after a restart, the same way it does for reorgs detected while running. This also avoids the unnecessary drop and re-fetch of the cache tip on every restart when no reorg occurred. Co-Authored-By: Claude Fable 5 --- common/cache.go | 6 ++++++ common/cache_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/common/cache.go b/common/cache.go index 9090f7dd..3c2922d5 100644 --- a/common/cache.go +++ b/common/cache.go @@ -221,6 +221,12 @@ func NewBlockCache(dbPath string, chainName string, startHeight int, syncFromHei c.nextBlock++ } Log.Info("Done reading ", c.nextBlock-c.firstBlock, " blocks from disk cache") + + // Initialize latestHash from the last block on disk so that the first + // block ingested after a restart is checked against the cache tip. + // Otherwise, a reorg that occurred while the server was down would go + // undetected, permanently leaving orphan blocks in the cache. + c.setLatestHash() return c } diff --git a/common/cache_test.go b/common/cache_test.go index 3963ff54..5aac7efc 100644 --- a/common/cache_test.go +++ b/common/cache_test.go @@ -81,6 +81,21 @@ func TestCache(t *testing.T) { if cache.nextBlock != 289466 { t.Fatal("unexpected nextBlock height") } + // The hash of the top block must be restored from disk so that a reorg + // that occurred while the server was down can be detected (the first + // block ingested after a restart must connect to the cache tip). + if cache.latestHash == hash32.Nil { + t.Fatal("latestHash not initialized after restart") + } + if cache.latestHash != hash32.FromSlice(compacts[5].Hash) { + t.Fatal("unexpected latestHash after restart") + } + if cache.HashMatch(hash32.FromSlice(compacts[4].Hash)) { + t.Fatal("HashMatch should reject a non-connecting block after restart") + } + if !cache.HashMatch(hash32.FromSlice(compacts[5].Hash)) { + t.Fatal("HashMatch should accept a block connecting to the cache tip") + } reorgCache(t) // Reorg to before the first block moves back to only the first block