core/state: integrate witness collector

This commit is contained in:
Gary Rong 2026-03-27 17:14:47 +08:00 committed by CPerezz
parent 5e23a29b73
commit d57dca07b1
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
14 changed files with 424 additions and 203 deletions

View file

@ -72,11 +72,10 @@ var (
accountReadTimer = metrics.NewRegisteredResettingTimer("chain/account/reads", nil) accountReadTimer = metrics.NewRegisteredResettingTimer("chain/account/reads", nil)
accountHashTimer = metrics.NewRegisteredResettingTimer("chain/account/hashes", nil) accountHashTimer = metrics.NewRegisteredResettingTimer("chain/account/hashes", nil)
accountUpdateTimer = metrics.NewRegisteredResettingTimer("chain/account/updates", nil) accountUpdateTimer = metrics.NewRegisteredResettingTimer("chain/account/updates", nil)
accountCommitTimer = metrics.NewRegisteredResettingTimer("chain/account/commits", nil) hasherCommitTimer = metrics.NewRegisteredResettingTimer("chain/trie/commits", nil)
storageReadTimer = metrics.NewRegisteredResettingTimer("chain/storage/reads", nil) storageReadTimer = metrics.NewRegisteredResettingTimer("chain/storage/reads", nil)
storageUpdateTimer = metrics.NewRegisteredResettingTimer("chain/storage/updates", nil) storageUpdateTimer = metrics.NewRegisteredResettingTimer("chain/storage/updates", nil)
storageCommitTimer = metrics.NewRegisteredResettingTimer("chain/storage/commits", nil)
codeReadTimer = metrics.NewRegisteredResettingTimer("chain/code/reads", nil) codeReadTimer = metrics.NewRegisteredResettingTimer("chain/code/reads", nil)
codeReadBytesTimer = metrics.NewRegisteredResettingTimer("chain/code/readbytes", nil) codeReadBytesTimer = metrics.NewRegisteredResettingTimer("chain/code/readbytes", nil)
@ -2112,12 +2111,16 @@ type ExecuteConfig struct {
// it writes the block and associated state to database. // it writes the block and associated state to database.
func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) { func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) {
var ( var (
err error err error
startTime = time.Now() startTime = time.Now()
statedb *state.StateDB statedb *state.StateDB
interrupt atomic.Bool interrupt atomic.Bool
sdb = state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps) sdb = state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
makeWitness bool
) )
if bc.chainConfig.IsByzantium(block.Number()) && (config.StatelessSelfValidation || config.MakeWitness) {
makeWitness = true
}
defer interrupt.Store(true) // terminate the prefetch at the end defer interrupt.Store(true) // terminate the prefetch at the end
if bc.cfg.NoPrefetch { if bc.cfg.NoPrefetch {
@ -2126,6 +2129,10 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
return nil, err return nil, err
} }
} else { } else {
// Enable trie node prewarming. The read-only state should also
// be prewarmed for constructing a comprehensive execution witness.
sdb = sdb.EnablePrefetch(makeWitness)
// If prefetching is enabled, run that against the current state to pre-cache // If prefetching is enabled, run that against the current state to pre-cache
// transactions and probabilistically some of the account/storage trie nodes. // transactions and probabilistically some of the account/storage trie nodes.
// //
@ -2171,20 +2178,16 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
// while processing transactions. Before Byzantium the prefetcher is mostly // while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction. // useless due to the intermediate root hashing after each transaction.
var witness *stateless.Witness var witness *stateless.Witness
if bc.chainConfig.IsByzantium(block.Number()) { if makeWitness {
// Generate witnesses either if we're self-testing, or if it's the // Generate witnesses either if we're self-testing, or if it's the
// only block being inserted. A bit crude, but witnesses are huge, // only block being inserted. A bit crude, but witnesses are huge,
// so we refuse to make an entire chain of them. // so we refuse to make an entire chain of them.
if config.StatelessSelfValidation || config.MakeWitness { witness, err = stateless.NewWitness(block.Header(), bc, config.EnableWitnessStats)
witness, err = stateless.NewWitness(block.Header(), bc, config.EnableWitnessStats) if err != nil {
if err != nil { return nil, err
return nil, err
}
} }
statedb.StartPrefetcher("chain", witness) statedb.TraceWitness(witness)
defer statedb.StopPrefetcher()
} }
// Instrument the blockchain tracing // Instrument the blockchain tracing
if config.EnableTracer { if config.EnableTracer {
if bc.logger != nil && bc.logger.OnBlockStart != nil { if bc.logger != nil && bc.logger.OnBlockStart != nil {
@ -2252,34 +2255,9 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
} }
var ( var (
xvtime = time.Since(xvstart)
proctime = time.Since(startTime) // processing + validation + cross validation proctime = time.Since(startTime) // processing + validation + cross validation
stats = &ExecuteStats{} stats = NewExecuteStats(statedb, ptime, vtime, time.Since(xvstart))
) )
// Update the metrics touched during block processing and validation
stats.AccountReads = statedb.AccountReads // Account reads are complete(in processing)
stats.StorageReads = statedb.StorageReads // Storage reads are complete(in processing)
stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete(in validation)
stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete(in validation)
stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation)
stats.CodeReads = statedb.CodeReads
stats.AccountLoaded = statedb.AccountLoaded
//stats.AccountUpdated = statedb.AccountUpdated
stats.AccountDeleted = statedb.AccountDeleted
stats.StorageLoaded = statedb.StorageLoaded
stats.StorageUpdated = int(statedb.StorageUpdated.Load())
stats.StorageDeleted = int(statedb.StorageDeleted.Load())
stats.CodeLoaded = statedb.CodeLoaded
stats.CodeLoadBytes = statedb.CodeLoadBytes
//stats.CodeUpdated = statedb.CodeUpdated
//stats.CodeUpdateBytes = statedb.CodeUpdateBytes
stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation
stats.CrossValidation = xvtime // The time spent on stateless cross validation
// Write the block to the chain and get the status. // Write the block to the chain and get the status.
var status WriteStatus var status WriteStatus
if config.WriteState { if config.WriteState {
@ -2294,10 +2272,9 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
return nil, err return nil, err
} }
// Update the metrics touched during block commit // Update the metrics touched during block commit
stats.AccountCommits = statedb.AccountCommits // Account commits are complete, we can mark them stats.HasherCommit = statedb.HasherCommits // Storage commits are complete, we can mark them
stats.StorageCommits = statedb.StorageCommits // Storage commits are complete, we can mark them
stats.DatabaseCommit = statedb.DatabaseCommits // Database commits are complete, we can mark them stats.DatabaseCommit = statedb.DatabaseCommits // Database commits are complete, we can mark them
stats.BlockWrite = time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.DatabaseCommits stats.BlockWrite = time.Since(wstart) - statedb.HasherCommits - statedb.DatabaseCommits
} }
// Report the collected witness statistics // Report the collected witness statistics
if witness != nil { if witness != nil {

View file

@ -424,6 +424,11 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)) return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
} }
// StateWithPrefetching returns a new mutable state based on a particular point in time.
func (bc *BlockChain) StateWithPrefetching(root common.Hash) (*state.StateDB, error) {
return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps).EnablePrefetch(false))
}
// HistoricState returns a historic state specified by the given root. // HistoricState returns a historic state specified by the given root.
// Live states are not available and won't be served, please use `State` // Live states are not available and won't be served, please use `State`
// or `StateAt` instead. // or `StateAt` instead.

