Skip to content

core: initialize history pruning in BlockChain #31636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/geth/chaincmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"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"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/era"
Expand Down Expand Up @@ -625,7 +625,7 @@ func pruneHistory(ctx *cli.Context) error {
defer chain.Stop()

// Determine the prune point. This will be the first PoS block.
prunePoint, ok := ethconfig.HistoryPrunePoints[chain.Genesis().Hash()]
prunePoint, ok := history.PrunePoints[chain.Genesis().Hash()]
if !ok || prunePoint == nil {
return errors.New("prune point not found")
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/workload/testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"os"
"slices"

"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/core/history"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/utesting"
"github.com/ethereum/go-ethereum/log"
Expand Down Expand Up @@ -124,13 +124,13 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
cfg.filterQueryFile = "queries/filter_queries_mainnet.json"
cfg.historyTestFile = "queries/history_mainnet.json"
cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = ethconfig.HistoryPrunePoints[params.MainnetGenesisHash].BlockNumber
*cfg.historyPruneBlock = history.PrunePoints[params.MainnetGenesisHash].BlockNumber
case ctx.Bool(testSepoliaFlag.Name):
cfg.fsys = builtinTestFiles
cfg.filterQueryFile = "queries/filter_queries_sepolia.json"
cfg.historyTestFile = "queries/history_sepolia.json"
cfg.historyPruneBlock = new(uint64)
*cfg.historyPruneBlock = ethconfig.HistoryPrunePoints[params.SepoliaGenesisHash].BlockNumber
*cfg.historyPruneBlock = history.PrunePoints[params.SepoliaGenesisHash].BlockNumber
default:
cfg.fsys = os.DirFS(".")
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)
Expand Down
10 changes: 8 additions & 2 deletions core/block_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ func testHeaderVerification(t *testing.T, scheme string) {
headers[i] = block.Header()
}
// Run the header checker for blocks one-by-one, checking for both valid and invalid nonces
chain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil)
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil)
defer chain.Stop()
if err != nil {
t.Fatal(err)
}

for i := 0; i < len(blocks); i++ {
for j, valid := range []bool{true, false} {
Expand Down Expand Up @@ -163,8 +166,11 @@ func testHeaderVerificationForMerging(t *testing.T, isClique bool) {
postHeaders[i] = block.Header()
}
// Run the header checker for blocks one-by-one, checking for both valid and invalid nonces
chain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{}, nil)
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{}, nil)
defer chain.Stop()
if err != nil {
t.Fatal(err)
}

// Verify the blocks before the merging
for i := 0; i < len(preBlocks); i++ {
Expand Down
64 changes: 61 additions & 3 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/common/prque"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"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/state/snapshot"
Expand Down Expand Up @@ -158,8 +159,7 @@ type CacheConfig struct {

// This defines the cutoff block for history expiry.
// Blocks before this number may be unavailable in the chain database.
HistoryPruningCutoffNumber uint64
HistoryPruningCutoffHash common.Hash
ChainHistoryMode history.HistoryMode
}

// triedbConfig derives the configures for trie database.
Expand Down Expand Up @@ -255,6 +255,7 @@ type BlockChain struct {
currentSnapBlock atomic.Pointer[types.Header] // Current head of snap-sync
currentFinalBlock atomic.Pointer[types.Header] // Latest (consensus) finalized block
currentSafeBlock atomic.Pointer[types.Header] // Latest (consensus) safe block
historyPrunePoint atomic.Pointer[history.PrunePoint]

bodyCache *lru.Cache[common.Hash, *types.Body]
bodyRLPCache *lru.Cache[common.Hash, rlp.RawValue]
Expand Down Expand Up @@ -533,6 +534,12 @@ func (bc *BlockChain) loadLastState() error {
}
bc.hc.SetCurrentHeader(headHeader)

// Initialize history pruning.
latest := max(headBlock.NumberU64(), headHeader.Number.Uint64())
if err := bc.initializeHistoryPruning(latest); err != nil {
return err
}

// Restore the last known head snap block
bc.currentSnapBlock.Store(headBlock.Header())
headFastBlockGauge.Update(int64(headBlock.NumberU64()))
Expand All @@ -555,6 +562,7 @@ func (bc *BlockChain) loadLastState() error {
headSafeBlockGauge.Update(int64(block.NumberU64()))
}
}

// Issue a status log for the user
var (
currentSnapBlock = bc.CurrentSnapBlock()
Expand All @@ -573,9 +581,57 @@ func (bc *BlockChain) loadLastState() error {
if pivot := rawdb.ReadLastPivotNumber(bc.db); pivot != nil {
log.Info("Loaded last snap-sync pivot marker", "number", *pivot)
}
if pruning := bc.historyPrunePoint.Load(); pruning != nil {
log.Info("Chain history is pruned", "earliest", pruning.BlockNumber, "hash", pruning.BlockHash)
}
return nil
}

// initializeHistoryPruning sets bc.historyPrunePoint.
func (bc *BlockChain) initializeHistoryPruning(latest uint64) error {
freezerTail, _ := bc.db.Tail()

switch bc.cacheConfig.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.
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 fmt.Errorf("unexpected database tail")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have a warning log here, saying that the Geth is run with KeepAll but it was pruned previously

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's something to consider, but for now, we really don't want people to use another pruning target block.

bc.historyPrunePoint.Store(predefinedPoint)
return nil

case history.KeepPostMerge:
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.cacheConfig.ChainHistoryMode.String()))
log.Error(fmt.Sprintf("Run 'geth prune-history' to prune pre-merge history."))
return fmt.Errorf("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())
return fmt.Errorf("history pruning requested for unknown network")
} else if freezerTail != predefinedPoint.BlockNumber {
log.Error("Chain history database is pruned to unknown block", "tail", freezerTail)
return fmt.Errorf("unexpected database tail")
}
bc.historyPrunePoint.Store(predefinedPoint)
return nil

default:
return fmt.Errorf("invalid history mode: %d", bc.cacheConfig.ChainHistoryMode)
}
}

