cmd/geth: add Cancun pruning points

This commit is contained in:
Sina Mahmoodi 2026-01-20 17:17:20 +01:00
parent 2eb1ccc6c4
commit b249bf9962
4 changed files with 156 additions and 47 deletions

View file

@ -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?

View file

@ -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,
}

View file

@ -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:

View file

@ -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{}