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 } 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. 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 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.