Skip to content

Commit 7e99cca

Browse files
fjlqianhh
authored andcommitted
core: initialize history pruning in BlockChain (#31636)
I added the history mode configuration in eth/ethconfig initially, since it seemed like the logical place. But it turns out we need access to the intended pruning setting at a deeper level, and it actually needs to be integrated with the blockchain startup procedure. With this change applied, if a node previously had its history pruned, and is subsequently restarted **without** the `--history.chain postmerge` flag, the `BlockChain` initialization code will now verify the freezer tail against the known pruning point of the predefined network and will restore pruning status. Note that this logic is quite restrictive, we allow non-zero tail only for known networks, and only for the specific pruning point that is defined.
1 parent 262e7b1 commit 7e99cca

File tree

11 files changed

+177
-79
lines changed

11 files changed

+177
-79
lines changed

cmd/geth/chaincmd.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ import (
3131
"github.com/ethereum/go-ethereum/common"
3232
"github.com/ethereum/go-ethereum/common/hexutil"
3333
"github.com/ethereum/go-ethereum/core"
34+
"github.com/ethereum/go-ethereum/core/history"
3435
"github.com/ethereum/go-ethereum/core/rawdb"
3536
"github.com/ethereum/go-ethereum/core/state"
3637
"github.com/ethereum/go-ethereum/core/types"
3738
"github.com/ethereum/go-ethereum/crypto"
38-
"github.com/ethereum/go-ethereum/eth/ethconfig"
3939
"github.com/ethereum/go-ethereum/ethdb"
4040
"github.com/ethereum/go-ethereum/internal/debug"
4141
"github.com/ethereum/go-ethereum/internal/era"
@@ -625,7 +625,7 @@ func pruneHistory(ctx *cli.Context) error {
625625
defer chain.Stop()
626626

627627
// Determine the prune point. This will be the first PoS block.
628-
prunePoint, ok := ethconfig.HistoryPrunePoints[chain.Genesis().Hash()]
628+
prunePoint, ok := history.PrunePoints[chain.Genesis().Hash()]
629629
if !ok || prunePoint == nil {
630630
return errors.New("prune point not found")
631631
}

cmd/workload/testsuite.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"os"
2424
"slices"
2525

26-
"github.com/ethereum/go-ethereum/eth/ethconfig"
26+
"github.com/ethereum/go-ethereum/core/history"
2727
"github.com/ethereum/go-ethereum/internal/flags"
2828
"github.com/ethereum/go-ethereum/internal/utesting"
2929
"github.com/ethereum/go-ethereum/log"
@@ -124,13 +124,13 @@ func testConfigFromCLI(ctx *cli.Context) (cfg testConfig) {
124124
cfg.filterQueryFile = "queries/filter_queries_mainnet.json"
125125
cfg.historyTestFile = "queries/history_mainnet.json"
126126
cfg.historyPruneBlock = new(uint64)
127-
*cfg.historyPruneBlock = ethconfig.HistoryPrunePoints[params.MainnetGenesisHash].BlockNumber
127+
*cfg.historyPruneBlock = history.PrunePoints[params.MainnetGenesisHash].BlockNumber
128128
case ctx.Bool(testSepoliaFlag.Name):
129129
cfg.fsys = builtinTestFiles
130130
cfg.filterQueryFile = "queries/filter_queries_sepolia.json"
131131
cfg.historyTestFile = "queries/history_sepolia.json"
132132
cfg.historyPruneBlock = new(uint64)
133-
*cfg.historyPruneBlock = ethconfig.HistoryPrunePoints[params.SepoliaGenesisHash].BlockNumber
133+
*cfg.historyPruneBlock = history.PrunePoints[params.SepoliaGenesisHash].BlockNumber
134134
default:
135135
cfg.fsys = os.DirFS(".")
136136
cfg.filterQueryFile = ctx.String(filterQueryFileFlag.Name)

core/block_validator_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ func testHeaderVerification(t *testing.T, scheme string) {
5050
headers[i] = block.Header()
5151
}
5252
// Run the header checker for blocks one-by-one, checking for both valid and invalid nonces
53-
chain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
53+
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil)
5454
defer chain.Stop()
55+
if err != nil {
56+
t.Fatal(err)
57+
}
5558

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

169175
// Verify the blocks before the merging
170176
for i := 0; i < len(preBlocks); i++ {

core/blockchain.go

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/ethereum/go-ethereum/consensus"
3838
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
3939
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
40+
"github.com/ethereum/go-ethereum/core/history"
4041
"github.com/ethereum/go-ethereum/core/rawdb"
4142
"github.com/ethereum/go-ethereum/core/state"
4243
"github.com/ethereum/go-ethereum/core/state/snapshot"
@@ -160,8 +161,7 @@ type CacheConfig struct {
160161

161162
// This defines the cutoff block for history expiry.
162163
// Blocks before this number may be unavailable in the chain database.
163-
HistoryPruningCutoffNumber uint64
164-
HistoryPruningCutoffHash common.Hash
164+
ChainHistoryMode history.HistoryMode
165165
}
166166

167167
// triedbConfig derives the configures for trie database.
@@ -258,6 +258,7 @@ type BlockChain struct {
258258
currentSnapBlock atomic.Pointer[types.Header] // Current head of snap-sync
259259
currentFinalBlock atomic.Pointer[types.Header] // Latest (consensus) finalized block
260260
currentSafeBlock atomic.Pointer[types.Header] // Latest (consensus) safe block
261+
historyPrunePoint atomic.Pointer[history.PrunePoint]
261262

262263
bodyCache *lru.Cache[common.Hash, *types.Body]
263264
bodyRLPCache *lru.Cache[common.Hash, rlp.RawValue]
@@ -558,6 +559,12 @@ func (bc *BlockChain) loadLastState() error {
558559
}
559560
bc.hc.SetCurrentHeader(headHeader)
560561

562+
// Initialize history pruning.
563+
latest := max(headBlock.NumberU64(), headHeader.Number.Uint64())
564+
if err := bc.initializeHistoryPruning(latest); err != nil {
565+
return err
566+
}
567+
561568
// Restore the last known head snap block
562569
bc.currentSnapBlock.Store(headBlock.Header())
563570
headFastBlockGauge.Update(int64(headBlock.NumberU64()))
@@ -580,6 +587,7 @@ func (bc *BlockChain) loadLastState() error {
580587
headSafeBlockGauge.Update(int64(block.NumberU64()))
581588
}
582589
}
590+
583591
// Issue a status log for the user
584592
var (
585593
currentSnapBlock = bc.CurrentSnapBlock()
@@ -607,9 +615,57 @@ func (bc *BlockChain) loadLastState() error {
607615
if pivot := rawdb.ReadLastPivotNumber(bc.db); pivot != nil {
608616
log.Info("Loaded last snap-sync pivot marker", "number", *pivot)
609617
}
618+
if pruning := bc.historyPrunePoint.Load(); pruning != nil {
619+
log.Info("Chain history is pruned", "earliest", pruning.BlockNumber, "hash", pruning.BlockHash)
620+
}
610621
return nil
611622
}
612623

624+
// initializeHistoryPruning sets bc.historyPrunePoint.
625+
func (bc *BlockChain) initializeHistoryPruning(latest uint64) error {
626+
freezerTail, _ := bc.db.Tail()
627+
628+
switch bc.cacheConfig.ChainHistoryMode {
629+
case history.KeepAll:
630+
if freezerTail == 0 {
631+
return nil
632+
}
633+
// The database was pruned somehow, so we need to figure out if it's a known
634+
// configuration or an error.
635+
predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()]
636+
if predefinedPoint == nil || freezerTail != predefinedPoint.BlockNumber {
637+
log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail)
638+
return fmt.Errorf("unexpected database tail")
639+
}
640+
bc.historyPrunePoint.Store(predefinedPoint)
641+
return nil
642+
643+
case history.KeepPostMerge:
644+
if freezerTail == 0 && latest != 0 {
645+
// This is the case where a user is trying to run with --history.chain
646+
// postmerge directly on an existing DB. We could just trigger the pruning
647+
// here, but it'd be a bit dangerous since they may not have intended this
648+
// action to happen. So just tell them how to do it.
649+
log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cacheConfig.ChainHistoryMode.String()))
650+
log.Error(fmt.Sprintf("Run 'geth prune-history' to prune pre-merge history."))
651+
return fmt.Errorf("history pruning requested via configuration")
652+
}
653+
predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()]
654+
if predefinedPoint == nil {
655+
log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash())
656+
return fmt.Errorf("history pruning requested for unknown network")
657+
} else if freezerTail != predefinedPoint.BlockNumber {
658+
log.Error("Chain history database is pruned to unknown block", "tail", freezerTail)
659+
return fmt.Errorf("unexpected database tail")
660+
}
661+
bc.historyPrunePoint.Store(predefinedPoint)
662+
return nil
663+
664+
default:
665+
return fmt.Errorf("invalid history mode: %d", bc.cacheConfig.ChainHistoryMode)
666+
}
667+
}
668+
613669
// SetHead rewinds the local chain to a new head. Depending on whether the node
614670
// was snap synced or full synced and in which state, the method will try to
615671
// delete minimal data from disk whilst retaining chain consistency.
@@ -1050,7 +1106,9 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error {
10501106
bc.hc.SetCurrentHeader(bc.genesisBlock.Header())
10511107
bc.currentSnapBlock.Store(bc.genesisBlock.Header())
10521108
headFastBlockGauge.Update(int64(bc.genesisBlock.NumberU64()))
1053-
return nil
1109+
1110+
// Reset history pruning status.
1111+
return bc.initializeHistoryPruning(0)
10541112
}
10551113

