diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 7e14ec1c60..1084100f39 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -731,13 +731,16 @@ func pruneHistory(ctx *cli.Context) error { // Determine the prune point based on the history mode. genesisHash := chain.Genesis().Hash() - prunePoint := history.GetPrunePoint(genesisHash, mode) - if prunePoint == nil { + policy, err := history.NewPolicy(mode, genesisHash) + if err != nil { + return err + } + if policy.Target == nil { return fmt.Errorf("prune point for %q not found for this network", mode.String()) } var ( - targetBlock = prunePoint.BlockNumber - targetBlockHash = prunePoint.BlockHash + targetBlock = policy.Target.BlockNumber + targetBlockHash = policy.Target.BlockHash ) // Check the current freezer tail to see if pruning is needed/possible. diff --git a/cmd/workload/testsuite.go b/cmd/workload/testsuite.go index 80cbd15352..4e33522f1b 100644 --- a/cmd/workload/testsuite.go +++ b/cmd/workload/testsuite.go @@ -155,7 +155,9 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) { } cfg.historyPruneBlock = new(uint64) - *cfg.historyPruneBlock = history.PrunePoints[params.MainnetGenesisHash].BlockNumber + if p, err := history.NewPolicy(history.KeepPostMerge, params.MainnetGenesisHash); err == nil { + *cfg.historyPruneBlock = p.Target.BlockNumber + } case ctx.Bool(testSepoliaFlag.Name): cfg.fsys = builtinTestFiles if ctx.IsSet(filterQueryFileFlag.Name) { @@ -180,7 +182,9 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) { } cfg.historyPruneBlock = new(uint64) - *cfg.historyPruneBlock = history.PrunePoints[params.SepoliaGenesisHash].BlockNumber + if p, err := history.NewPolicy(history.KeepPostMerge, params.SepoliaGenesisHash); err == nil { + *cfg.historyPruneBlock = p.Target.BlockNumber + } default: cfg.fsys = os.DirFS(".") cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name) diff --git a/core/blockchain.go b/core/blockchain.go index 42a8405ec9..1b45a5ac39 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -194,9 +194,8 @@ type BlockChainConfig struct { SnapshotNoBuild bool // Whether the background generation is allowed SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it - // This defines the cutoff block for history expiry. - // Blocks before this number may be unavailable in the chain database. - ChainHistoryMode history.HistoryMode + // HistoryPolicy defines the chain history pruning intent. + HistoryPolicy history.HistoryPolicy // Misc options NoPrefetch bool // Whether to disable heuristic state prefetching when processing blocks @@ -227,13 +226,13 @@ type BlockChainConfig struct { // Note the returned object is safe to modify! func DefaultConfig() *BlockChainConfig { return &BlockChainConfig{ - TrieCleanLimit: 256, - TrieDirtyLimit: 256, - TrieTimeLimit: 5 * time.Minute, - StateScheme: rawdb.HashScheme, - SnapshotLimit: 256, - SnapshotWait: true, - ChainHistoryMode: history.KeepAll, + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + StateScheme: rawdb.HashScheme, + SnapshotLimit: 256, + SnapshotWait: true, + HistoryPolicy: history.HistoryPolicy{Mode: history.KeepAll}, // Transaction indexing is disabled by default. // This is appropriate for most unit tests. TxLookupLimit: -1, @@ -715,82 +714,44 @@ func (bc *BlockChain) loadLastState() error { // initializeHistoryPruning sets bc.historyPrunePoint. func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { - var ( - freezerTail, _ = bc.db.Tail() - genesisHash = bc.genesisBlock.Hash() - mergePoint = history.MergePrunePoints[genesisHash] - praguePoint = history.PraguePrunePoints[genesisHash] - ) - switch bc.cfg.ChainHistoryMode { - case history.KeepAll: - if freezerTail == 0 { - return nil - } - // The database was pruned somehow, so we need to figure out if it's a known - // configuration or an error. - if mergePoint != nil && freezerTail == mergePoint.BlockNumber { - bc.historyPrunePoint.Store(mergePoint) - return nil - } - if praguePoint != nil && freezerTail == praguePoint.BlockNumber { - bc.historyPrunePoint.Store(praguePoint) - return nil - } - log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail) - return errors.New("unexpected database tail") + freezerTail, _ := bc.db.Tail() + policy := bc.cfg.HistoryPolicy - case history.KeepPostMerge: - if mergePoint == nil { - return errors.New("history pruning requested for unknown network") + switch policy.Mode { + case history.KeepAll: + if freezerTail > 0 { + // Database was pruned externally. Record the actual state. + log.Warn("Chain history database is pruned", "tail", freezerTail, "mode", policy.Mode) + bc.historyPrunePoint.Store(&history.PrunePoint{ + BlockNumber: freezerTail, + BlockHash: bc.GetCanonicalHash(freezerTail), + }) } - if freezerTail == 0 && latest != 0 { - log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String())) - log.Error("Run 'geth prune-history --history.chain postmerge' to prune pre-merge history.") - return errors.New("history pruning requested via configuration") - } - // Check if DB is pruned further than requested (to Prague). - if praguePoint != nil && freezerTail == praguePoint.BlockNumber { - log.Error("Chain history database is pruned to Prague block, but postmerge mode was requested.") - log.Error("History cannot be unpruned. To restore history, use 'geth import-history'.") - log.Error("If you intended to keep post-Prague history, use '--history.chain postprague' instead.") - return errors.New("database pruned beyond requested history mode") - } - if freezerTail > 0 && freezerTail != mergePoint.BlockNumber { - return errors.New("chain history database pruned to unknown block") - } - bc.historyPrunePoint.Store(mergePoint) return nil - case history.KeepPostPrague: - if praguePoint == nil { - return errors.New("history pruning requested for unknown network") - } - // Check if already at the prague prune point. - if freezerTail == praguePoint.BlockNumber { - bc.historyPrunePoint.Store(praguePoint) + case history.KeepPostMerge, history.KeepPostPrague: + target := policy.Target + // Already at the target. + if freezerTail == target.BlockNumber { + bc.historyPrunePoint.Store(target) return nil } - // Check if database needs pruning. - if latest != 0 { - if freezerTail == 0 { - log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String())) - log.Error("Run 'geth prune-history --history.chain postprague' to prune pre-Prague history.") - return errors.New("history pruning requested via configuration") - } - if mergePoint != nil && freezerTail == mergePoint.BlockNumber { - log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is only pruned to merge block.", bc.cfg.ChainHistoryMode.String())) - log.Error("Run 'geth prune-history --history.chain postprague' to prune pre-Prague history.") - return errors.New("history pruning requested via configuration") - } - log.Error("Chain history database is pruned to unknown block", "tail", freezerTail) - return errors.New("unexpected database tail") + // Database is pruned beyond the target. + if freezerTail > target.BlockNumber { + return fmt.Errorf("database pruned beyond requested history (tail=%d, target=%d)", freezerTail, target.BlockNumber) } - // Fresh database (latest == 0), will sync from prague point. - bc.historyPrunePoint.Store(praguePoint) + // Database needs pruning (freezerTail < target). + if latest != 0 { + log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned to the target block.", policy.Mode.String())) + log.Error(fmt.Sprintf("Run 'geth prune-history --history.chain %s' to prune history.", policy.Mode.String())) + return errors.New("history pruning required") + } + // Fresh database (latest == 0), will sync from target point. + bc.historyPrunePoint.Store(target) return nil default: - return fmt.Errorf("invalid history mode: %d", bc.cfg.ChainHistoryMode) + return fmt.Errorf("invalid history mode: %d", policy.Mode) } } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index ce592f0267..d3ca21b2b3 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -36,7 +36,6 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core/history" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -4337,26 +4336,13 @@ func TestInsertChainWithCutoff(t *testing.T) { func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, genesis *Genesis, blocks []*types.Block, receipts []types.Receipts) { // log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true))) - // Add a known pruning point for the duration of the test. ghash := genesis.ToBlock().Hash() cutoffBlock := blocks[cutoff-1] - history.PrunePoints[ghash] = &history.PrunePoint{ - BlockNumber: cutoffBlock.NumberU64(), - BlockHash: cutoffBlock.Hash(), - } - defer func() { - delete(history.PrunePoints, ghash) - }() - - // Enable pruning in cache config. - config := DefaultConfig().WithStateScheme(rawdb.PathScheme) - config.ChainHistoryMode = history.KeepPostMerge db, _ := rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{}) defer db.Close() - options := DefaultConfig().WithStateScheme(rawdb.PathScheme) - chain, _ := NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), options) + chain, _ := NewBlockChain(db, genesis, beacon.New(ethash.NewFaker()), DefaultConfig().WithStateScheme(rawdb.PathScheme)) defer chain.Stop() var ( diff --git a/core/history/historymode.go b/core/history/historymode.go index bdaf07826d..1adfe014b2 100644 --- a/core/history/historymode.go +++ b/core/history/historymode.go @@ -77,57 +77,62 @@ func (m *HistoryMode) UnmarshalText(text []byte) error { return nil } +// PrunePoint identifies a specific block for history pruning. type PrunePoint struct { BlockNumber uint64 BlockHash common.Hash } -// MergePrunePoints contains the pre-defined history pruning cutoff blocks for known networks. -// They point to the first post-merge block. Any pruning should truncate *up to* but excluding -// the given block. -var MergePrunePoints = map[common.Hash]*PrunePoint{ - // mainnet - params.MainnetGenesisHash: { - BlockNumber: 15537393, - BlockHash: common.HexToHash("0x55b11b918355b1ef9c5db810302ebad0bf2544255b530cdce90674d5887bb286"), +// staticPrunePoints contains the pre-defined history pruning cutoff blocks for +// known networks, keyed by history mode and genesis hash. They point to the first +// block after the respective fork. Any pruning should truncate *up to* but +// excluding the given block. +var staticPrunePoints = map[HistoryMode]map[common.Hash]*PrunePoint{ + KeepPostMerge: { + params.MainnetGenesisHash: { + BlockNumber: 15537393, + BlockHash: common.HexToHash("0x55b11b918355b1ef9c5db810302ebad0bf2544255b530cdce90674d5887bb286"), + }, + params.SepoliaGenesisHash: { + BlockNumber: 1450409, + BlockHash: common.HexToHash("0x229f6b18ca1552f1d5146deceb5387333f40dc6275aebee3f2c5c4ece07d02db"), + }, }, - // sepolia - params.SepoliaGenesisHash: { - BlockNumber: 1450409, - BlockHash: common.HexToHash("0x229f6b18ca1552f1d5146deceb5387333f40dc6275aebee3f2c5c4ece07d02db"), + KeepPostPrague: { + params.MainnetGenesisHash: { + BlockNumber: 22431084, + BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"), + }, + params.SepoliaGenesisHash: { + BlockNumber: 7836331, + BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"), + }, }, } -// PraguePrunePoints contains the pre-defined history pruning cutoff blocks for the Prague -// (Pectra) upgrade. They point to the first post-Prague block. Any pruning should truncate -// *up to* but excluding the given block. -var PraguePrunePoints = map[common.Hash]*PrunePoint{ - // mainnet - first Prague block (May 7, 2025) - params.MainnetGenesisHash: { - BlockNumber: 22431084, - BlockHash: common.HexToHash("0x50c8cab760b2948349c590461b166773c45d8f4858cccf5a43025ab2960152e8"), - }, - // sepolia - first Prague block (March 5, 2025) - params.SepoliaGenesisHash: { - BlockNumber: 7836331, - BlockHash: common.HexToHash("0xe6571beb68bf24dbd8a6ba354518996920c55a3f8d8fdca423e391b8ad071f22"), - }, +// HistoryPolicy describes the configured history pruning strategy. It captures +// user intent as opposed to the actual DB state. +type HistoryPolicy struct { + Mode HistoryMode + // Static prune point for PostMerge/PostPrague, nil otherwise. + Target *PrunePoint } -// PrunePoints is an alias for MergePrunePoints for backward compatibility. -// Deprecated: Use GetPrunePoint or MergePrunePoints directly. -var PrunePoints = MergePrunePoints - -// GetPrunePoint returns the prune point for the given genesis hash and history mode. -// Returns nil if no prune point is defined for the given combination. -func GetPrunePoint(genesisHash common.Hash, mode HistoryMode) *PrunePoint { +// NewPolicy constructs a HistoryPolicy from the given mode and genesis hash. +func NewPolicy(mode HistoryMode, genesisHash common.Hash) (HistoryPolicy, error) { switch mode { - case KeepPostMerge: - return MergePrunePoints[genesisHash] - case KeepPostPrague: - return PraguePrunePoints[genesisHash] + case KeepAll: + return HistoryPolicy{Mode: KeepAll}, nil + + case KeepPostMerge, KeepPostPrague: + point := staticPrunePoints[mode][genesisHash] + if point == nil { + return HistoryPolicy{}, fmt.Errorf("%s history pruning not available for network %s", mode, genesisHash.Hex()) + } + return HistoryPolicy{Mode: mode, Target: point}, nil + default: - return nil + return HistoryPolicy{}, fmt.Errorf("invalid history mode: %d", mode) } } diff --git a/core/history/historymode_test.go b/core/history/historymode_test.go new file mode 100644 index 0000000000..87eae188dd --- /dev/null +++ b/core/history/historymode_test.go @@ -0,0 +1,58 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package history + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +func TestNewPolicy(t *testing.T) { + // KeepAll: no target. + p, err := NewPolicy(KeepAll, params.MainnetGenesisHash) + if err != nil { + t.Fatalf("KeepAll: %v", err) + } + if p.Mode != KeepAll || p.Target != nil { + t.Errorf("KeepAll: unexpected policy %+v", p) + } + + // PostMerge: resolves known mainnet prune point. + p, err = NewPolicy(KeepPostMerge, params.MainnetGenesisHash) + if err != nil { + t.Fatalf("PostMerge: %v", err) + } + if p.Target == nil || p.Target.BlockNumber != 15537393 { + t.Errorf("PostMerge: unexpected target %+v", p.Target) + } + + // PostPrague: resolves known mainnet prune point. + p, err = NewPolicy(KeepPostPrague, params.MainnetGenesisHash) + if err != nil { + t.Fatalf("PostPrague: %v", err) + } + if p.Target == nil || p.Target.BlockNumber != 22431084 { + t.Errorf("PostPrague: unexpected target %+v", p.Target) + } + + // PostMerge on unknown network: error. + if _, err = NewPolicy(KeepPostMerge, common.HexToHash("0xdeadbeef")); err == nil { + t.Fatal("PostMerge unknown network: expected error") + } +} diff --git a/eth/backend.go b/eth/backend.go index 72228614f0..e9bea59734 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/filtermaps" + "github.com/ethereum/go-ethereum/core/history" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/pruner" "github.com/ethereum/go-ethereum/core/txpool" @@ -175,7 +176,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // Here we determine genesis hash and active ChainConfig. // We need these to figure out the consensus parameters and to set up history pruning. - chainConfig, _, err := core.LoadChainConfig(chainDb, config.Genesis) + chainConfig, genesisHash, err := core.LoadChainConfig(chainDb, config.Genesis) if err != nil { return nil, err } @@ -220,6 +221,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { rawdb.WriteDatabaseVersion(chainDb, core.BlockChainVersion) } } + histPolicy, err := history.NewPolicy(config.HistoryMode, genesisHash) + if err != nil { + return nil, err + } var ( options = &core.BlockChainConfig{ TrieCleanLimit: config.TrieCleanCache, @@ -233,7 +238,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { TrienodeHistory: config.TrienodeHistory, NodeFullValueCheckpoint: config.NodeFullValueCheckpoint, StateScheme: scheme, - ChainHistoryMode: config.HistoryMode, + HistoryPolicy: histPolicy, TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)), VmConfig: vm.Config{ EnablePreimageRecording: config.EnablePreimageRecording,