Skip to content

Commit f65d08f

Browse files
fjl0g-wh
authored andcommitted
core: initialize history pruning in BlockChain (ethereum#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 fd2d83a commit f65d08f

File tree

14 files changed

+186
-88
lines changed

14 files changed

+186
-88
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)
53+
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, 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)
169+
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{}, 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
@@ -36,6 +36,7 @@ import (
3636
"github.com/ethereum/go-ethereum/common/prque"
3737
"github.com/ethereum/go-ethereum/consensus"
3838
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
39+
"github.com/ethereum/go-ethereum/core/history"
3940
"github.com/ethereum/go-ethereum/core/rawdb"
4041
"github.com/ethereum/go-ethereum/core/state"
4142
"github.com/ethereum/go-ethereum/core/state/snapshot"
@@ -158,8 +159,7 @@ type CacheConfig struct {
158159

159160
// This defines the cutoff block for history expiry.
160161
// Blocks before this number may be unavailable in the chain database.
161-
HistoryPruningCutoffNumber uint64
162-
HistoryPruningCutoffHash common.Hash
162+
ChainHistoryMode history.HistoryMode
163163
}
164164

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

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

537+
// Initialize history pruning.
538+
latest := max(headBlock.NumberU64(), headHeader.Number.Uint64())
539+
if err := bc.initializeHistoryPruning(latest); err != nil {
540+
return err
541+
}
542+
536543
// Restore the last known head snap block
537544
bc.currentSnapBlock.Store(headBlock.Header())
538545
headFastBlockGauge.Update(int64(headBlock.NumberU64()))
@@ -555,6 +562,7 @@ func (bc *BlockChain) loadLastState() error {
555562
headSafeBlockGauge.Update(int64(block.NumberU64()))
556563
}
557564
}
565+
558566
// Issue a status log for the user
559567
var (
560568
currentSnapBlock = bc.CurrentSnapBlock()
@@ -573,9 +581,57 @@ func (bc *BlockChain) loadLastState() error {
573581
if pivot := rawdb.ReadLastPivotNumber(bc.db); pivot != nil {
574582
log.Info("Loaded last snap-sync pivot marker", "number", *pivot)
575583
}
584+
if pruning := bc.historyPrunePoint.Load(); pruning != nil {
585+
log.Info("Chain history is pruned", "earliest", pruning.BlockNumber, "hash", pruning.BlockHash)
586+
}
576587
return nil
577588
}
578589

590+
// initializeHistoryPruning sets bc.historyPrunePoint.
591+
func (bc *BlockChain) initializeHistoryPruning(latest uint64) error {
592+
freezerTail, _ := bc.db.Tail()
593+
594+
switch bc.cacheConfig.ChainHistoryMode {
595+
case history.KeepAll:
596+
if freezerTail == 0 {
597+
return nil
598+
}
599+
// The database was pruned somehow, so we need to figure out if it's a known
600+
// configuration or an error.
601+
predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()]
602+
if predefinedPoint == nil || freezerTail != predefinedPoint.BlockNumber {
603+
log.Error("Chain history database is pruned with unknown configuration", "tail", freezerTail)
604+
return fmt.Errorf("unexpected database tail")
605+
}
606+
bc.historyPrunePoint.Store(predefinedPoint)
607+
return nil
608+
609+
case history.KeepPostMerge:
610+
if freezerTail == 0 && latest != 0 {
611+
// This is the case where a user is trying to run with --history.chain
612+
// postmerge directly on an existing DB. We could just trigger the pruning
613+
// here, but it'd be a bit dangerous since they may not have intended this
614+
// action to happen. So just tell them how to do it.
615+
log.Error(fmt.Sprintf("Chain history mode is configured as %q, but database is not pruned.", bc.cacheConfig.ChainHistoryMode.String()))
616+
log.Error(fmt.Sprintf("Run 'geth prune-history' to prune pre-merge history."))
617+
return fmt.Errorf("history pruning requested via configuration")
618+
}
619+
predefinedPoint := history.PrunePoints[bc.genesisBlock.Hash()]
620+
if predefinedPoint == nil {
621+
log.Error("Chain history pruning is not supported for this network", "genesis", bc.genesisBlock.Hash())
622+
return fmt.Errorf("history pruning requested for unknown network")
623+
} else if freezerTail != predefinedPoint.BlockNumber {
624+
log.Error("Chain history database is pruned to unknown block", "tail", freezerTail)
625+
return fmt.Errorf("unexpected database tail")
626+
}
627+
bc.historyPrunePoint.Store(predefinedPoint)
628+
return nil
629+
630+
default:
631+
return fmt.Errorf("invalid history mode: %d", bc.cacheConfig.ChainHistoryMode)
632+
}
633+
}
634+
579635
// SetHead rewinds the local chain to a new head. Depending on whether the node
580636
// was snap synced or full synced and in which state, the method will try to
581637
// delete minimal data from disk whilst retaining chain consistency.
@@ -1014,7 +1070,9 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error {
10141070
bc.hc.SetCurrentHeader(bc.genesisBlock.Header())
10151071
bc.currentSnapBlock.Store(bc.genesisBlock.Header())
10161072
headFastBlockGauge.Update(int64(bc.genesisBlock.NumberU64()))
1017-
return nil
1073+
1074+
// Reset history pruning status.
1075+
return bc.initializeHistoryPruning(0)
10181076
}
10191077