View file

@ -29,14 +29,30 @@ import (
// ExecuteStats includes all the statistics of a block execution in details. // ExecuteStats includes all the statistics of a block execution in details.
type ExecuteStats struct { type ExecuteStats struct {
// State read times // State read times
AccountReads time.Duration // Time spent on the account reads AccountReads time.Duration // Time spent on the account reads
StorageReads time.Duration // Time spent on the storage reads StorageReads time.Duration // Time spent on the storage reads
CodeReads time.Duration // Time spent on the contract code read
// State hash times
AccountHashes time.Duration // Time spent on the account trie hash AccountHashes time.Duration // Time spent on the account trie hash
AccountUpdates time.Duration // Time spent on the account trie update AccountUpdates time.Duration // Time spent on the account trie update
AccountCommits time.Duration // Time spent on the account trie commit
StorageUpdates time.Duration // Time spent on the storage trie update StorageUpdates time.Duration // Time spent on the storage trie update
StorageCommits time.Duration // Time spent on the storage trie commit
CodeReads time.Duration // Time spent on the contract code read // EVM execution time
Execution time.Duration // Time spent on the EVM execution
// Validation times
Validation time.Duration // Time spent on the block validation
CrossValidation time.Duration // Optional, time spent on the block cross validation
// Commit times
HasherCommit time.Duration // Time spent on trie commit
DatabaseCommit time.Duration // Time spent on database commit
BlockWrite time.Duration // Time spent on block write
// Others
TotalTime time.Duration // The total time spent on block execution
MgasPerSecond float64 // The million gas processed per second
AccountLoaded int // Number of accounts loaded AccountLoaded int // Number of accounts loaded
AccountUpdated int // Number of accounts updated AccountUpdated int // Number of accounts updated
@ -49,19 +65,40 @@ type ExecuteStats struct {
CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702) CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702)
CodeUpdateBytes int // Total bytes of code written CodeUpdateBytes int // Total bytes of code written
Execution time.Duration // Time spent on the EVM execution
Validation time.Duration // Time spent on the block validation
CrossValidation time.Duration // Optional, time spent on the block cross validation
DatabaseCommit time.Duration // Time spent on database commit
BlockWrite time.Duration // Time spent on block write
TotalTime time.Duration // The total time spent on block execution
MgasPerSecond float64 // The million gas processed per second
// Cache hit rates // Cache hit rates
StateReadCacheStats state.ReaderStats StateReadCacheStats state.ReaderStats
StatePrefetchCacheStats state.ReaderStats StatePrefetchCacheStats state.ReaderStats
} }
func NewExecuteStats(stateDB *state.StateDB, process time.Duration, validation time.Duration, crossValidation time.Duration) *ExecuteStats {
return &ExecuteStats{
// State read times
AccountReads: stateDB.AccountReads,
StorageReads: stateDB.StorageReads,
CodeReads: stateDB.CodeReads,
// State hash times
AccountHashes: stateDB.AccountHashes,
AccountUpdates: stateDB.AccountUpdates,
StorageUpdates: stateDB.StorageUpdates,
Execution: process - stateDB.StateReadTime(),
Validation: validation - stateDB.StateHashTime(),
CrossValidation: crossValidation,
AccountLoaded: stateDB.AccountLoaded,
AccountUpdated: stateDB.AccountUpdated,
AccountDeleted: stateDB.AccountDeleted,
StorageLoaded: stateDB.StorageLoaded,
StorageUpdated: int(stateDB.StorageUpdated.Load()),
StorageDeleted: int(stateDB.StorageDeleted.Load()),
CodeLoaded: stateDB.CodeLoaded,
CodeLoadBytes: stateDB.CodeLoadBytes,
CodeUpdated: stateDB.CodeUpdated,
CodeUpdateBytes: stateDB.CodeUpdateBytes,
}
}
// reportMetrics uploads execution statistics to the metrics system. // reportMetrics uploads execution statistics to the metrics system.
func (s *ExecuteStats) reportMetrics() { func (s *ExecuteStats) reportMetrics() {
if s.AccountLoaded != 0 { if s.AccountLoaded != 0 {
@ -80,8 +117,7 @@ func (s *ExecuteStats) reportMetrics() {
accountUpdateTimer.Update(s.AccountUpdates) // Account updates are complete(in validation) accountUpdateTimer.Update(s.AccountUpdates) // Account updates are complete(in validation)
storageUpdateTimer.Update(s.StorageUpdates) // Storage updates are complete(in validation) storageUpdateTimer.Update(s.StorageUpdates) // Storage updates are complete(in validation)
accountHashTimer.Update(s.AccountHashes) // Account hashes are complete(in validation) accountHashTimer.Update(s.AccountHashes) // Account hashes are complete(in validation)
accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them hasherCommitTimer.Update(s.HasherCommit) // Trie commits are complete, we can mark them
storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them
blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing
blockValidationTimer.Update(s.Validation) // The time spent on block validation blockValidationTimer.Update(s.Validation) // The time spent on block validation
@ -206,7 +242,7 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat
ExecutionMs: durationToMs(s.Execution), ExecutionMs: durationToMs(s.Execution),
StateReadMs: durationToMs(s.AccountReads + s.StorageReads + s.CodeReads), StateReadMs: durationToMs(s.AccountReads + s.StorageReads + s.CodeReads),
StateHashMs: durationToMs(s.AccountHashes + s.AccountUpdates + s.StorageUpdates), StateHashMs: durationToMs(s.AccountHashes + s.AccountUpdates + s.StorageUpdates),
CommitMs: durationToMs(max(s.AccountCommits, s.StorageCommits) + s.DatabaseCommit + s.BlockWrite), CommitMs: durationToMs(s.HasherCommit + s.DatabaseCommit + s.BlockWrite),
TotalMs: durationToMs(s.TotalTime), TotalMs: durationToMs(s.TotalTime),
}, },
Throughput: slowBlockThru{ Throughput: slowBlockThru{

View file

@ -150,6 +150,9 @@ type CachingDB struct {
triedb *triedb.Database triedb *triedb.Database
codedb *CodeDB codedb *CodeDB
snap *snapshot.Tree snap *snapshot.Tree
prefetch bool
prefetchRead bool
} }
// NewDatabase creates a state database with the provided data sources. // NewDatabase creates a state database with the provided data sources.
@ -177,6 +180,13 @@ func (db *CachingDB) WithSnapshot(snapshot *snapshot.Tree) *CachingDB {
return db return db
} }
// EnablePrefetch enables the hasher prefetching feature.
func (db *CachingDB) EnablePrefetch(prefetchRead bool) *CachingDB {
db.prefetch = true
db.prefetchRead = prefetchRead
return db
}
// StateReader returns a state reader associated with the specified state root. // StateReader returns a state reader associated with the specified state root.
func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) { func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
var readers []StateReader var readers []StateReader
@ -224,7 +234,7 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) {
// Hasher implements Database, returning a hasher associated with the specified // Hasher implements Database, returning a hasher associated with the specified
// state root. // state root.
func (db *CachingDB) Hasher(stateRoot common.Hash) (Hasher, error) { func (db *CachingDB) Hasher(stateRoot common.Hash) (Hasher, error) {
return newMerkleHasher(stateRoot, db.triedb, true, true) return newMerkleHasher(stateRoot, db.triedb, db.prefetch, db.prefetchRead)
} }
// ReadersWithCacheStats creates a pair of state readers that share the same // ReadersWithCacheStats creates a pair of state readers that share the same

View file

@ -93,9 +93,9 @@ type Prefetcher interface {
// WitnessCollector is an optional extension implemented by hashers that can // WitnessCollector is an optional extension implemented by hashers that can
// construct a state witness for the most recent committed state transition. // construct a state witness for the most recent committed state transition.
type WitnessCollector interface { type WitnessCollector interface {
// Witness returns the state witness corresponding to the most recent // CollectWitness returns the state witness corresponding to the most recent
// committed state transition. // committed state transition.
Witness() (*stateless.Witness, error) CollectWitness(*stateless.Witness)
} }
// Prover is an optional extension implemented by hashers that can construct // Prover is an optional extension implemented by hashers that can construct

View file

@ -21,12 +21,14 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
"golang.org/x/sync/errgroup"
) )
// wrapTrie pairs a StateTrie with an optional background prefetcher that // wrapTrie pairs a StateTrie with an optional background prefetcher that
@ -36,6 +38,7 @@ type wrapTrie struct {
prefetcher *prefetcher prefetcher *prefetcher
} }
// newWrapTrie creates a merkle trie with the optional prefetcher enabled.
func newWrapTrie(id *trie.ID, db *triedb.Database, prefetch bool, prefetchRead bool) (*wrapTrie, error) { func newWrapTrie(id *trie.ID, db *triedb.Database, prefetch bool, prefetchRead bool) (*wrapTrie, error) {
t, err := trie.NewStateTrie(id, db) t, err := trie.NewStateTrie(id, db)
if err != nil { if err != nil {
@ -59,7 +62,7 @@ func (tr *wrapTrie) term() {
tr.prefetcher = nil tr.prefetcher = nil
} }
// The methods below shadow the embedded StateTrie so that any direct trie // The methods below shadow the embedded trie.StateTrie so that any direct trie
// access auto-terminates the prefetcher first. This makes data-race freedom // access auto-terminates the prefetcher first. This makes data-race freedom
// structural: callers never need to remember to call term() manually. // structural: callers never need to remember to call term() manually.
@ -98,9 +101,9 @@ func (tr *wrapTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
return tr.StateTrie.Prove(key, proofDb) return tr.StateTrie.Prove(key, proofDb)
} }
func (tr *wrapTrie) copy() *wrapTrie { func (tr *wrapTrie) Witness() map[string][]byte {
tr.term() tr.term()
return &wrapTrie{StateTrie: tr.StateTrie.Copy()} return tr.StateTrie.Witness()
} }
func (tr *wrapTrie) prefetchAccounts(addresses []common.Address, read bool) { func (tr *wrapTrie) prefetchAccounts(addresses []common.Address, read bool) {
@ -117,22 +120,31 @@ func (tr *wrapTrie) prefetchStorage(addr common.Address, keys []common.Hash, rea
tr.prefetcher.scheduleSlots(addr, keys, read) tr.prefetcher.scheduleSlots(addr, keys, read)
} }
// rootReader wraps the account trie for loading the storage root. It is // copy returns a deep-copied state trie. Notably the prefetcher is deliberately
// essential to use an independent trie to prevent potential data races // not copied, as it only belongs to the original one.
// with the optional prefetcher. func (tr *wrapTrie) copy() *wrapTrie {
type rootReader struct { tr.term()
return &wrapTrie{StateTrie: tr.StateTrie.Copy()}
}
// storageRootReader wraps the account trie for loading the storage root. It is
// essential to use an independent trie to prevent potential data races with
// the optional prefetcher.
//
// TODO(rjl493456442) use the flat state for better read efficiency.
type storageRootReader struct {
tr *trie.StateTrie tr *trie.StateTrie
} }
func newRootReader(root common.Hash, db *triedb.Database) (*rootReader, error) { func newStorageRootReader(root common.Hash, db *triedb.Database) (*storageRootReader, error) {
t, err := trie.NewStateTrie(trie.StateTrieID(root), db) t, err := trie.NewStateTrie(trie.StateTrieID(root), db)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &rootReader{tr: t}, nil return &storageRootReader{tr: t}, nil
} }
func (r *rootReader) readStorageRoot(address common.Address) (common.Hash, error) { func (r *storageRootReader) read(address common.Address) (common.Hash, error) {
acct, err := r.tr.GetAccount(address) acct, err := r.tr.GetAccount(address)
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
@ -143,8 +155,8 @@ func (r *rootReader) readStorageRoot(address common.Address) (common.Hash, error
return acct.Root, nil return acct.Root, nil
} }
func (r *rootReader) copy() *rootReader { func (r *storageRootReader) copy() *storageRootReader {
return &rootReader{tr: r.tr.Copy()} return &storageRootReader{tr: r.tr.Copy()}
} }
// merkleHasher is a Hasher implementation backed by the traditional two-layer // merkleHasher is a Hasher implementation backed by the traditional two-layer
@ -152,7 +164,7 @@ func (r *rootReader) copy() *rootReader {
type merkleHasher struct { type merkleHasher struct {
db *triedb.Database db *triedb.Database
root common.Hash root common.Hash
reader *rootReader reader *storageRootReader
prefetch bool prefetch bool
prefetchRead bool prefetchRead bool
@ -160,7 +172,7 @@ type merkleHasher struct {
storageTries map[common.Address]*wrapTrie storageTries map[common.Address]*wrapTrie
// deletedTries preserves storage tries of accounts that were deleted // deletedTries preserves storage tries of accounts that were deleted
// during the block. Keyed by address; only the first deletion per // during the block keyed by address. Only the first deletion per
// address is recorded (the pre-block incarnation). // address is recorded (the pre-block incarnation).
deletedTries map[common.Address]*wrapTrie deletedTries map[common.Address]*wrapTrie
@ -169,7 +181,8 @@ type merkleHasher struct {
// UpdateStorage or set to EmptyRootHash on deletion. // UpdateStorage or set to EmptyRootHash on deletion.
storageRoots map[common.Address]Hashes storageRoots map[common.Address]Hashes
storageLock sync.Mutex // guards storage trie fields // Lock guards storage trie fields
storageLock sync.Mutex
} }
func newMerkleHasher(root common.Hash, db *triedb.Database, prefetch bool, prefetchRead bool) (*merkleHasher, error) { func newMerkleHasher(root common.Hash, db *triedb.Database, prefetch bool, prefetchRead bool) (*merkleHasher, error) {
@ -177,7 +190,7 @@ func newMerkleHasher(root common.Hash, db *triedb.Database, prefetch bool, prefe
if err != nil { if err != nil {
return nil, err return nil, err
} }
r, err := newRootReader(root, db) r, err := newStorageRootReader(root, db)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -201,11 +214,14 @@ func (h *merkleHasher) storageRoot(addr common.Address) (common.Hash, error) {
if hashes, ok := h.storageRoots[addr]; ok { if hashes, ok := h.storageRoots[addr]; ok {
return hashes.Hash, nil return hashes.Hash, nil
} }
root, err := h.reader.readStorageRoot(addr) root, err := h.reader.read(addr)
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
h.storageRoots[addr] = Hashes{Prev: root, Hash: root} h.storageRoots[addr] = Hashes{
Prev: root,
Hash: root,
}
return root, nil return root, nil
} }
@ -223,6 +239,7 @@ func (h *merkleHasher) openStorageTrie(address common.Address, prefetch bool) (*
return nil, err return nil, err
} }
id := trie.StorageTrieID(h.root, crypto.Keccak256Hash(address.Bytes()), root) id := trie.StorageTrieID(h.root, crypto.Keccak256Hash(address.Bytes()), root)
tr, err := newWrapTrie(id, h.db, h.prefetch && prefetch, h.prefetchRead) tr, err := newWrapTrie(id, h.db, h.prefetch && prefetch, h.prefetchRead)
if err != nil { if err != nil {
return nil, err return nil, err
@ -231,8 +248,9 @@ func (h *merkleHasher) openStorageTrie(address common.Address, prefetch bool) (*
return tr, nil return tr, nil
} }
// deleteAccount removes the account specified by the address from the state.
func (h *merkleHasher) deleteAccount(addr common.Address) error { func (h *merkleHasher) deleteAccount(addr common.Address) error {
// Deletion: capture the original storage root before modifying the trie. // Capture the original storage root before modifying the trie.
_, err := h.storageRoot(addr) _, err := h.storageRoot(addr)
if err != nil { if err != nil {
return err return err
@ -251,6 +269,7 @@ func (h *merkleHasher) deleteAccount(addr common.Address) error {
return h.acctTrie.DeleteAccount(addr) return h.acctTrie.DeleteAccount(addr)
} }
// update writes the account specified by the address into the state.
func (h *merkleHasher) updateAccount(addr common.Address, account AccountMut) error { func (h *merkleHasher) updateAccount(addr common.Address, account AccountMut) error {
root, err := h.storageRoot(addr) root, err := h.storageRoot(addr)
if err != nil { if err != nil {
@ -265,10 +284,12 @@ func (h *merkleHasher) updateAccount(addr common.Address, account AccountMut) er
return h.acctTrie.UpdateAccount(addr, data, 0) return h.acctTrie.UpdateAccount(addr, data, 0)
} }
// UpdateAccount implements Hasher. // UpdateAccount implements Hasher, writing a list of account mutations
// into the state. The assumption is held all the storage changes have
// already been written beforehand.
func (h *merkleHasher) UpdateAccount(addresses []common.Address, accounts []AccountMut) error { func (h *merkleHasher) UpdateAccount(addresses []common.Address, accounts []AccountMut) error {
var err error
for i, addr := range addresses { for i, addr := range addresses {
var err error
if accounts[i].Account == nil { if accounts[i].Account == nil {
err = h.deleteAccount(addr) err = h.deleteAccount(addr)
} else { } else {
@ -281,7 +302,9 @@ func (h *merkleHasher) UpdateAccount(addresses []common.Address, accounts []Acco
return nil return nil
} }
// UpdateStorage implements Hasher. // UpdateStorage implements Hasher, writing a list of storage slot mutations
// into the state. This function must be invoked first before writing the
// associated account metadata into the state.
func (h *merkleHasher) UpdateStorage(address common.Address, keys []common.Hash, values []common.Hash) error { func (h *merkleHasher) UpdateStorage(address common.Address, keys []common.Hash, values []common.Hash) error {
tr, err := h.openStorageTrie(address, false) tr, err := h.openStorageTrie(address, false)
if err != nil { if err != nil {
@ -289,18 +312,19 @@ func (h *merkleHasher) UpdateStorage(address common.Address, keys []common.Hash,
} }
for i, key := range keys { for i, key := range keys {
if values[i] == (common.Hash{}) { if values[i] == (common.Hash{}) {
if err := tr.DeleteStorage(address, key[:]); err != nil { err = tr.DeleteStorage(address, key[:])
return err
}
} else { } else {
if err := tr.UpdateStorage(address, key[:], common.TrimLeftZeroes(values[i][:])); err != nil { err = tr.UpdateStorage(address, key[:], common.TrimLeftZeroes(values[i][:]))
return err }
} if err != nil {
return err
} }
} }
// Hash outside the lock to allow full parallelism across accounts. // Hash outside the lock to allow full parallelism across accounts.
hash := tr.Hash() hash := tr.Hash()
// Write back the storage root back for reflecting the most recent
// changes.
h.storageLock.Lock() h.storageLock.Lock()
h.storageRoots[address] = Hashes{ h.storageRoots[address] = Hashes{
Prev: h.storageRoots[address].Prev, Prev: h.storageRoots[address].Prev,
@ -310,50 +334,64 @@ func (h *merkleHasher) UpdateStorage(address common.Address, keys []common.Hash,
return nil return nil
} }
// Hash implements Hasher, computing the state root hash without committing.
func (h *merkleHasher) Hash() common.Hash { func (h *merkleHasher) Hash() common.Hash {
return h.acctTrie.Hash() return h.acctTrie.Hash()
} }
// Close terminates all prefetcher goroutines. Safe to call multiple times. // Commit implements Hasher, finalizing all pending changes and returning
func (h *merkleHasher) Close() { // the resulting state root hash, along with the set of dirty trie nodes
h.acctTrie.term() // generated by the updates.
for _, tr := range h.storageTries {
tr.term()
}
for _, tr := range h.deletedTries {
tr.term()
}
}
func (h *merkleHasher) Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]Hashes, error) { func (h *merkleHasher) Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]Hashes, error) {
// Explicitly terminate all resolved tries. Some of them may not be // Explicitly terminate all resolved tries. Some of them may not be
// terminated due to read-only prefetching. This is essential to // terminated due to read-only prefetching. This is essential to
// prevent goroutine leaks. // prevent goroutine leaks.
h.Close() h.Close()
nodes := trienode.NewMergedNodeSet() var (
eg errgroup.Group
root common.Hash
lock sync.Mutex
nodes = trienode.NewMergedNodeSet()
merge = func(set *trienode.NodeSet) error {
lock.Lock()
defer lock.Unlock()
return nodes.Merge(set)
}
)
eg.Go(func() error {
r, set := h.acctTrie.Commit(true)
root = r
if set == nil {
return nil
}
return merge(set)
})
for _, tr := range h.storageTries { for _, tr := range h.storageTries {
if _, set := tr.Commit(false); set != nil { eg.Go(func() error {
if err := nodes.Merge(set); err != nil { _, set := tr.Commit(false)
return common.Hash{}, nil, nil, err if set == nil {
return nil
} }
} return merge(set)
})
} }
root, set := h.acctTrie.Commit(true) if err := eg.Wait(); err != nil {
if set != nil { return common.Hash{}, nil, nil, err
if err := nodes.Merge(set); err != nil {
return common.Hash{}, nil, nil, err
}
} }
return root, nodes, h.storageRoots, nil return root, nodes, h.storageRoots, nil
} }
// Copy implements Hasher, returning a deep-copied hasher instance.
func (h *merkleHasher) Copy() Hasher { func (h *merkleHasher) Copy() Hasher {
cpy := &merkleHasher{ cpy := &merkleHasher{
db: h.db, db: h.db,
root: h.root, root: h.root,
reader: h.reader.copy(), reader: h.reader.copy(),
prefetch: false, prefetch: false,
prefetchRead: false,
acctTrie: h.acctTrie.copy(), acctTrie: h.acctTrie.copy(),
storageTries: make(map[common.Address]*wrapTrie, len(h.storageTries)), storageTries: make(map[common.Address]*wrapTrie, len(h.storageTries)),
deletedTries: make(map[common.Address]*wrapTrie, len(h.deletedTries)), deletedTries: make(map[common.Address]*wrapTrie, len(h.deletedTries)),
@ -368,12 +406,24 @@ func (h *merkleHasher) Copy() Hasher {
return cpy return cpy
} }
// ProveAccount implements Prover. // Close terminates all prefetcher goroutines. Safe to call multiple times.
func (h *merkleHasher) Close() {
h.acctTrie.term()
for _, tr := range h.storageTries {
tr.term()
}
for _, tr := range h.deletedTries {
tr.term()
}
}
// ProveAccount implements Prover, constructing a proof for the given account.
func (h *merkleHasher) ProveAccount(addr common.Address, proofDb ethdb.KeyValueWriter) error { func (h *merkleHasher) ProveAccount(addr common.Address, proofDb ethdb.KeyValueWriter) error {
return h.acctTrie.Prove(crypto.Keccak256(addr.Bytes()), proofDb) return h.acctTrie.Prove(crypto.Keccak256(addr.Bytes()), proofDb)
} }
// ProveStorage implements Prover. // ProveStorage implements Prover, constructing a proof for the given storage
// slot of the specified account.
func (h *merkleHasher) ProveStorage(addr common.Address, key common.Hash, proofDb ethdb.KeyValueWriter) error { func (h *merkleHasher) ProveStorage(addr common.Address, key common.Hash, proofDb ethdb.KeyValueWriter) error {
tr, err := h.openStorageTrie(addr, false) tr, err := h.openStorageTrie(addr, false)
if err != nil { if err != nil {
@ -382,6 +432,19 @@ func (h *merkleHasher) ProveStorage(addr common.Address, key common.Hash, proofD
return tr.Prove(crypto.Keccak256(key.Bytes()), proofDb) return tr.Prove(crypto.Keccak256(key.Bytes()), proofDb)
} }
// CollectWitness implements WitnessCollector. It aggregates all trie nodes
// accessed (both read and write) across the account trie, all active storage
// tries and deleted storage tries into a single state witness.
func (h *merkleHasher) CollectWitness(witness *stateless.Witness) {
witness.AddState(h.acctTrie.Witness(), common.Hash{})
for addr, tr := range h.storageTries {
witness.AddState(tr.Witness(), crypto.Keccak256Hash(addr.Bytes()))
}
for addr, tr := range h.deletedTries {
witness.AddState(tr.Witness(), crypto.Keccak256Hash(addr.Bytes()))
}
}
// PrefetchAccount implements Prefetcher, preloading the nodes of specific accounts. // PrefetchAccount implements Prefetcher, preloading the nodes of specific accounts.
func (h *merkleHasher) PrefetchAccount(addresses []common.Address, read bool) { func (h *merkleHasher) PrefetchAccount(addresses []common.Address, read bool) {
if !h.prefetch { if !h.prefetch {

View file

@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/stateless"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
"github.com/holiman/uint256" "github.com/holiman/uint256"
@ -539,3 +540,90 @@ func TestMerkleHasherCopy(t *testing.T) {
t.Fatal("original root changed after mutating copy") t.Fatal("original root changed after mutating copy")
} }
} }
// proofNodes collects the raw RLP-encoded trie nodes written by Prove calls.
type proofNodes struct{ nodes [][]byte }
func (p *proofNodes) Put(key []byte, value []byte) error {
p.nodes = append(p.nodes, common.CopyBytes(value))
return nil
}
func (p *proofNodes) Delete([]byte) error { return nil }
// TestMerkleHasherWitness verifies that the witness returned by Witness()
// contains every trie node on the Merkle proof path for each accessed account
// and storage slot, including nodes from deleted storage tries.
func TestMerkleHasherWitness(t *testing.T) {
h := makeBaseState(t, hasherTestConfig{"prefetchAll", true, true})
// Mutate addr1 storage, then delete and recreate with different
// storage so that both deletedTries and storageTries are populated.
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1}, false)
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot1}, []common.Hash{hasherVal2}); err != nil {
t.Fatal(err)
}
if err := h.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherDeleteAccount()}); err != nil {
t.Fatal(err)
}
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
t.Fatal(err)
}
if err := h.UpdateAccount(
[]common.Address{hasherAddr1, hasherAddr2},
[]AccountMut{hasherAccount(10, 500), hasherAccount(2, 300)},
); err != nil {
t.Fatal(err)
}
witness := &stateless.Witness{
Codes: make(map[string]struct{}),
State: make(map[string]struct{}),
}
h.CollectWitness(witness)
if len(witness.State) == 0 {
t.Fatal("witness should contain trie nodes")
}
// Open a separate prover from the same pre-state root. Proofs
// generated here traverse the same trie paths that the mutating
// hasher loaded, so every proof node must be in the witness.
prover, err := newMerkleHasher(h.root, h.db, false, false)
if err != nil {
t.Fatal(err)
}
defer prover.Close()
// Collect all expected proof nodes into a single set. The union of
// account proofs (addr1, addr2) and storage proofs (addr1/slot1)
// should exactly equal witness.State — no missing, no extra.
expected := make(map[string]struct{})
for _, addr := range []common.Address{hasherAddr1, hasherAddr2} {
pn := &proofNodes{}
if err := prover.ProveAccount(addr, pn); err != nil {
t.Fatal(err)
}
for _, node := range pn.nodes {
expected[string(node)] = struct{}{}
}
}
// Storage proof for addr1/slot1 (accessed before deletion).
// Slot2 was in the base state but never read or written during the
// block, so its leaf node is correctly absent from the witness.
pn := &proofNodes{}
if err := prover.ProveStorage(hasherAddr1, hasherSlot1, pn); err != nil {
t.Fatal(err)
}
for _, node := range pn.nodes {
expected[string(node)] = struct{}{}
}
// Every expected proof node must be in the witness.
for node := range expected {
if _, ok := witness.State[node]; !ok {
t.Fatal("proof node missing from witness")
}
}
// The witness must not contain any extra nodes beyond the proofs.
if len(witness.State) != len(expected) {
t.Fatalf("witness has %d nodes, expected %d (extra junk present)", len(witness.State), len(expected))
}
}

View file

@ -19,14 +19,14 @@ package state
import "github.com/ethereum/go-ethereum/metrics" import "github.com/ethereum/go-ethereum/metrics"
var ( var (
accountReadMeters = metrics.NewRegisteredMeter("state/read/account", nil) accountReadMeters = metrics.NewRegisteredMeter("state/read/account", nil)
storageReadMeters = metrics.NewRegisteredMeter("state/read/storage", nil) storageReadMeters = metrics.NewRegisteredMeter("state/read/storage", nil)
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil) accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil) storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil) accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil) storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
accountTrieUpdatedMeter = metrics.NewRegisteredMeter("state/update/accountnodes", nil) //accountTrieUpdatedMeter = metrics.NewRegisteredMeter("state/update/accountnodes", nil)
storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil) //storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil)
accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil) //accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil)
storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil) //storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil)
) )

View file

@ -63,7 +63,6 @@ type Account struct {
} }
// newEmptyAccount returns an empty account. // newEmptyAccount returns an empty account.
// nolint:unused
func newEmptyAccount() *Account { func newEmptyAccount() *Account {
return &Account{ return &Account{
Balance: uint256.NewInt(0), Balance: uint256.NewInt(0),

View file

@ -38,7 +38,11 @@ type mutation struct {
} }
func (m *mutation) copy() *mutation { func (m *mutation) copy() *mutation {
return &mutation{typ: m.typ, applied: m.applied, precedingDelete: m.precedingDelete} return &mutation{
typ: m.typ,
applied: m.applied,
precedingDelete: m.precedingDelete,
}
} }
func (m *mutation) isDelete() bool { func (m *mutation) isDelete() bool {

View file

@ -171,14 +171,13 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
s.originStorage[key] = common.Hash{} // track the empty slot as origin value s.originStorage[key] = common.Hash{} // track the empty slot as origin value
return common.Hash{} return common.Hash{}
} }
s.db.StorageLoaded++
start := time.Now() start := time.Now()
value, err := s.db.reader.Storage(s.address, key) value, err := s.db.reader.Storage(s.address, key)
if err != nil { if err != nil {
s.db.setError(err) s.db.setError(err)
return common.Hash{} return common.Hash{}
} }
s.db.StorageLoaded++
s.db.StorageReads += time.Since(start) s.db.StorageReads += time.Since(start)
s.originStorage[key] = value s.originStorage[key] = value
@ -267,8 +266,10 @@ func (s *stateObject) updateTrie() error {
return nil return nil
} }
var ( var (
keys = make([]common.Hash, 0, len(s.uncommittedStorage)) updates int64
vals = make([]common.Hash, 0, len(s.uncommittedStorage)) deletes int64
keys = make([]common.Hash, 0, len(s.uncommittedStorage))
vals = make([]common.Hash, 0, len(s.uncommittedStorage))
) )
for key, origin := range s.uncommittedStorage { for key, origin := range s.uncommittedStorage {
// Skip noop changes, persist actual changes // Skip noop changes, persist actual changes
@ -281,10 +282,17 @@ func (s *stateObject) updateTrie() error {
log.Error("Storage slot is not found in pending area", "address", s.address, "slot", key) log.Error("Storage slot is not found in pending area", "address", s.address, "slot", key)
continue continue
} }
if value == (common.Hash{}) {
deletes += 1
} else {
updates += 1
}
keys = append(keys, key) keys = append(keys, key)
vals = append(vals, value) vals = append(vals, value)
} }
s.uncommittedStorage = make(Storage) // empties the commit markers s.uncommittedStorage = make(Storage) // empties the commit markers
s.db.StorageUpdated.Add(updates)
s.db.StorageDeleted.Add(deletes)
return s.db.hasher.UpdateStorage(s.address, keys, vals) return s.db.hasher.UpdateStorage(s.address, keys, vals)
} }
@ -337,7 +345,7 @@ func (s *stateObject) commit() (*accountUpdate, error) {
// commit the contract code if it's modified // commit the contract code if it's modified
if s.dirtyCode { if s.dirtyCode {
s.dirtyCode = false // reset the dirty flag s.dirtyCode = false // reset the dirty flag
op.code = &contractCode{ op.code = &contractCode{
hash: common.BytesToHash(s.CodeHash()), hash: common.BytesToHash(s.CodeHash()),
blob: s.code, blob: s.code,

View file

@ -23,7 +23,6 @@ import (
"maps" "maps"
"slices" "slices"
"sort" "sort"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -111,30 +110,11 @@ type StateDB struct {
// Snapshot and RevertToSnapshot. // Snapshot and RevertToSnapshot.
journal *journal journal *journal
// State witness if cross validation is needed
witness *stateless.Witness
// Measurements gathered during execution for debugging purposes // Measurements gathered during execution for debugging purposes
AccountReads time.Duration Stats
AccountHashes time.Duration
AccountUpdates time.Duration
AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration
StorageCommits time.Duration
DatabaseCommits time.Duration
CodeReads time.Duration
AccountLoaded int // Number of accounts retrieved from the database during the state transition
AccountDeleted int // Number of accounts deleted during the state transition
StorageLoaded int // Number of storage slots retrieved from the database during the state transition
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
// CodeLoadBytes is the total number of bytes read from contract code.
// This value may be smaller than the actual number of bytes read, since
// some APIs (e.g. CodeSize) may load the entire code from either the
// cache or the database when the size is not available in the cache.
CodeLoaded int // Number of contract code loaded during the state transition
CodeLoadBytes int // Total bytes of resolved code
} }
// New creates a new state from a given trie. // New creates a new state from a given trie.
@ -173,16 +153,11 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
return sdb, nil return sdb, nil
} }
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // TraceWitness enables execution witness gathering.
// state trie concurrently while the state is mutated so that when we reach the func (s *StateDB) TraceWitness(witness *stateless.Witness) {
// commit phase, most of the needed data is already hot. s.witness = witness
func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
} }
// StopPrefetcher terminates a running prefetcher and reports any leftover stats
// from the gathered metrics.
func (s *StateDB) StopPrefetcher() {}
// setError remembers the first non-nil error it is called with. // setError remembers the first non-nil error it is called with.
func (s *StateDB) setError(err error) { func (s *StateDB) setError(err error) {
if s.dbErr == nil { if s.dbErr == nil {
@ -206,7 +181,7 @@ func (s *StateDB) AddLog(log *types.Log) {
} }
// GetLogs returns the logs matching the specified transaction hash, and annotates // GetLogs returns the logs matching the specified transaction hash, and annotates
// them with the given blockNumber and blockHash. // them with the given block attributes.
func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common.Hash, blockTime uint64) []*types.Log { func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common.Hash, blockTime uint64) []*types.Log {
logs := s.logs[hash] logs := s.logs[hash]
for _, l := range logs { for _, l := range logs {
@ -217,6 +192,7 @@ func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common
return logs return logs
} }
// Logs returns the un-annotated logs in order.
func (s *StateDB) Logs() []*types.Log { func (s *StateDB) Logs() []*types.Log {
logs := make([]*types.Log, 0, s.logSize) logs := make([]*types.Log, 0, s.logSize)
for _, lgs := range s.logs { for _, lgs := range s.logs {
@ -296,6 +272,9 @@ func (s *StateDB) TxIndex() int {
func (s *StateDB) GetCode(addr common.Address) []byte { func (s *StateDB) GetCode(addr common.Address) []byte {
stateObject := s.getStateObject(addr) stateObject := s.getStateObject(addr)
if stateObject != nil { if stateObject != nil {
if s.witness != nil {
s.witness.AddCode(stateObject.Code())
}
return stateObject.Code() return stateObject.Code()
} }
return nil return nil
@ -304,6 +283,9 @@ func (s *StateDB) GetCode(addr common.Address) []byte {
func (s *StateDB) GetCodeSize(addr common.Address) int { func (s *StateDB) GetCodeSize(addr common.Address) int {
stateObject := s.getStateObject(addr) stateObject := s.getStateObject(addr)
if stateObject != nil { if stateObject != nil {
if s.witness != nil {
s.witness.AddCode(stateObject.Code())
}
return stateObject.CodeSize() return stateObject.CodeSize()
} }
return 0 return 0
@ -502,14 +484,13 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
if _, ok := s.stateObjectsDestruct[addr]; ok { if _, ok := s.stateObjectsDestruct[addr]; ok {
return nil return nil
} }
s.AccountLoaded++
start := time.Now() start := time.Now()
acct, err := s.reader.Account(addr) acct, err := s.reader.Account(addr)
if err != nil { if err != nil {
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err)) s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
return nil return nil
} }
s.AccountLoaded++
s.AccountReads += time.Since(start) s.AccountReads += time.Since(start)
// Short circuit if the account is not found // Short circuit if the account is not found
@ -611,6 +592,9 @@ func (s *StateDB) Copy() *StateDB {
transientStorage: s.transientStorage.Copy(), transientStorage: s.transientStorage.Copy(),
journal: s.journal.copy(), journal: s.journal.copy(),
} }
if s.witness != nil {
state.witness = s.witness.Copy()
}
if s.accessEvents != nil { if s.accessEvents != nil {
state.accessEvents = s.accessEvents.Copy() state.accessEvents = s.accessEvents.Copy()
} }
@ -756,6 +740,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue continue
} }
op.precedingDelete = false op.precedingDelete = false
delAddrs = append(delAddrs, addr) delAddrs = append(delAddrs, addr)
delAccts = append(delAccts, AccountMut{Account: nil}) delAccts = append(delAccts, AccountMut{Account: nil})
} }
@ -764,6 +749,7 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
s.setError(err) s.setError(err)
return common.Hash{} return common.Hash{}
} }
s.AccountDeleted += len(delAddrs)
} }
s.AccountUpdates += time.Since(start) s.AccountUpdates += time.Since(start)
@ -797,14 +783,20 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
if op.isDelete() { if op.isDelete() {
accounts = append(accounts, AccountMut{Account: nil}) accounts = append(accounts, AccountMut{Account: nil})
s.AccountDeleted += 1
continue continue
} }
obj := s.stateObjects[addr] obj := s.stateObjects[addr]
mut := AccountMut{Account: &obj.data} mut := AccountMut{Account: &obj.data}
if obj.dirtyCode { if obj.dirtyCode {
mut.Code = &CodeMut{Code: obj.code} mut.Code = &CodeMut{Code: obj.code}
// Count code writes post-Finalise so reverted CREATEs are excluded.
s.CodeUpdated += 1
s.CodeUpdateBytes += len(obj.code)
} }
accounts = append(accounts, mut) accounts = append(accounts, mut)
s.AccountUpdated += 1
} }
if err := s.hasher.UpdateAccount(addresses, accounts); err != nil { if err := s.hasher.UpdateAccount(addresses, accounts); err != nil {
s.setError(err) s.setError(err)
@ -932,8 +924,7 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err) return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
} }
op.storages = storages op.storages, op.storagesOrigin = storages, storagesOrigin
op.storagesOrigin = storagesOrigin
// Aggregate the associated trie node changes. // Aggregate the associated trie node changes.
if err := nodes.Merge(set); err != nil { if err := nodes.Merge(set); err != nil {
@ -957,15 +948,6 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
if s.dbErr != nil { if s.dbErr != nil {
return nil, fmt.Errorf("commit aborted due to database error: %v", s.dbErr) return nil, fmt.Errorf("commit aborted due to database error: %v", s.dbErr)
} }
// Commit objects to the trie, measuring the elapsed time
var (
accountTrieNodesUpdated int
accountTrieNodesDeleted int
storageTrieNodesUpdated int
storageTrieNodesDeleted int
updates = make(map[common.Hash]*accountUpdate, len(s.mutations)) // aggregated account updates
)
// Given that some accounts could be destroyed and then recreated within // Given that some accounts could be destroyed and then recreated within
// the same block, account deletions must be processed first. This ensures // the same block, account deletions must be processed first. This ensures
// that the storage trie nodes deleted during destruction and recreated // that the storage trie nodes deleted during destruction and recreated
@ -974,6 +956,8 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Aggregated account updates
updates := make(map[common.Hash]*accountUpdate, len(s.mutations))
for addr, op := range s.mutations { for addr, op := range s.mutations {
if op.isDelete() { if op.isDelete() {
continue continue
@ -992,29 +976,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
// Handle all state updates afterwards, concurrently to one another to shave // Handle all state updates afterwards, concurrently to one another to shave
// off some milliseconds from the commit operation. Also accumulate the code // off some milliseconds from the commit operation. Also accumulate the code
// writes to run in parallel with the computations. // writes to run in parallel with the computations.
start := time.Now()
root, set, secondaryHashes, err := s.hasher.Commit() root, set, secondaryHashes, err := s.hasher.Commit()
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.HasherCommits = time.Since(start)
if err := nodes.MergeSet(set); err != nil { if err := nodes.MergeSet(set); err != nil {
return nil, err return nil, err
} }
accountReadMeters.Mark(int64(s.AccountLoaded))
storageReadMeters.Mark(int64(s.StorageLoaded))
storageUpdatedMeter.Mark(s.StorageUpdated.Load())
accountDeletedMeter.Mark(int64(s.AccountDeleted))
storageDeletedMeter.Mark(s.StorageDeleted.Load())
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
// Clear the metric markers
s.AccountLoaded, s.AccountDeleted = 0, 0
s.StorageLoaded = 0
s.StorageUpdated.Store(0)
s.StorageDeleted.Store(0)
// Clear all internal flags and update state root at the end. // Clear all internal flags and update state root at the end.
s.mutations = make(map[common.Address]*mutation) s.mutations = make(map[common.Address]*mutation)
s.stateObjectsDestruct = make(map[common.Address]*stateObject) s.stateObjectsDestruct = make(map[common.Address]*stateObject)
@ -1022,6 +993,12 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
origin := s.originalRoot origin := s.originalRoot
s.originalRoot = root s.originalRoot = root
if s.witness != nil {
builder, ok := s.hasher.(WitnessCollector)
if ok {
builder.CollectWitness(s.witness)
}
}
return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes, secondaryHashes), nil return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes, secondaryHashes), nil
} }
@ -1160,7 +1137,7 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre
// Witness retrieves the current state witness being collected. // Witness retrieves the current state witness being collected.
func (s *StateDB) Witness() *stateless.Witness { func (s *StateDB) Witness() *stateless.Witness {
return nil return s.witness
} }
func (s *StateDB) AccessEvents() *AccessEvents { func (s *StateDB) AccessEvents() *AccessEvents {

View file

@ -0,0 +1,61 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// 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 state
import (
"sync/atomic"
"time"
)
// Stats contains all measurements gathered during state execution for
// debugging and metrics purposes.
type Stats struct {
AccountReads time.Duration // Account read time
StorageReads time.Duration // Storage read time
CodeReads time.Duration // Code read time
AccountHashes time.Duration // Account trie hash time
AccountUpdates time.Duration // Account trie update time
StorageUpdates time.Duration // Storage trie update and hash time
HasherCommits time.Duration // Trie commit time
DatabaseCommits time.Duration // Database commit time
AccountLoaded int // Number of accounts retrieved from the database during the state transition
AccountUpdated int // Number of accounts updated during the state transition
AccountDeleted int // Number of accounts deleted during the state transition
StorageLoaded int // Number of storage slots retrieved from the database during the state transition
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
// CodeLoadBytes is the total number of bytes read from contract code.
// This value may be smaller than the actual number of bytes read, since
// some APIs (e.g. CodeSize) may load the entire code from either the
// cache or the database when the size is not available in the cache.
CodeLoaded int // Number of contract code loaded during the state transition
CodeLoadBytes int // Total bytes of resolved code
CodeUpdated int // Number of contracts with code changes that persisted
CodeUpdateBytes int // Total bytes of persisted code written
}
// StateReadTime returns the total time spent on the state read.
func (s *Stats) StateReadTime() time.Duration {
return s.AccountReads + s.StorageReads + s.CodeReads
}
// StateHashTime returns the total time spent on the state hash.
func (s *Stats) StateHashTime() time.Duration {
return s.AccountHashes + s.AccountUpdates + s.StorageUpdates
}

View file

@ -80,11 +80,6 @@ func (env *environment) txFitsSize(tx *types.Transaction) bool {
return env.size+tx.Size() < params.MaxBlockSize-maxBlockSizeBufferZone return env.size+tx.Size() < params.MaxBlockSize-maxBlockSizeBufferZone
} }
// discard terminates the background threads before discarding it.
func (env *environment) discard() {
env.state.StopPrefetcher()
}
const ( const (
commitInterruptNone int32 = iota commitInterruptNone int32 = iota
commitInterruptNewHead commitInterruptNewHead
@ -147,8 +142,6 @@ func (miner *Miner) generateWork(ctx context.Context, genParam *generateParams,
if err != nil { if err != nil {
return &newPayloadResult{err: err} return &newPayloadResult{err: err}
} }
defer work.discard()
// Check withdrawals fit max block size. // Check withdrawals fit max block size.
// Due to the cap on withdrawal count, this can actually never happen, but we still need to // Due to the cap on withdrawal count, this can actually never happen, but we still need to
// check to ensure the CL notices there's a problem if the withdrawal cap is ever lifted. // check to ensure the CL notices there's a problem if the withdrawal cap is ever lifted.
@ -334,8 +327,8 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
if err != nil { if err != nil {
return nil, err return nil, err
} }
state.TraceWitness(bundle)
} }
state.StartPrefetcher("miner", bundle)
// Note the passed coinbase may be different with header.Coinbase. // Note the passed coinbase may be different with header.Coinbase.
return &environment{ return &environment{
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time), signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),