From 0cd3072289a016df366a8482f17fe3dbd37d8ef7 Mon Sep 17 00:00:00 2001 From: Allis Yu Date: Tue, 21 Apr 2026 01:19:24 +0800 Subject: [PATCH 1/4] fix(pruner): preserve genesis in kv store during prune-block The original prune-block implementation calls TruncateTail(newTail) without first copying the genesis block to the kv store. Since the freezer's truncate hides every item below newTail (including block 0), a pruned node fails to restart with: Fatal: Failed to register the Ethereum service: failed to retrieve genesis from ancient out of bounds Fix by caching canonical hash, header, body, and total difficulty for block 0 before truncation, and writing them back to the kv tables after truncation. The kv-store path is the natural fallback for rawdb readers, so subsequent ReadBlock(hash, 0) calls from geth startup succeed. Receipts are omitted because the genesis block has no transactions. Change is idempotent: re-running the pruner with the same dataset simply re-writes identical keys. Verified via go build + go vet. --- core/state/pruner/block_pruner.go | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/state/pruner/block_pruner.go b/core/state/pruner/block_pruner.go index 0781cb3217d8..e29b9fba4066 100644 --- a/core/state/pruner/block_pruner.go +++ b/core/state/pruner/block_pruner.go @@ -143,6 +143,40 @@ func (p *BlockPruner) Prune() error { "blocksToDelete", newTail-oldTail, "amountReserved", p.amountReserved) + // Preserve the genesis block in the kv store before truncating the + // ancient tail. The freezer's TruncateTail(newTail) hides every item + // numbered below newTail, including block 0. After such truncation, + // geth refuses to start with: + // + // Fatal: Failed to register the Ethereum service: + // failed to retrieve genesis from ancient out of bounds + // + // because the startup code reads genesis via rawdb.ReadBlock(hash, 0) + // and the ancient lookup returns nothing once tail > 0. The kv-store + // path is the natural fallback for rawdb readers, so we copy genesis + // (canonical hash, header, body, total difficulty) back into the kv + // tables. Receipts are omitted because the genesis block has no + // transactions. + // + // This read must happen before TruncateTail: afterwards, the ancient + // lookup for block 0 will fail and we would be writing zeroes. + genesisHash := rawdb.ReadCanonicalHash(p.db, 0) + if genesisHash == (common.Hash{}) { + return errors.New("failed to read genesis canonical hash; refusing to truncate") + } + genesisHeader := rawdb.ReadHeader(p.db, genesisHash, 0) + if genesisHeader == nil { + return fmt.Errorf("failed to read genesis header for %s; refusing to truncate", genesisHash.Hex()) + } + genesisBody := rawdb.ReadBody(p.db, genesisHash, 0) + if genesisBody == nil { + return fmt.Errorf("failed to read genesis body for %s; refusing to truncate", genesisHash.Hex()) + } + genesisTd := rawdb.ReadTd(p.db, genesisHash, 0) + if genesisTd == nil { + return fmt.Errorf("failed to read genesis total difficulty for %s; refusing to truncate", genesisHash.Hex()) + } + // Perform the in-place tail truncation on the ancient store. This is a // local operation that drops data files on disk once the truncated range // spans an entire file (2 GiB per file by default), and hides partial @@ -156,6 +190,16 @@ func (p *BlockPruner) Prune() error { } log.Info("Ancient tail truncated", "newTail", newTail, "blocksDeleted", newTail-oldTail) + // Write genesis back to the kv store. This is idempotent; repeated + // invocations with the same genesis data simply overwrite the same + // keys. Placement matters: it must happen after TruncateTail so that + // genesis survives as the only block below newTail. + rawdb.WriteCanonicalHash(p.db, genesisHash, 0) + rawdb.WriteHeader(p.db, genesisHeader) + rawdb.WriteBody(p.db, genesisHash, 0, genesisBody) + rawdb.WriteTd(p.db, genesisHash, 0, genesisTd) + log.Info("Genesis block preserved in kv store", "hash", genesisHash.Hex()) + // Make sure the transaction index tail is not pointing below the new // ancient tail. Otherwise, when the node starts with --txlookuplimit, // the background indexer would try to walk pruned block bodies and From 58c2a677c8dc224fd8a250249267136f40e49e81 Mon Sep 17 00:00:00 2001 From: Allis Yu Date: Tue, 21 Apr 2026 01:28:54 +0800 Subject: [PATCH 2/4] chore: bump version to 1.4.9-unstable Reflects the genesis-preservation fix to snapshot prune-block introduced in commit 0cd307228 on this branch. Marked -unstable since this has not been tagged as a formal release. --- params/version.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/params/version.go b/params/version.go index ccfeecd1876d..3966f0315a17 100644 --- a/params/version.go +++ b/params/version.go @@ -23,8 +23,8 @@ import ( const ( VersionMajor = 1 // Major version component of the current release VersionMinor = 4 // Minor version component of the current release - VersionPatch = 8 // Patch version component of the current release - VersionMeta = "stable" // Version metadata to append to the version string + VersionPatch = 9 // Patch version component of the current release + VersionMeta = "unstable" // Version metadata to append to the version string ) // Version holds the textual version string. From 27fbed94b0b7be72928428465f32d2c721a778f5 Mon Sep 17 00:00:00 2001 From: Allis Yu Date: Tue, 21 Apr 2026 09:44:55 +0800 Subject: [PATCH 3/4] fix(rawdb): skip ancient genesis cross-check when freezer tail > 0 After snapshot prune-block truncates the freezer tail past block 0, the ancient store can no longer return genesis. The startup consistency check in NewDatabaseWithFreezer tried to read chainFreezerHashTable at position 0 unconditionally and failed with: Fatal: Failed to register the Ethereum service: failed to retrieve genesis from ancient out of bounds Guard the cross-check with tail == 0. When the tail has moved, the kv-store copy written by prune-block is the authoritative source; no mismatch is possible because prune-block copies verbatim bytes read from ancient moments before truncation. --- core/rawdb/database.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 6f09c2324267..a712503cc5e9 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -226,14 +226,22 @@ func NewDatabaseWithFreezer(db ethdb.KeyValueStore, ancient string, namespace st // it to the freezer content. if kvgenesis, _ := db.Get(headerHashKey(0)); len(kvgenesis) > 0 { if frozen, _ := frdb.Ancients(); frozen > 0 { - // If the freezer already contains something, ensure that the genesis blocks - // match, otherwise we might mix up freezers across chains and destroy both - // the freezer and the key-value store. - frgenesis, err := frdb.Ancient(chainFreezerHashTable, 0) - if err != nil { - return nil, fmt.Errorf("failed to retrieve genesis from ancient %v", err) - } else if !bytes.Equal(kvgenesis, frgenesis) { - return nil, fmt.Errorf("genesis mismatch: %#x (leveldb) != %#x (ancients)", kvgenesis, frgenesis) + // If offline block-pruning has advanced the freezer tail past + // block 0, the ancient store can no longer produce genesis; + // the kv store is the sole source of truth in that case and + // snapshot prune-block copied genesis there before truncating. + // Skip the ancient cross-check when we detect a non-zero tail. + frtail, _ := frdb.Tail() + if frtail == 0 { + // Genesis still lives in ancient; cross-validate the genesis + // blocks match, otherwise we might mix up freezers across + // chains and destroy both the freezer and the key-value store. + frgenesis, err := frdb.Ancient(chainFreezerHashTable, 0) + if err != nil { + return nil, fmt.Errorf("failed to retrieve genesis from ancient %v", err) + } else if !bytes.Equal(kvgenesis, frgenesis) { + return nil, fmt.Errorf("genesis mismatch: %#x (leveldb) != %#x (ancients)", kvgenesis, frgenesis) + } } // Key-value store and freezer belong to the same network. Ensure that they // are contiguous, otherwise we might end up with a non-functional freezer. From a1b71116ff32f361b860e2716a66a84eb777d454 Mon Sep 17 00:00:00 2001 From: Allis Yu Date: Tue, 21 Apr 2026 09:45:31 +0800 Subject: [PATCH 4/4] fix(cmd/geth): honor freezer tail in legacy receipts check dbHasLegacyReceipts scanned from firstIdx=0, calling db.Ancient( "receipts", 0) on mainnets where the mainnet hash heuristic does not apply (KCC NetworkId != 1). After snapshot prune-block advances the freezer tail, any read below the tail fails with: ERROR Failed to check db for legacy receipts err="out of bounds" The check is non-fatal (logged as ERROR, startup continues), but the noise obscures real issues and the check is effectively broken for pruned nodes. Clamp firstIdx up to db.Tail() so the scan only covers the accessible range. A pruned KCC mainnet is post-Byzantium for its entire retained window anyway, so there cannot be any legacy receipts in that range. --- cmd/geth/dbcmd.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index bb53a632e862..fd7c1ee7d6b2 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -797,6 +797,13 @@ func dbHasLegacyReceipts(db ethdb.Database, firstIdx uint64) (bool, uint64, erro if numAncients < 1 { return false, 0, nil } + // If offline block-pruning has advanced the freezer tail past firstIdx, + // scanning from position 0 will fail with "out of bounds" on the first + // Ancient() call. Clamp firstIdx to the current tail so we scan only + // the accessible range. + if tail, terr := db.Tail(); terr == nil && tail > firstIdx { + firstIdx = tail + } if firstIdx >= numAncients { return false, firstIdx, nil }