10561114
// Export writes the active chain to the given writer.

core/blockchain_reader.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,11 @@ func (bc *BlockChain) TxIndexProgress() (TxIndexProgress, error) {
433433
// HistoryPruningCutoff returns the configured history pruning point.
434434
// Blocks before this might not be available in the database.
435435
func (bc *BlockChain) HistoryPruningCutoff() (uint64, common.Hash) {
436-
return bc.cacheConfig.HistoryPruningCutoffNumber, bc.cacheConfig.HistoryPruningCutoffHash
436+
pt := bc.historyPrunePoint.Load()
437+
if pt == nil {
438+
return 0, bc.genesisBlock.Hash()
439+
}
440+
return pt.BlockNumber, pt.BlockHash
437441
}
438442

439443
// TrieDB retrieves the low level trie database used for data storage.

core/blockchain_test.go

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/ethereum/go-ethereum/consensus"
3434
"github.com/ethereum/go-ethereum/consensus/beacon"
3535
"github.com/ethereum/go-ethereum/consensus/ethash"
36+
"github.com/ethereum/go-ethereum/core/history"
3637
"github.com/ethereum/go-ethereum/core/rawdb"
3738
"github.com/ethereum/go-ethereum/core/state"
3839
"github.com/ethereum/go-ethereum/core/types"
@@ -4312,13 +4313,7 @@ func testChainReorgSnapSync(t *testing.T, ancientLimit uint64) {
43124313
// be persisted without the receipts and bodies; chain after should be persisted
43134314
// normally.
43144315
func TestInsertChainWithCutoff(t *testing.T) {
4315-
testInsertChainWithCutoff(t, 32, 32) // cutoff = 32, ancientLimit = 32
4316-
testInsertChainWithCutoff(t, 32, 64) // cutoff = 32, ancientLimit = 64 (entire chain in ancient)
4317-
testInsertChainWithCutoff(t, 32, 65) // cutoff = 32, ancientLimit = 65 (64 blocks in ancient, 1 block in live)
4318-
}
4319-
4320-
func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64) {
4321-
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
4316+
const chainLength = 64
43224317

43234318
// Configure and generate a sample block chain
43244319
var (
@@ -4333,24 +4328,51 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64)
43334328
signer = types.LatestSigner(gspec.Config)
43344329
engine = beacon.New(ethash.NewFaker())
43354330
)
4336-
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, int(2*cutoff), func(i int, block *BlockGen) {
4331+
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, chainLength, func(i int, block *BlockGen) {
43374332
block.SetCoinbase(common.Address{0x00})
4338-
43394333
tx, err := types.SignTx(types.NewTransaction(block.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, block.header.BaseFee, nil), signer, key)
43404334
if err != nil {
43414335
panic(err)
43424336
}
43434337
block.AddTx(tx)
43444338
})
4345-
db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
4346-
defer db.Close()
43474339

4340+
// Run the actual tests.
4341+
t.Run("cutoff-32/ancientLimit-32", func(t *testing.T) {
4342+
// cutoff = 32, ancientLimit = 32
4343+
testInsertChainWithCutoff(t, 32, 32, gspec, blocks, receipts)
4344+
})
4345+
t.Run("cutoff-32/ancientLimit-64", func(t *testing.T) {
4346+
// cutoff = 32, ancientLimit = 64 (entire chain in ancient)
4347+
testInsertChainWithCutoff(t, 32, 64, gspec, blocks, receipts)
4348+
})
4349+
t.Run("cutoff-32/ancientLimit-64", func(t *testing.T) {
4350+
// cutoff = 32, ancientLimit = 65 (64 blocks in ancient, 1 block in live)
4351+
testInsertChainWithCutoff(t, 32, 65, gspec, blocks, receipts)
4352+
})
4353+
}
4354+
4355+
func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, genesis *Genesis, blocks []*types.Block, receipts []types.Receipts) {
4356+
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
4357+
4358+
// Add a known pruning point for the duration of the test.
4359+
ghash := genesis.ToBlock().Hash()
43484360
cutoffBlock := blocks[cutoff-1]
4361+
history.PrunePoints[ghash] = &history.PrunePoint{
4362+
BlockNumber: cutoffBlock.NumberU64(),
4363+
BlockHash: cutoffBlock.Hash(),
4364+
}
4365+
defer func() {
4366+
delete(history.PrunePoints, ghash)
4367+
}()
4368+
4369+
// Enable pruning in cache config.
43494370
config := DefaultCacheConfigWithScheme(rawdb.PathScheme)
4350-
config.HistoryPruningCutoffNumber = cutoffBlock.NumberU64()
4351-
config.HistoryPruningCutoffHash = cutoffBlock.Hash()
4371+
config.ChainHistoryMode = history.KeepPostMerge
43524372

4353-
chain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.PathScheme), gspec, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil, nil)
4373+
db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
4374+
defer db.Close()
4375+
chain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.PathScheme), genesis, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil, nil)
43544376
defer chain.Stop()
43554377

