diff --git a/cmd/geth/main.go b/cmd/geth/main.go index fda6edb78dc..fd3f4e1bab6 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -86,6 +86,7 @@ var ( utils.SnapshotFlag, utils.TxLookupLimitFlag, // deprecated utils.TransactionHistoryFlag, + utils.ChainHistoryFlag, utils.StateHistoryFlag, utils.LightServeFlag, // deprecated utils.LightIngressFlag, // deprecated diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 1394f9060e2..7c32d7e7342 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -272,6 +272,12 @@ var ( Value: ethconfig.Defaults.TransactionHistory, Category: flags.StateCategory, } + ChainHistoryFlag = &cli.StringFlag{ + Name: "history.chain", + Usage: `Blockchain history retention ("all" or "postmerge")`, + Value: ethconfig.Defaults.HistoryMode.String(), + Category: flags.StateCategory, + } // Beacon client light sync settings BeaconApiFlag = &cli.StringSliceFlag{ Name: "beacon.api", @@ -1566,10 +1572,19 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(SyncTargetFlag.Name) { cfg.SyncMode = ethconfig.FullSync // dev sync target forces full sync } else if ctx.IsSet(SyncModeFlag.Name) { - if err = cfg.SyncMode.UnmarshalText([]byte(ctx.String(SyncModeFlag.Name))); err != nil { - Fatalf("invalid --syncmode flag: %v", err) + value := ctx.String(SyncModeFlag.Name) + if err = cfg.SyncMode.UnmarshalText([]byte(value)); err != nil { + Fatalf("--%v: %v", SyncModeFlag.Name, err) + } + } + + if ctx.IsSet(ChainHistoryFlag.Name) { + value := ctx.String(ChainHistoryFlag.Name) + if err = cfg.HistoryMode.UnmarshalText([]byte(value)); err != nil { + Fatalf("--%s: %v", ChainHistoryFlag.Name, err) } } + if ctx.IsSet(NetworkIdFlag.Name) { cfg.NetworkId = ctx.Uint64(NetworkIdFlag.Name) } @@ -2086,7 +2101,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh gspec = MakeGenesis(ctx) chainDb = MakeChainDatabase(ctx, stack, readonly) ) - config, err := core.LoadChainConfig(chainDb, gspec) + config, _, err := core.LoadChainConfig(chainDb, gspec) if err != nil { Fatalf("%v", err) } diff --git a/core/blockchain.go b/core/blockchain.go index b98c2d43aa8..7aad5c9d26e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -154,6 +154,10 @@ type CacheConfig 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. + HistoryPruningCutoff uint64 } // triedbConfig derives the configures for trie database. @@ -2519,3 +2523,9 @@ func (bc *BlockChain) SetTrieFlushInterval(interval time.Duration) { func (bc *BlockChain) GetTrieFlushInterval() time.Duration { return time.Duration(bc.flushInterval.Load()) } + +// HistoryPruningCutoff returns the configured history pruning point. +// Blocks before this might not be available in the database. +func (bc *BlockChain) HistoryPruningCutoff() uint64 { + return bc.cacheConfig.HistoryPruningCutoff +} diff --git a/core/genesis.go b/core/genesis.go index 9bd8c4f8b92..23f4a73ec51 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -386,7 +386,7 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *triedb.Database, g // LoadChainConfig loads the stored chain config if it is already present in // database, otherwise, return the config in the provided genesis specification. -func LoadChainConfig(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, error) { +func LoadChainConfig(db ethdb.Database, genesis *Genesis) (cfg *params.ChainConfig, ghash common.Hash, err error) { // Load the stored chain config from the database. It can be nil // in case the database is empty. Notably, we only care about the // chain config corresponds to the canonical chain. @@ -394,27 +394,28 @@ func LoadChainConfig(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, if stored != (common.Hash{}) { storedcfg := rawdb.ReadChainConfig(db, stored) if storedcfg != nil { - return storedcfg, nil + return storedcfg, stored, nil } } // Load the config from the provided genesis specification if genesis != nil { // Reject invalid genesis spec without valid chain config if genesis.Config == nil { - return nil, errGenesisNoConfig + return nil, common.Hash{}, errGenesisNoConfig } // If the canonical genesis header is present, but the chain // config is missing(initialize the empty leveldb with an // external ancient chain segment), ensure the provided genesis // is matched. - if stored != (common.Hash{}) && genesis.ToBlock().Hash() != stored { - return nil, &GenesisMismatchError{stored, genesis.ToBlock().Hash()} + ghash := genesis.ToBlock().Hash() + if stored != (common.Hash{}) && ghash != stored { + return nil, ghash, &GenesisMismatchError{stored, ghash} } - return genesis.Config, nil + return genesis.Config, ghash, nil } // There is no stored chain config and no new config provided, // In this case the default chain config(mainnet) will be used - return params.MainnetChainConfig, nil + return params.MainnetChainConfig, params.MainnetGenesisHash, nil } // chainConfigOrDefault retrieves the attached chain configuration. If the genesis diff --git a/eth/backend.go b/eth/backend.go index 8fbbc20a47d..1790e413fd5 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -110,6 +110,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if !config.SyncMode.IsValid() { return nil, fmt.Errorf("invalid sync mode %d", config.SyncMode) } + if !config.HistoryMode.IsValid() { + return nil, fmt.Errorf("invalid history mode %d", config.HistoryMode) + } if config.Miner.GasPrice == nil || config.Miner.GasPrice.Sign() <= 0 { log.Warn("Sanitizing invalid miner gas price", "provided", config.Miner.GasPrice, "updated", ethconfig.Defaults.Miner.GasPrice) config.Miner.GasPrice = new(big.Int).Set(ethconfig.Defaults.Miner.GasPrice) @@ -125,7 +128,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } log.Info("Allocated trie memory caches", "clean", common.StorageSize(config.TrieCleanCache)*1024*1024, "dirty", common.StorageSize(config.TrieDirtyCache)*1024*1024) - // Assemble the Ethereum object chainDb, err := stack.OpenDatabaseWithFreezer("chaindata", config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, "eth/db/chaindata/", false) if err != nil { return nil, err @@ -140,8 +142,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { log.Error("Failed to recover state", "error", err) } } - // Transfer mining-related config to the ethash config. - chainConfig, err := core.LoadChainConfig(chainDb, config.Genesis) + + // Here we determine genesis hash and active ChainConfig. + // We need these to figure out the consensus parameters and to set up history pruning. + chainConfig, genesisHash, err := core.LoadChainConfig(chainDb, config.Genesis) if err != nil { return nil, err } @@ -149,10 +153,24 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if err != nil { return nil, err } + + // Validate history pruning configuration. + var historyPruningCutoff uint64 + if config.HistoryMode == ethconfig.PostMergeHistory { + prunecfg, ok := ethconfig.HistoryPrunePoints[genesisHash] + if !ok { + return nil, fmt.Errorf("no history pruning point is defined for genesis %x", genesisHash) + } + historyPruningCutoff = prunecfg.BlockNumber + } + + // Set networkID to chainID by default. networkID := config.NetworkId if networkID == 0 { networkID = chainConfig.ChainID.Uint64() } + + // Assemble the Ethereum object. eth := &Ethereum{ config: config, chainDb: chainDb, @@ -190,15 +208,16 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { EnablePreimageRecording: config.EnablePreimageRecording, } cacheConfig = &core.CacheConfig{ - TrieCleanLimit: config.TrieCleanCache, - TrieCleanNoPrefetch: config.NoPrefetch, - TrieDirtyLimit: config.TrieDirtyCache, - TrieDirtyDisabled: config.NoPruning, - TrieTimeLimit: config.TrieTimeout, - SnapshotLimit: config.SnapshotCache, - Preimages: config.Preimages, - StateHistory: config.StateHistory, - StateScheme: scheme, + TrieCleanLimit: config.TrieCleanCache, + TrieCleanNoPrefetch: config.NoPrefetch, + TrieDirtyLimit: config.TrieDirtyCache, + TrieDirtyDisabled: config.NoPruning, + TrieTimeLimit: config.TrieTimeout, + SnapshotLimit: config.SnapshotCache, + Preimages: config.Preimages, + StateHistory: config.StateHistory, + StateScheme: scheme, + HistoryPruningCutoff: historyPruningCutoff, } ) if config.VMTrace != "" { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 0bf9ce3579d..ec6de9e6638 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -48,6 +48,7 @@ var FullNodeGPO = gasprice.Config{ // Defaults contains default settings for use on the Ethereum main net. var Defaults = Config{ + HistoryMode: AllHistory, SyncMode: SnapSync, NetworkId: 0, // enable auto configuration of networkID == chainID TxLookupLimit: 2350000, @@ -81,6 +82,9 @@ type Config struct { NetworkId uint64 SyncMode SyncMode + // HistoryMode configures chain history retention. + HistoryMode HistoryMode + // This can be set to list of enrtree:// URLs which will be queried for // nodes to connect to. EthDiscoveryURLs []string diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 11c0c06372c..bcfe8a31d62 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -19,6 +19,7 @@ func (c Config) MarshalTOML() (interface{}, error) { Genesis *core.Genesis `toml:",omitempty"` NetworkId uint64 SyncMode SyncMode + HistoryMode HistoryMode EthDiscoveryURLs []string SnapDiscoveryURLs []string NoPruning bool @@ -55,6 +56,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.Genesis = c.Genesis enc.NetworkId = c.NetworkId enc.SyncMode = c.SyncMode + enc.HistoryMode = c.HistoryMode enc.EthDiscoveryURLs = c.EthDiscoveryURLs enc.SnapDiscoveryURLs = c.SnapDiscoveryURLs enc.NoPruning = c.NoPruning @@ -95,6 +97,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { Genesis *core.Genesis `toml:",omitempty"` NetworkId *uint64 SyncMode *SyncMode + HistoryMode *HistoryMode EthDiscoveryURLs []string SnapDiscoveryURLs []string NoPruning *bool @@ -140,6 +143,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.SyncMode != nil { c.SyncMode = *dec.SyncMode } + if dec.HistoryMode != nil { + c.HistoryMode = *dec.HistoryMode + } if dec.EthDiscoveryURLs != nil { c.EthDiscoveryURLs = dec.EthDiscoveryURLs } diff --git a/eth/ethconfig/historymode.go b/eth/ethconfig/historymode.go new file mode 100644 index 00000000000..cf904ab5914 --- /dev/null +++ b/eth/ethconfig/historymode.go @@ -0,0 +1,90 @@ +// Copyright 2025 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 ethconfig + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +// HistoryMode configures history pruning. +type HistoryMode uint32 + +const ( + // AllHistory (default) means that all chain history down to genesis block will be kept. + AllHistory HistoryMode = iota + + // PostMergeHistory sets the history pruning point to the merge activation block. + PostMergeHistory +) + +func (m HistoryMode) IsValid() bool { + return m <= PostMergeHistory +} + +func (m HistoryMode) String() string { + switch m { + case AllHistory: + return "all" + case PostMergeHistory: + return "postmerge" + default: + return fmt.Sprintf("invalid HistoryMode(%d)", m) + } +} + +// MarshalText implements encoding.TextMarshaler. +func (m HistoryMode) MarshalText() ([]byte, error) { + if m.IsValid() { + return []byte(m.String()), nil + } + return nil, fmt.Errorf("unknown history mode %d", m) +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (m *HistoryMode) UnmarshalText(text []byte) error { + switch string(text) { + case "all": + *m = AllHistory + case "postmerge": + *m = PostMergeHistory + default: + return fmt.Errorf(`unknown sync mode %q, want "all" or "postmerge"`, text) + } + return nil +} + +type HistoryPrunePoint struct { + BlockNumber uint64 + BlockHash common.Hash +} + +// HistoryPrunePoints contains the pre-defined history pruning cutoff blocks for known networks. +var HistoryPrunePoints = map[common.Hash]*HistoryPrunePoint{ + // mainnet + params.MainnetGenesisHash: { + BlockNumber: 15537394, + BlockHash: common.HexToHash("0x56a9bb0302da44b8c0b3df540781424684c3af04d0b7a38d72842b762076a664"), + }, + // sepolia + params.SepoliaGenesisHash: { + BlockNumber: 1735371, + BlockHash: common.HexToHash("0x36fb89fba5b7857cf0ca78b5a9625b4043ff4555dfce9b7bcdcdd758a11eb946"), + }, +}