Skip to content

Commit 9bedd6c

Browse files
holimanjagdeep sidhu
authored andcommitted
eth: make traceChain avoid OOM on long-running tracing (ethereum#23736)
This PR changes long-running chain tracing, so that it at some points releases the memory trie db, and switch over to a fresh disk-backed trie.
1 parent dbb329d commit 9bedd6c

File tree

6 files changed

+52
-23
lines changed

6 files changed

+52
-23
lines changed

eth/api_backend.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,8 @@ func (b *EthAPIBackend) StartMining(threads int) error {
357357
return b.eth.StartMining(threads)
358358
}
359359

360-
func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error) {
361-
return b.eth.stateAtBlock(block, reexec, base, checkLive)
360+
func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive, preferDisk bool) (*state.StateDB, error) {
361+
return b.eth.stateAtBlock(block, reexec, base, checkLive, preferDisk)
362362
}
363363

364364
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (core.Message, vm.BlockContext, *state.StateDB, error) {

eth/state_accessor.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@ import (
3535
// are attempted to be reexecuted to generate the desired state. The optional
3636
// base layer statedb can be passed then it's regarded as the statedb of the
3737
// parent block.
38-
func (eth *Ethereum) stateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (statedb *state.StateDB, err error) {
38+
// Parameters:
39+
// - block: The block for which we want the state (== state at the stateRoot of the parent)
40+
// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state
41+
// - base: If the caller is tracing multiple blocks, the caller can provide the parent state
42+
// continuously from the callsite.
43+
// - checklive: if true, then the live 'blockchain' state database is used. If the caller want to
44+
// perform Commit or other 'save-to-disk' changes, this should be set to false to avoid
45+
// storing trash persistently
46+
// - preferDisk: this arg can be used by the caller to signal that even though the 'base' is provided,
47+
// it would be preferrable to start from a fresh state, if we have it on disk.
48+
func (eth *Ethereum) stateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (statedb *state.StateDB, err error) {
3949
var (
4050
current *types.Block
4151
database state.Database
@@ -50,6 +60,15 @@ func (eth *Ethereum) stateAtBlock(block *types.Block, reexec uint64, base *state
5060
}
5161
}
5262
if base != nil {
63+
if preferDisk {
64+
// Create an ephemeral trie.Database for isolating the live one. Otherwise
65+
// the internal junks created by tracing will be persisted into the disk.
66+
database = state.NewDatabaseWithConfig(eth.chainDb, &trie.Config{Cache: 16})
67+
if statedb, err = state.New(block.Root(), database, nil); err == nil {
68+
log.Info("Found disk backend for state trie", "root", block.Root(), "number", block.Number())
69+
return statedb, nil
70+
}
71+
}
5372
// The optional base statedb is given, mark the start point as parent block
5473
statedb, database, report = base, base.Database(), false
5574
current = eth.blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1)
@@ -152,7 +171,7 @@ func (eth *Ethereum) stateAtTransaction(block *types.Block, txIndex int, reexec
152171
}
153172
// Lookup the statedb of parent block from the live database,
154173
// otherwise regenerate it on the flight.
155-
statedb, err := eth.stateAtBlock(parent, reexec, nil, true)
174+
statedb, err := eth.stateAtBlock(parent, reexec, nil, true, false)
156175
if err != nil {
157176
return nil, vm.BlockContext{}, nil, err
158177
}

eth/tracers/api.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ const (
5353
// and reexecute to produce missing historical state necessary to run a specific
5454
// trace.
5555
defaultTraceReexec = uint64(128)
56+
57+
// defaultTracechainMemLimit is the size of the triedb, at which traceChain
58+
// switches over and tries to use a disk-backed database instead of building
59+
// on top of memory.
60+
// For non-archive nodes, this limit _will_ be overblown, as disk-backed tries
61+
// will only be found every ~15K blocks or so.
62+
defaultTracechainMemLimit = common.StorageSize(500 * 1024 * 1024)
5663
)
5764

5865
// Backend interface provides the common API services (that are provided by
@@ -67,7 +74,10 @@ type Backend interface {
6774
ChainConfig() *params.ChainConfig
6875
Engine() consensus.Engine
6976
ChainDb() ethdb.Database
70-
StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error)
77+
// StateAtBlock returns the state corresponding to the stateroot of the block.
78+
// N.B: For executing transactions on block N, the required stateRoot is block N-1,
79+
// so this method should be called with the parent.
80+
StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive, preferDisk bool) (*state.StateDB, error)
7181
StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (core.Message, vm.BlockContext, *state.StateDB, error)
7282
// SYSCOIN
7383
ReadSYSHash(ctx context.Context, number rpc.BlockNumber) ([]byte, error)
@@ -329,6 +339,7 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config
329339
}
330340
close(results)
331341
}()
342+
var preferDisk bool
332343
// Feed all the blocks both into the tracer, as well as fast process concurrently
333344
for number = start.NumberU64(); number < end.NumberU64(); number++ {
334345
// Stop tracing if interruption was requested
@@ -358,18 +369,24 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config
358369
}
359370
// Prepare the statedb for tracing. Don't use the live database for
360371
// tracing to avoid persisting state junks into the database.
361-
statedb, err = api.backend.StateAtBlock(localctx, block, reexec, statedb, false)
372+
statedb, err = api.backend.StateAtBlock(localctx, block, reexec, statedb, false, preferDisk)
362373
if err != nil {
363374
failed = err
364375
break
365376
}
366-
if statedb.Database().TrieDB() != nil {
377+
if trieDb := statedb.Database().TrieDB(); trieDb != nil {
367378
// Hold the reference for tracer, will be released at the final stage
368-
statedb.Database().TrieDB().Reference(block.Root(), common.Hash{})
379+
trieDb.Reference(block.Root(), common.Hash{})
369380

370381
// Release the parent state because it's already held by the tracer
371382
if parent != (common.Hash{}) {
372-
statedb.Database().TrieDB().Dereference(parent)
383+
trieDb.Dereference(parent)
384+
}
385+
// Prefer disk if the trie db memory grows too much
386+
s1, s2 := trieDb.Size()
387+
if !preferDisk && (s1+s2) > defaultTracechainMemLimit {
388+
log.Info("Switching to prefer-disk mode for tracing", "size", s1+s2)
389+
preferDisk = true
373390
}
374391
}
375392
parent = block.Root()
@@ -505,7 +522,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
505522
if config != nil && config.Reexec != nil {
506523
reexec = *config.Reexec
507524
}
508-
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true)
525+
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
509526
if err != nil {
510527
return nil, err
511528
}
@@ -566,7 +583,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
566583
if config != nil && config.Reexec != nil {
567584
reexec = *config.Reexec
568585
}
569-
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true)
586+
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
570587
if err != nil {
571588
return nil, err
572589
}
@@ -655,7 +672,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block
655672
if config != nil && config.Reexec != nil {
656673
reexec = *config.Reexec
657674
}
658-
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true)
675+
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
659676
if err != nil {
660677
return nil, err
661678
}
@@ -819,7 +836,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
819836
if config != nil && config.Reexec != nil {
820837
reexec = *config.Reexec
821838
}
822-
statedb, err := api.backend.StateAtBlock(ctx, block, reexec, nil, true)
839+
statedb, err := api.backend.StateAtBlock(ctx, block, reexec, nil, true, false)
823840
if err != nil {
824841
return nil, err
825842
}

