eth: make traceChain avoid OOM on long-running tracing #23736 (#1263)

This commit is contained in:
Daniel Liu 2025-08-28 18:52:32 +08:00 committed by GitHub
parent 3926b08074
commit 9425af8b7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 13 deletions

View file

@ -413,8 +413,8 @@ func (b *EthAPIBackend) Miner() *miner.Miner {
return b.eth.Miner()
}
func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error) {
return b.eth.stateAtBlock(block, reexec, base, checkLive)
func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (*state.StateDB, error) {
return b.eth.stateAtBlock(block, reexec, base, checkLive, preferDisk)
}
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (core.Message, vm.BlockContext, *state.StateDB, error) {

View file

@ -36,7 +36,17 @@ import (
// are attempted to be reexecuted to generate the desired state. The optional
// base layer statedb can be passed then it's regarded as the statedb of the
// parent block.
func (eth *Ethereum) stateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (statedb *state.StateDB, err error) {
// Parameters:
// - block: The block for which we want the state (== state at the stateRoot of the parent)
// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state
// - base: If the caller is tracing multiple blocks, the caller can provide the parent state
// continuously from the callsite.
// - checklive: if true, then the live 'blockchain' state database is used. If the caller want to
// perform Commit or other 'save-to-disk' changes, this should be set to false to avoid
// storing trash persistently
// - preferDisk: this arg can be used by the caller to signal that even though the 'base' is provided,
// it would be preferrable to start from a fresh state, if we have it on disk.
func (eth *Ethereum) stateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (statedb *state.StateDB, err error) {
var (
current *types.Block
database state.Database
@ -51,6 +61,15 @@ func (eth *Ethereum) stateAtBlock(block *types.Block, reexec uint64, base *state
}
}
if base != nil {
if preferDisk {
// Create an ephemeral trie.Database for isolating the live one. Otherwise
// the internal junks created by tracing will be persisted into the disk.
database = state.NewDatabaseWithConfig(eth.chainDb, &trie.Config{Cache: 16})
if statedb, err = state.New(block.Root(), database); err == nil {
log.Info("Found disk backend for state trie", "root", block.Root(), "number", block.Number())
return statedb, nil
}
}
// The optional base statedb is given, mark the start point as parent block
statedb, database, report = base, base.Database(), false
current = eth.blockchain.GetBlock(block.ParentHash(), block.NumberU64()-1)
@ -153,7 +172,7 @@ func (eth *Ethereum) stateAtTransaction(block *types.Block, txIndex int, reexec
}
// Lookup the statedb of parent block from the live database,
// otherwise regenerate it on the flight.
statedb, err := eth.stateAtBlock(parent, reexec, nil, true)
statedb, err := eth.stateAtBlock(parent, reexec, nil, true, false)
if err != nil {
return nil, vm.BlockContext{}, nil, err
}

View file

@ -53,6 +53,13 @@ const (
// and reexecute to produce missing historical state necessary to run a specific
// trace.
defaultTraceReexec = uint64(128)
// defaultTracechainMemLimit is the size of the triedb, at which traceChain
// switches over and tries to use a disk-backed database instead of building
// on top of memory.
// For non-archive nodes, this limit _will_ be overblown, as disk-backed tries
// will only be found every ~15K blocks or so.
defaultTracechainMemLimit = common.StorageSize(500 * 1024 * 1024)
)
// Backend interface provides the common API services (that are provided by
@ -68,7 +75,10 @@ type Backend interface {
ChainConfig() *params.ChainConfig
Engine() consensus.Engine
ChainDb() ethdb.Database
StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error)
// StateAtBlock returns the state corresponding to the stateroot of the block.
// N.B: For executing transactions on block N, the required stateRoot is block N-1,
// so this method should be called with the parent.
StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (*state.StateDB, error)
StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (core.Message, vm.BlockContext, *state.StateDB, error)
}
@ -318,6 +328,7 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config
}
close(results)
}()
var preferDisk bool
// Feed all the blocks both into the tracer, as well as fast process concurrently
for number = start.NumberU64(); number < end.NumberU64(); number++ {
// clean out any derefs
@ -341,18 +352,24 @@ func (api *API) traceChain(ctx context.Context, start, end *types.Block, config
}
// Prepare the statedb for tracing. Don't use the live database for
// tracing to avoid persisting state junks into the database.
statedb, err = api.backend.StateAtBlock(localctx, block, reexec, statedb, false)
statedb, err = api.backend.StateAtBlock(localctx, block, reexec, statedb, false, preferDisk)
if err != nil {
failed = err
break
}
if statedb.Database().TrieDB() != nil {
if trieDb := statedb.Database().TrieDB(); trieDb != nil {
// Hold the reference for tracer, will be released at the final stage
statedb.Database().TrieDB().Reference(block.Root(), common.Hash{})
trieDb.Reference(block.Root(), common.Hash{})
// Release the parent state because it's already held by the tracer
if parent != (common.Hash{}) {
statedb.Database().TrieDB().Dereference(parent)
trieDb.Dereference(parent)
}
// Prefer disk if the trie db memory grows too much
s1, s2 := trieDb.Size()
if !preferDisk && (s1+s2) > defaultTracechainMemLimit {
log.Info("Switching to prefer-disk mode for tracing", "size", s1+s2)
preferDisk = true
}
}
parent = block.Root()
@ -458,7 +475,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true)
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
if err != nil {
return nil, err
}
@ -524,7 +541,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true)
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
if err != nil {
return nil, err
}
@ -676,7 +693,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, err := api.backend.StateAtBlock(ctx, block, reexec, nil, true)
statedb, err := api.backend.StateAtBlock(ctx, block, reexec, nil, true, false)
if err != nil {
return nil, err
}

View file

@ -144,7 +144,7 @@ func (b *testBackend) ChainDb() ethdb.Database {
return b.chaindb
}
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool) (*state.StateDB, error) {
func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (*state.StateDB, error) {
statedb, err := b.chain.StateAt(block.Root())
if err != nil {
return nil, errStateNotFound