43564378
var (
@@ -4381,8 +4403,8 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64)
43814403
t.Errorf("head header #%d: header mismatch: want: %v, got: %v", headHeader.Number, blocks[len(blocks)-1].Hash(), headHeader.Hash())
43824404
}
43834405
headBlock := chain.CurrentBlock()
4384-
if headBlock.Hash() != gspec.ToBlock().Hash() {
4385-
t.Errorf("head block #%d: header mismatch: want: %v, got: %v", headBlock.Number, gspec.ToBlock().Hash(), headBlock.Hash())
4406+
if headBlock.Hash() != ghash {
4407+
t.Errorf("head block #%d: header mismatch: want: %v, got: %v", headBlock.Number, ghash, headBlock.Hash())
43864408
}
43874409

43884410
// Iterate over all chain data components, and cross reference

eth/ethconfig/historymode.go renamed to core/history/historymode.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// You should have received a copy of the GNU Lesser General Public License
1515
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
1616

17-
package ethconfig
17+
package history
1818

1919
import (
2020
"fmt"
@@ -27,22 +27,22 @@ import (
2727
type HistoryMode uint32
2828

2929
const (
30-
// AllHistory (default) means that all chain history down to genesis block will be kept.
31-
AllHistory HistoryMode = iota
30+
// KeepAll (default) means that all chain history down to genesis block will be kept.
31+
KeepAll HistoryMode = iota
3232

33-
// PostMergeHistory sets the history pruning point to the merge activation block.
34-
PostMergeHistory
33+
// KeepPostMerge sets the history pruning point to the merge activation block.
34+
KeepPostMerge
3535
)
3636

3737
func (m HistoryMode) IsValid() bool {
38-
return m <= PostMergeHistory
38+
return m <= KeepPostMerge
3939
}
4040

4141
func (m HistoryMode) String() string {
4242
switch m {
43-
case AllHistory:
43+
case KeepAll:
4444
return "all"
45-
case PostMergeHistory:
45+
case KeepPostMerge:
4646
return "postmerge"
4747
default:
4848
return fmt.Sprintf("invalid HistoryMode(%d)", m)
@@ -61,24 +61,24 @@ func (m HistoryMode) MarshalText() ([]byte, error) {
6161
func (m *HistoryMode) UnmarshalText(text []byte) error {
6262
switch string(text) {
6363
case "all":
64-
*m = AllHistory
64+
*m = KeepAll
6565
case "postmerge":
66-
*m = PostMergeHistory
66+
*m = KeepPostMerge
6767
default:
6868
return fmt.Errorf(`unknown sync mode %q, want "all" or "postmerge"`, text)
6969
}
7070
return nil
7171
}
7272

73-
type HistoryPrunePoint struct {
73+
type PrunePoint struct {
7474
BlockNumber uint64
7575
BlockHash common.Hash
7676
}
7777

78-
// HistoryPrunePoints contains the pre-defined history pruning cutoff blocks for known networks.
78+
// PrunePoints the pre-defined history pruning cutoff blocks for known networks.
7979
// They point to the first post-merge block. Any pruning should truncate *up to* but excluding
8080
// given block.
81-
var HistoryPrunePoints = map[common.Hash]*HistoryPrunePoint{
81+
var PrunePoints = map[common.Hash]*PrunePoint{
8282
// mainnet
8383
params.MainnetGenesisHash: {
8484
BlockNumber: 15537393,
@@ -91,7 +91,7 @@ var HistoryPrunePoints = map[common.Hash]*HistoryPrunePoint{
9191
},
9292
}
9393

94-
// PrunedHistoryError is returned when the requested history is pruned.
94+
// PrunedHistoryError is returned by APIs when the requested history is pruned.
9595
type PrunedHistoryError struct{}
9696

9797
func (e *PrunedHistoryError) Error() string { return "pruned history unavailable" }

0 commit comments

Comments
 (0)