eth/tracers/api_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func (b *testBackend) ChainDb() ethdb.Database {
142142
return b.chaindb
143143
}
144144

145-
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error) {
145+
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (*state.StateDB, error) {
146146
statedb, err := b.chain.StateAt(block.Root())
147147
if err != nil {
148148
return nil, errStateNotFound

eth/tracers/tracer.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -553,17 +553,10 @@ func New(code string, ctx *Context) (*Tracer, error) {
553553
tracer.vm.Pop()
554554
hasExit := tracer.vm.GetPropString(tracer.tracerObject, "exit")
555555
tracer.vm.Pop()
556-
557556
if hasEnter != hasExit {
558557
return nil, fmt.Errorf("trace object must expose either both or none of enter() and exit()")
559558
}
560-
if !hasStep {
561-
// If there's no step function, the enter and exit must be present
562-
if !hasEnter {
563-
return nil, fmt.Errorf("trace object must expose either step() or both enter() and exit()")
564-
}
565-
}
566-
tracer.traceCallFrames = hasEnter
559+
tracer.traceCallFrames = hasEnter && hasExit
567560
tracer.traceSteps = hasStep
568561

569562
// Tracer is valid, inject the big int library to access large numbers

les/api_backend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ func (b *LesApiBackend) CurrentHeader() *types.Header {
328328
return b.eth.blockchain.CurrentHeader()
329329
}
330330

331-
func (b *LesApiBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error) {
331+
func (b *LesApiBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (*state.StateDB, error) {
332332
return b.eth.stateAtBlock(ctx, block, reexec)
333333
}
334334

0 commit comments

Comments
 (0)