// SetHead rewinds the local chain to a new head. Depending on whether the node
// was snap synced or full synced and in which state, the method will try to
// delete minimal data from disk whilst retaining chain consistency.
Expand Down Expand Up @@ -1014,7 +1070,9 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error {
bc.hc.SetCurrentHeader(bc.genesisBlock.Header())
bc.currentSnapBlock.Store(bc.genesisBlock.Header())
headFastBlockGauge.Update(int64(bc.genesisBlock.NumberU64()))
return nil

// Reset history pruning status.
return bc.initializeHistoryPruning(0)
}

// Export writes the active chain to the given writer.
Expand Down
6 changes: 5 additions & 1 deletion core/blockchain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,11 @@ func (bc *BlockChain) TxIndexProgress() (TxIndexProgress, error) {
// HistoryPruningCutoff returns the configured history pruning point.
// Blocks before this might not be available in the database.
func (bc *BlockChain) HistoryPruningCutoff() (uint64, common.Hash) {
return bc.cacheConfig.HistoryPruningCutoffNumber, bc.cacheConfig.HistoryPruningCutoffHash
pt := bc.historyPrunePoint.Load()
if pt == nil {
return 0, bc.genesisBlock.Hash()
}
return pt.BlockNumber, pt.BlockHash
}

// TrieDB retrieves the low level trie database used for data storage.
Expand Down
54 changes: 38 additions & 16 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ 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"
Expand Down Expand Up @@ -4257,13 +4258,7 @@ func testChainReorgSnapSync(t *testing.T, ancientLimit uint64) {
// be persisted without the receipts and bodies; chain after should be persisted
// normally.
func TestInsertChainWithCutoff(t *testing.T) {
testInsertChainWithCutoff(t, 32, 32) // cutoff = 32, ancientLimit = 32
testInsertChainWithCutoff(t, 32, 64) // cutoff = 32, ancientLimit = 64 (entire chain in ancient)
testInsertChainWithCutoff(t, 32, 65) // cutoff = 32, ancientLimit = 65 (64 blocks in ancient, 1 block in live)
}

func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64) {
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
const chainLength = 64

// Configure and generate a sample block chain
var (
Expand All @@ -4278,24 +4273,51 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64)
signer = types.LatestSigner(gspec.Config)
engine = beacon.New(ethash.NewFaker())
)
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, int(2*cutoff), func(i int, block *BlockGen) {
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, chainLength, func(i int, block *BlockGen) {
block.SetCoinbase(common.Address{0x00})

tx, err := types.SignTx(types.NewTransaction(block.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, block.header.BaseFee, nil), signer, key)
if err != nil {
panic(err)
}
block.AddTx(tx)
})
db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
defer db.Close()

// Run the actual tests.
t.Run("cutoff-32/ancientLimit-32", func(t *testing.T) {
// cutoff = 32, ancientLimit = 32
testInsertChainWithCutoff(t, 32, 32, gspec, blocks, receipts)
})
t.Run("cutoff-32/ancientLimit-64", func(t *testing.T) {
// cutoff = 32, ancientLimit = 64 (entire chain in ancient)
testInsertChainWithCutoff(t, 32, 64, gspec, blocks, receipts)
})
t.Run("cutoff-32/ancientLimit-64", func(t *testing.T) {
// cutoff = 32, ancientLimit = 65 (64 blocks in ancient, 1 block in live)
testInsertChainWithCutoff(t, 32, 65, gspec, blocks, receipts)
})
}

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 := DefaultCacheConfigWithScheme(rawdb.PathScheme)
config.HistoryPruningCutoffNumber = cutoffBlock.NumberU64()
config.HistoryPruningCutoffHash = cutoffBlock.Hash()
config.ChainHistoryMode = history.KeepPostMerge

