From b249bf996234adbdd067365ca230e33fc0604f02 Mon Sep 17 00:00:00 2001 From: Sina Mahmoodi Date: Tue, 20 Jan 2026 17:17:20 +0100 Subject: [PATCH] cmd/geth: add Cancun pruning points --- cmd/geth/chaincmd.go | 79 ++++++++++++++++++++++++++----------- cmd/utils/flags.go | 2 +- core/blockchain.go | 72 ++++++++++++++++++++++++--------- core/history/historymode.go | 50 ++++++++++++++++++++--- 4 files changed, 156 insertions(+), 47 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 0af0a61602..cf54903d04 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -203,13 +203,19 @@ This command dumps out the state for a given block (or latest, if none provided) pruneHistoryCommand = &cli.Command{ Action: pruneHistory, Name: "prune-history", - Usage: "Prune blockchain history (block bodies and receipts) up to the merge block", + Usage: "Prune blockchain history (block bodies and receipts) up to a specified point", ArgsUsage: "", - Flags: utils.DatabaseFlags, + Flags: slices.Concat(utils.DatabaseFlags, []cli.Flag{ + utils.ChainHistoryFlag, + }), Description: ` The prune-history command removes historical block bodies and receipts from the -blockchain database up to the merge block, while preserving block headers. This -helps reduce storage requirements for nodes that don't need full historical data.`, +blockchain database up to a specified point, while preserving block headers. This +helps reduce storage requirements for nodes that don't need full historical data. + +The --history.chain flag is required to specify the pruning target: + - postmerge: Prune up to the merge block + - postcancun: Prune up to the Cancun (Dencun) upgrade block`, } downloadEraCommand = &cli.Command{ @@ -670,47 +676,74 @@ func hashish(x string) bool { } func pruneHistory(ctx *cli.Context) error { + // Parse and validate the history mode flag. + if !ctx.IsSet(utils.ChainHistoryFlag.Name) { + return errors.New("--history.chain flag is required (use 'postmerge' or 'postcancun')") + } + var mode history.HistoryMode + if err := mode.UnmarshalText([]byte(ctx.String(utils.ChainHistoryFlag.Name))); err != nil { + return err + } + if mode == history.KeepAll { + return errors.New("--history.chain=all is not valid for pruning. To restore history, use 'geth import-history'") + } + stack, _ := makeConfigNode(ctx) defer stack.Close() - // Open the chain database + // Open the chain database. chain, chaindb := utils.MakeChain(ctx, stack, false) defer chaindb.Close() defer chain.Stop() - // Determine the prune point. This will be the first PoS block. - prunePoint, ok := history.PrunePoints[chain.Genesis().Hash()] - if !ok || prunePoint == nil { - return errors.New("prune point not found") + // Determine the prune point based on the history mode. + genesisHash := chain.Genesis().Hash() + prunePoint := history.GetPrunePoint(genesisHash, mode) + if prunePoint == nil { + return fmt.Errorf("prune point for %q not found for this network", mode.String()) } var ( - mergeBlock = prunePoint.BlockNumber - mergeBlockHash = prunePoint.BlockHash.Hex() + targetBlock = prunePoint.BlockNumber + targetBlockHash = prunePoint.BlockHash ) - // Check we're far enough past merge to ensure all data is in freezer + // Check the current freezer tail to see if pruning is needed/possible. + freezerTail, _ := chaindb.Tail() + if freezerTail > 0 { + if freezerTail == targetBlock { + log.Info("Database already pruned to target block", "tail", freezerTail) + return nil + } + if freezerTail > targetBlock { + // Database is pruned beyond the target - can't unprune. + return fmt.Errorf("database is already pruned to block %d, which is beyond target %d. Cannot unprune. To restore history, use 'geth import-history'", freezerTail, targetBlock) + } + // freezerTail < targetBlock: we can prune further, continue below. + } + + // Check we're far enough past the target to ensure all data is in freezer. currentHeader := chain.CurrentHeader() if currentHeader == nil { return errors.New("current header not found") } - if currentHeader.Number.Uint64() < mergeBlock+params.FullImmutabilityThreshold { - return fmt.Errorf("chain not far enough past merge block, need %d more blocks", - mergeBlock+params.FullImmutabilityThreshold-currentHeader.Number.Uint64()) + if currentHeader.Number.Uint64() < targetBlock+params.FullImmutabilityThreshold { + return fmt.Errorf("chain not far enough past target block %d, need %d more blocks", + targetBlock, targetBlock+params.FullImmutabilityThreshold-currentHeader.Number.Uint64()) } - // Double-check the prune block in db has the expected hash. - hash := rawdb.ReadCanonicalHash(chaindb, mergeBlock) - if hash != common.HexToHash(mergeBlockHash) { - return fmt.Errorf("merge block hash mismatch: got %s, want %s", hash.Hex(), mergeBlockHash) + // Double-check the target block in db has the expected hash. + hash := rawdb.ReadCanonicalHash(chaindb, targetBlock) + if hash != targetBlockHash { + return fmt.Errorf("target block hash mismatch: got %s, want %s", hash.Hex(), targetBlockHash.Hex()) } - log.Info("Starting history pruning", "head", currentHeader.Number, "tail", mergeBlock, "tailHash", mergeBlockHash) + log.Info("Starting history pruning", "head", currentHeader.Number, "target", targetBlock, "targetHash", targetBlockHash.Hex()) start := time.Now() - rawdb.PruneTransactionIndex(chaindb, mergeBlock) - if _, err := chaindb.TruncateTail(mergeBlock); err != nil { + rawdb.PruneTransactionIndex(chaindb, targetBlock) + if _, err := chaindb.TruncateTail(targetBlock); err != nil { return fmt.Errorf("failed to truncate ancient data: %v", err) } - log.Info("History pruning completed", "tail", mergeBlock, "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("History pruning completed", "tail", targetBlock, "elapsed", common.PrettyDuration(time.Since(start))) // TODO(s1na): what if there is a crash between the two prune operations? diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index fe8375454f..cc64743a52 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -309,7 +309,7 @@ var ( } ChainHistoryFlag = &cli.StringFlag{ Name: "history.chain", - Usage: `Blockchain history retention ("all" or "postmerge")`, + Usage: `Blockchain history retention ("all", "postmerge", or "postcancun")`, Value: ethconfig.Defaults.HistoryMode.String(), Category: flags.StateCategory, } diff --git a/core/blockchain.go b/core/blockchain.go index fc0e70c271..1dd944d436 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -702,8 +702,12 @@ func (bc *BlockChain) loadLastState() error { // initializeHistoryPruning sets bc.historyPrunePoint. func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { - freezerTail, _ := bc.db.Tail() - + var ( + freezerTail, _ = bc.db.Tail() + genesisHash = bc.genesisBlock.Hash() + mergePoint = history.MergePrunePoints[genesisHash] + cancunPoint = history.CancunPrunePoints[genesisHash] + ) switch bc.cfg.ChainHistoryMode { case history.KeepAll: if freezerTail == 0 { @@ -711,33 +715,65 @@ func (bc *BlockChain) initializeHistoryPruning(latest uint64) error { } // The database was pruned somehow, so we need to figure out if it's a known // configuration or an error. - predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()] - if predefinedPoint == nil || freezerTail != predefinedPoint.BlockNumber { - log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail) - return errors.New("unexpected database tail") + if mergePoint != nil && freezerTail == mergePoint.BlockNumber { + bc.historyPrunePoint.Store(mergePoint) + return nil } - bc.historyPrunePoint.Store(predefinedPoint) - return nil + if cancunPoint != nil && freezerTail == cancunPoint.BlockNumber { + bc.historyPrunePoint.Store(cancunPoint) + return nil + } + log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail) + return errors.New("unexpected database tail") case history.KeepPostMerge: + if mergePoint == nil { + return errors.New("history pruning requested for unknown network") + } if freezerTail == 0 && latest != 0 { - // This is the case where a user is trying to run with --history.chain - // postmerge directly on an existing DB. We could just trigger the pruning - // here, but it'd be a bit dangerous since they may not have intended this - // action to happen. So just tell them how to do it. log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cfg.ChainHistoryMode.String())) - log.Error(fmt.Sprintf("Run 'geth prune-history' to prune pre-merge history.")) + log.Error("Run 'geth prune-history --history.chain postmerge' to prune pre-merge history.") return errors.New("history pruning requested via configuration") } - predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()] - if predefinedPoint == nil { - log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash()) + // Check if DB is pruned further than requested (to Cancun). + if cancunPoint != nil && freezerTail == cancunPoint.BlockNumber { + log.Error("Chain history database is pruned to Cancun 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-Cancun history, use '--history.chain postcancun' 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.KeepPostCancun: + if cancunPoint == nil { return errors.New("history pruning requested for unknown network") - } else if freezerTail > 0 && freezerTail != predefinedPoint.BlockNumber { + } + // Check if already at the cancun prune point. + if freezerTail == cancunPoint.BlockNumber { + bc.historyPrunePoint.Store(cancunPoint) + 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 postcancun' to prune pre-Cancun 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 postcancun' to prune pre-Cancun 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") } - bc.historyPrunePoint.Store(predefinedPoint) + // Fresh database (latest == 0), will sync from cancun point. + bc.historyPrunePoint.Store(cancunPoint) return nil default: diff --git a/core/history/historymode.go b/core/history/historymode.go index e735222d37..dbcf7ed018 100644 --- a/core/history/historymode.go +++ b/core/history/historymode.go @@ -32,10 +32,13 @@ const ( // KeepPostMerge sets the history pruning point to the merge activation block. KeepPostMerge + + // KeepPostCancun sets the history pruning point to the Cancun (Dencun) activation block. + KeepPostCancun ) func (m HistoryMode) IsValid() bool { - return m <= KeepPostMerge + return m <= KeepPostCancun } func (m HistoryMode) String() string { @@ -44,6 +47,8 @@ func (m HistoryMode) String() string { return "all" case KeepPostMerge: return "postmerge" + case KeepPostCancun: + return "postcancun" default: return fmt.Sprintf("invalid HistoryMode(%d)", m) } @@ -64,8 +69,10 @@ func (m *HistoryMode) UnmarshalText(text []byte) error { *m = KeepAll case "postmerge": *m = KeepPostMerge + case "postcancun": + *m = KeepPostCancun default: - return fmt.Errorf(`unknown sync mode %q, want "all" or "postmerge"`, text) + return fmt.Errorf(`unknown history mode %q, want "all", "postmerge", or "postcancun"`, text) } return nil } @@ -75,10 +82,10 @@ type PrunePoint struct { BlockHash common.Hash } -// PrunePoints the pre-defined history pruning cutoff blocks for known networks. +// 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 -// given block. -var PrunePoints = map[common.Hash]*PrunePoint{ +// the given block. +var MergePrunePoints = map[common.Hash]*PrunePoint{ // mainnet params.MainnetGenesisHash: { BlockNumber: 15537393, @@ -91,6 +98,39 @@ var PrunePoints = map[common.Hash]*PrunePoint{ }, } +// CancunPrunePoints contains the pre-defined history pruning cutoff blocks for the Cancun +// (Dencun) upgrade. They point to the first post-Cancun block. Any pruning should truncate +// *up to* but excluding the given block. +var CancunPrunePoints = map[common.Hash]*PrunePoint{ + // mainnet - first Cancun block (March 13, 2024) + params.MainnetGenesisHash: { + BlockNumber: 19426587, + BlockHash: common.HexToHash("0xf8e2f40d98fe5862bc947c8c83d34799c50fb344d7445d020a8a946d891b62ee"), + }, + // sepolia - first Cancun block (January 30, 2024) + params.SepoliaGenesisHash: { + BlockNumber: 5187023, + BlockHash: common.HexToHash("0x8f9753667f95418f70db36279a269ed6523cea399ecc3f4cfa2f1689a3a4b130"), + }, +} + +// 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 { + switch mode { + case KeepPostMerge: + return MergePrunePoints[genesisHash] + case KeepPostCancun: + return CancunPrunePoints[genesisHash] + default: + return nil + } +} + // PrunedHistoryError is returned by APIs when the requested history is pruned. type PrunedHistoryError struct{}