10201078
// 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
@@ -410,7 +410,11 @@ func (bc *BlockChain) TxIndexProgress() (TxIndexProgress, error) {
410410
// HistoryPruningCutoff returns the configured history pruning point.
411411
// Blocks before this might not be available in the database.
412412
func (bc *BlockChain) HistoryPruningCutoff() (uint64, common.Hash) {
413-
return bc.cacheConfig.HistoryPruningCutoffNumber, bc.cacheConfig.HistoryPruningCutoffHash
413+
pt := bc.historyPrunePoint.Load()
414+
if pt == nil {
415+
return 0, bc.genesisBlock.Hash()
416+
}
417+
return pt.BlockNumber, pt.BlockHash
414418
}
415419

416420
// 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"
@@ -4257,13 +4258,7 @@ func testChainReorgSnapSync(t *testing.T, ancientLimit uint64) {
42574258
// be persisted without the receipts and bodies; chain after should be persisted
42584259
// normally.
42594260
func TestInsertChainWithCutoff(t *testing.T) {
4260-
testInsertChainWithCutoff(t, 32, 32) // cutoff = 32, ancientLimit = 32
4261-
testInsertChainWithCutoff(t, 32, 64) // cutoff = 32, ancientLimit = 64 (entire chain in ancient)
4262-
testInsertChainWithCutoff(t, 32, 65) // cutoff = 32, ancientLimit = 65 (64 blocks in ancient, 1 block in live)
4263-
}
4264-
4265-
func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64) {
4266-
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
4261+
const chainLength = 64
42674262

42684263
// Configure and generate a sample block chain
42694264
var (
@@ -4278,24 +4273,51 @@ func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64)
42784273
signer = types.LatestSigner(gspec.Config)
42794274
engine = beacon.New(ethash.NewFaker())
42804275
)
4281-
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, int(2*cutoff), func(i int, block *BlockGen) {
4276+
_, blocks, receipts := GenerateChainWithGenesis(gspec, engine, chainLength, func(i int, block *BlockGen) {
42824277
block.SetCoinbase(common.Address{0x00})
4283-
42844278
tx, err := types.SignTx(types.NewTransaction(block.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, block.header.BaseFee, nil), signer, key)
42854279
if err != nil {
42864280
panic(err)
42874281
}
42884282
block.AddTx(tx)
42894283
})
4290-
db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
4291-
defer db.Close()
42924284

4285+
// Run the actual tests.
4286+
t.Run("cutoff-32/ancientLimit-32", func(t *testing.T) {
4287+
// cutoff = 32, ancientLimit = 32
4288+
testInsertChainWithCutoff(t, 32, 32, gspec, blocks, receipts)
4289+
})
4290+
t.Run("cutoff-32/ancientLimit-64", func(t *testing.T) {
4291+
// cutoff = 32, ancientLimit = 64 (entire chain in ancient)
4292+
testInsertChainWithCutoff(t, 32, 64, gspec, blocks, receipts)
4293+
})
4294+
t.Run("cutoff-32/ancientLimit-64", func(t *testing.T) {
4295+
// cutoff = 32, ancientLimit = 65 (64 blocks in ancient, 1 block in live)
4296+
testInsertChainWithCutoff(t, 32, 65, gspec, blocks, receipts)
4297+
})
4298+
}
4299+
4300+
func testInsertChainWithCutoff(t *testing.T, cutoff uint64, ancientLimit uint64, genesis *Genesis, blocks []*types.Block, receipts []types.Receipts) {
4301+
// log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
4302+
4303+
// Add a known pruning point for the duration of the test.
4304+
ghash := genesis.ToBlock().Hash()
42934305
cutoffBlock := blocks[cutoff-1]
4306+
history.PrunePoints[ghash] = &history.PrunePoint{
4307+
BlockNumber: cutoffBlock.NumberU64(),
4308+
BlockHash: cutoffBlock.Hash(),
4309+
}
4310+
defer func() {
4311+
delete(history.PrunePoints, ghash)
4312+
}()
4313+
4314+
// Enable pruning in cache config.
42944315
config := DefaultCacheConfigWithScheme(rawdb.PathScheme)
4295-
config.HistoryPruningCutoffNumber = cutoffBlock.NumberU64()
4296-
config.HistoryPruningCutoffHash = cutoffBlock.Hash()
4316+
config.ChainHistoryMode = history.KeepPostMerge
42974317

4298-
chain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.PathScheme), gspec, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil)
4318+
db, _ := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), "", "", false)
4319+
defer db.Close()
4320+
chain, _ := NewBlockChain(db, DefaultCacheConfigWithScheme(rawdb.PathScheme), genesis, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil)
42994321
defer chain.Stop()
43004322

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

43334355
// 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)