chain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.PathScheme), gspec, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil)
db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
defer db.Close()
chain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.PathScheme), genesis, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil)
defer chain.Stop()

var (
Expand Down Expand Up @@ -4326,8 +4348,8 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64)
t.Errorf("head header #%d: header mismatch: want: %v, got: %v", headHeader.Number, blocks[len(blocks)-1].Hash(), headHeader.Hash())
}
headBlock := chain.CurrentBlock()
if headBlock.Hash() != gspec.ToBlock().Hash() {
t.Errorf("head block #%d: header mismatch: want: %v, got: %v", headBlock.Number, gspec.ToBlock().Hash(), headBlock.Hash())
if headBlock.Hash() != ghash {
t.Errorf("head block #%d: header mismatch: want: %v, got: %v", headBlock.Number, ghash, headBlock.Hash())
}

// Iterate over all chain data components, and cross reference
Expand Down
28 changes: 14 additions & 14 deletions eth/ethconfig/historymode.go → core/history/historymode.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package ethconfig
package history

import (
"fmt"
Expand All @@ -27,22 +27,22 @@ import (
type HistoryMode uint32

const (
// AllHistory (default) means that all chain history down to genesis block will be kept.
AllHistory HistoryMode = iota
// KeepAll (default) means that all chain history down to genesis block will be kept.
KeepAll HistoryMode = iota

// PostMergeHistory sets the history pruning point to the merge activation block.
PostMergeHistory
// KeepPostMerge sets the history pruning point to the merge activation block.
KeepPostMerge
)

func (m HistoryMode) IsValid() bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick, i would prefer to rename it to Mode. Combined with the package name it's history.Mode

return m <= PostMergeHistory
return m <= KeepPostMerge
}

func (m HistoryMode) String() string {
switch m {
case AllHistory:
case KeepAll:
return "all"
case PostMergeHistory:
case KeepPostMerge:
return "postmerge"
default:
return fmt.Sprintf("invalid HistoryMode(%d)", m)
Expand All @@ -61,24 +61,24 @@ func (m HistoryMode) MarshalText() ([]byte, error) {
func (m *HistoryMode) UnmarshalText(text []byte) error {
switch string(text) {
case "all":
*m = AllHistory
*m = KeepAll
case "postmerge":
*m = PostMergeHistory
*m = KeepPostMerge
default:
return fmt.Errorf(`unknown sync mode %q, want "all" or "postmerge"`, text)
}
return nil
}

type HistoryPrunePoint struct {
type PrunePoint struct {
BlockNumber uint64
BlockHash common.Hash
}

// HistoryPrunePoints contains the pre-defined history pruning cutoff blocks for known networks.
// PrunePointsins the pre-defined history pruning cutoff blocks for known networks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix the typo

// They point to the first post-merge block. Any pruning should truncate *up to* but excluding
// given block.
var HistoryPrunePoints = map[common.Hash]*HistoryPrunePoint{
var PrunePoints = map[common.Hash]*PrunePoint{
// mainnet
params.MainnetGenesisHash: {
BlockNumber: 15537393,
Expand All @@ -91,7 +91,7 @@ var HistoryPrunePoints = map[common.Hash]*HistoryPrunePoint{
},
}

// PrunedHistoryError is returned when the requested history is pruned.
// PrunedHistoryError is returned by APIs when the requested history is pruned.
type PrunedHistoryError struct{}

func (e *PrunedHistoryError) Error() string { return "pruned history unavailable" }
Expand Down
Loading