From 4f59baa7d1a7435b9ef59749ee9d4aa5b024cc04 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Fri, 30 Jan 2026 12:06:17 +0800 Subject: [PATCH 1/2] core: rework state reader with stats --- core/blockchain.go | 8 +- core/blockchain_stats.go | 38 ++++----- core/state/database.go | 7 +- core/state/reader.go | 151 ++++++++++++++---------------------- core/state/reader_stater.go | 82 ++++++++++++++++++++ 5 files changed, 165 insertions(+), 121 deletions(-) create mode 100644 core/state/reader_stater.go diff --git a/core/blockchain.go b/core/blockchain.go index 6f1db96463..1f81eae78a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2108,8 +2108,12 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s // Upload the statistics of reader at the end defer func() { if result != nil { - result.stats.StatePrefetchCacheStats = prefetch.GetStats() - result.stats.StateReadCacheStats = process.GetStats() + if stater, ok := prefetch.(state.ReaderStater); ok { + result.stats.StatePrefetchCacheStats = stater.GetStats() + } + if stater, ok := process.(state.ReaderStater); ok { + result.stats.StateReadCacheStats = stater.GetStats() + } } }() go func(start time.Time, throwaway *state.StateDB, block *types.Block) { diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index adc66266c4..5f207ccfdf 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -94,15 +94,15 @@ func (s *ExecuteStats) reportMetrics() { chainMgaspsMeter.Update(time.Duration(s.MgasPerSecond)) // TODO(rjl493456442) generalize the ResettingTimer // Cache hit rates - accountCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.AccountCacheHit) - accountCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.AccountCacheMiss) - storageCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StorageCacheHit) - storageCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StorageCacheMiss) + accountCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.AccountCacheHit) + accountCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.AccountCacheMiss) + storageCacheHitPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.StorageCacheHit) + storageCacheMissPrefetchMeter.Mark(s.StatePrefetchCacheStats.StateStats.StorageCacheMiss) - accountCacheHitMeter.Mark(s.StateReadCacheStats.AccountCacheHit) - accountCacheMissMeter.Mark(s.StateReadCacheStats.AccountCacheMiss) - storageCacheHitMeter.Mark(s.StateReadCacheStats.StorageCacheHit) - storageCacheMissMeter.Mark(s.StateReadCacheStats.StorageCacheMiss) + accountCacheHitMeter.Mark(s.StateReadCacheStats.StateStats.AccountCacheHit) + accountCacheMissMeter.Mark(s.StateReadCacheStats.StateStats.AccountCacheMiss) + storageCacheHitMeter.Mark(s.StateReadCacheStats.StateStats.StorageCacheHit) + storageCacheMissMeter.Mark(s.StateReadCacheStats.StateStats.StorageCacheMiss) } // slowBlockLog represents the JSON structure for slow block logging. @@ -177,14 +177,6 @@ type slowBlockCodeCacheEntry struct { MissBytes int64 `json:"miss_bytes"` } -// calculateHitRate computes the cache hit rate as a percentage (0-100). -func calculateHitRate(hits, misses int64) float64 { - if total := hits + misses; total > 0 { - return float64(hits) / float64(total) * 100.0 - } - return 0.0 -} - // durationToMs converts a time.Duration to milliseconds as a float64 // with sub-millisecond precision for accurate cross-client metrics. func durationToMs(d time.Duration) float64 { @@ -238,19 +230,19 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat }, Cache: slowBlockCache{ Account: slowBlockCacheEntry{ - Hits: s.StateReadCacheStats.AccountCacheHit, - Misses: s.StateReadCacheStats.AccountCacheMiss, - HitRate: calculateHitRate(s.StateReadCacheStats.AccountCacheHit, s.StateReadCacheStats.AccountCacheMiss), + Hits: s.StateReadCacheStats.StateStats.AccountCacheHit, + Misses: s.StateReadCacheStats.StateStats.AccountCacheMiss, + HitRate: s.StateReadCacheStats.StateStats.AccountCacheHitRate(), }, Storage: slowBlockCacheEntry{ - Hits: s.StateReadCacheStats.StorageCacheHit, - Misses: s.StateReadCacheStats.StorageCacheMiss, - HitRate: calculateHitRate(s.StateReadCacheStats.StorageCacheHit, s.StateReadCacheStats.StorageCacheMiss), + Hits: s.StateReadCacheStats.StateStats.StorageCacheHit, + Misses: s.StateReadCacheStats.StateStats.StorageCacheMiss, + HitRate: s.StateReadCacheStats.StateStats.StorageCacheHitRate(), }, Code: slowBlockCodeCacheEntry{ Hits: s.StateReadCacheStats.CodeStats.CacheHit, Misses: s.StateReadCacheStats.CodeStats.CacheMiss, - HitRate: calculateHitRate(s.StateReadCacheStats.CodeStats.CacheHit, s.StateReadCacheStats.CodeStats.CacheMiss), + HitRate: s.StateReadCacheStats.CodeStats.HitRate(), HitBytes: s.StateReadCacheStats.CodeStats.CacheHitBytes, MissBytes: s.StateReadCacheStats.CodeStats.CacheMissBytes, }, diff --git a/core/state/database.go b/core/state/database.go index 4a5547d075..16f30dadf1 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -224,15 +224,14 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) { // ReadersWithCacheStats creates a pair of state readers that share the same // underlying state reader and internal state cache, while maintaining separate // statistics respectively. -func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithStats, ReaderWithStats, error) { +func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reader, error) { r, err := db.StateReader(stateRoot) if err != nil { return nil, nil, err } sr := newStateReaderWithCache(r) - - ra := newReaderWithStats(sr, newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache)) - rb := newReaderWithStats(sr, newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache)) + ra := newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newStateReaderWithStats(sr)) + rb := newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newStateReaderWithStats(sr)) return ra, rb, nil } diff --git a/core/state/reader.go b/core/state/reader.go index 35b732173b..0835d9a0ef 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -18,7 +18,6 @@ package state import ( "errors" - "fmt" "sync" "sync/atomic" @@ -38,6 +37,8 @@ import ( ) // ContractCodeReader defines the interface for accessing contract code. +// +// ContractCodeReader is supposed to be thread-safe. type ContractCodeReader interface { // Has returns the flag indicating whether the contract code with // specified address and hash exists or not. @@ -58,35 +59,10 @@ type ContractCodeReader interface { CodeSize(addr common.Address, codeHash common.Hash) (int, error) } -// ContractCodeReaderStats aggregates statistics for the contract code reader. -type ContractCodeReaderStats struct { - CacheHit int64 // Number of cache hits - CacheMiss int64 // Number of cache misses - CacheHitBytes int64 // Total bytes served from cache - CacheMissBytes int64 // Total bytes read on cache misses -} - -// HitRate returns the cache hit rate. -func (s ContractCodeReaderStats) HitRate() float64 { - if s.CacheHit == 0 { - return 0 - } - return float64(s.CacheHit) / float64(s.CacheHit+s.CacheMiss) -} - -// ContractCodeReaderWithStats extends ContractCodeReader by adding GetStats to -// expose statistics of code reader. -type ContractCodeReaderWithStats interface { - ContractCodeReader - - GetStats() ContractCodeReaderStats -} - // StateReader defines the interface for accessing accounts and storage slots // associated with a specific state. // -// StateReader is assumed to be thread-safe and implementation must take care -// of the concurrency issue by themselves. +// StateReader is supposed to be thread-safe. type StateReader interface { // Account retrieves the account associated with a particular address. // @@ -114,40 +90,6 @@ type Reader interface { StateReader } -// ReaderStats wraps the statistics of reader. -type ReaderStats struct { - AccountCacheHit int64 - AccountCacheMiss int64 - StorageCacheHit int64 - StorageCacheMiss int64 - CodeStats ContractCodeReaderStats -} - -// String implements fmt.Stringer, returning string format statistics. -func (s ReaderStats) String() string { - var ( - accountCacheHitRate float64 - storageCacheHitRate float64 - ) - if s.AccountCacheHit > 0 { - accountCacheHitRate = float64(s.AccountCacheHit) / float64(s.AccountCacheHit+s.AccountCacheMiss) * 100 - } - if s.StorageCacheHit > 0 { - storageCacheHitRate = float64(s.StorageCacheHit) / float64(s.StorageCacheHit+s.StorageCacheMiss) * 100 - } - msg := fmt.Sprintf("Reader statistics\n") - msg += fmt.Sprintf("account: hit: %d, miss: %d, rate: %.2f\n", s.AccountCacheHit, s.AccountCacheMiss, accountCacheHitRate) - msg += fmt.Sprintf("storage: hit: %d, miss: %d, rate: %.2f\n", s.StorageCacheHit, s.StorageCacheMiss, storageCacheHitRate) - msg += fmt.Sprintf("code: hit: %d(%v), miss: %d(%v), rate: %.2f\n", s.CodeStats.CacheHit, common.StorageSize(s.CodeStats.CacheHitBytes), s.CodeStats.CacheMiss, common.StorageSize(s.CodeStats.CacheMissBytes), s.CodeStats.HitRate()) - return msg -} - -// ReaderWithStats wraps the additional method to retrieve the reader statistics from. -type ReaderWithStats interface { - Reader - GetStats() ReaderStats -} - // cachingCodeReader implements ContractCodeReader, accessing contract code either in // local key-value store or the shared code cache. // @@ -210,15 +152,16 @@ func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash) return len(code), nil } -// Has returns the flag indicating whether the contract code with -// specified address and hash exists or not. +// Has implements ContractCodeReader, returning the flag indicating whether +// the contract code with specified address and hash exists or not. func (r *cachingCodeReader) Has(addr common.Address, codeHash common.Hash) bool { code, _ := r.Code(addr, codeHash) return len(code) > 0 } -// GetStats returns the statistics of the code reader. -func (r *cachingCodeReader) GetStats() ContractCodeReaderStats { +// GetCodeStats implements ContractCodeReaderStater, returning the statistics +// of the code reader. +func (r *cachingCodeReader) GetCodeStats() ContractCodeReaderStats { return ContractCodeReaderStats{ CacheHit: r.hit.Load(), CacheMiss: r.miss.Load(), @@ -495,20 +438,6 @@ func (r *multiStateReader) Storage(addr common.Address, slot common.Hash) (commo return common.Hash{}, errors.Join(errs...) } -// reader is the wrapper of ContractCodeReader and StateReader interface. -type reader struct { - ContractCodeReader - StateReader -} - -// newReader constructs a reader with the supplied code reader and state reader. -func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { - return &reader{ - ContractCodeReader: codeReader, - StateReader: stateReader, - } -} - // stateReaderWithCache is a wrapper around StateReader that maintains additional // state caches to support concurrent state access. type stateReaderWithCache struct { @@ -619,9 +548,10 @@ func (r *stateReaderWithCache) Storage(addr common.Address, slot common.Hash) (c return value, err } -type readerWithStats struct { +// stateReaderWithStats is a wrapper over the stateReaderWithCache, tracking +// the cache hit statistics of the reader. +type stateReaderWithStats struct { *stateReaderWithCache - ContractCodeReaderWithStats accountCacheHit atomic.Int64 accountCacheMiss atomic.Int64 @@ -629,11 +559,10 @@ type readerWithStats struct { storageCacheMiss atomic.Int64 } -// newReaderWithStats constructs the reader with additional statistics tracked. -func newReaderWithStats(sr *stateReaderWithCache, cr ContractCodeReaderWithStats) *readerWithStats { - return &readerWithStats{ - stateReaderWithCache: sr, - ContractCodeReaderWithStats: cr, +// newReaderWithStats constructs the state reader with additional statistics tracked. +func newStateReaderWithStats(sr *stateReaderWithCache) *stateReaderWithStats { + return &stateReaderWithStats{ + stateReaderWithCache: sr, } } @@ -641,7 +570,7 @@ func newReaderWithStats(sr *stateReaderWithCache, cr ContractCodeReaderWithStats // The returned account might be nil if it's not existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *readerWithStats) Account(addr common.Address) (*types.StateAccount, error) { +func (r *stateReaderWithStats) Account(addr common.Address) (*types.StateAccount, error) { account, incache, err := r.stateReaderWithCache.account(addr) if err != nil { return nil, err @@ -659,7 +588,7 @@ func (r *readerWithStats) Account(addr common.Address) (*types.StateAccount, err // existent. // // An error will be returned if the state is corrupted in the underlying reader. -func (r *readerWithStats) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { +func (r *stateReaderWithStats) Storage(addr common.Address, slot common.Hash) (common.Hash, error) { value, incache, err := r.stateReaderWithCache.storage(addr, slot) if err != nil { return common.Hash{}, err @@ -672,13 +601,51 @@ func (r *readerWithStats) Storage(addr common.Address, slot common.Hash) (common return value, nil } -// GetStats implements ReaderWithStats, returning the statistics of state reader. -func (r *readerWithStats) GetStats() ReaderStats { - return ReaderStats{ +// GetStateStats implements StateReaderStater, returning the statistics of the +// state reader. +func (r *stateReaderWithStats) GetStateStats() StateReaderStats { + return StateReaderStats{ AccountCacheHit: r.accountCacheHit.Load(), AccountCacheMiss: r.accountCacheMiss.Load(), StorageCacheHit: r.storageCacheHit.Load(), StorageCacheMiss: r.storageCacheMiss.Load(), - CodeStats: r.ContractCodeReaderWithStats.GetStats(), + } +} + +// reader aggregates a code reader and a state reader into a single object. +type reader struct { + ContractCodeReader + StateReader +} + +// newReader constructs a reader with the supplied code reader and state reader. +func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { + return &reader{ + ContractCodeReader: codeReader, + StateReader: stateReader, + } +} + +// GetCodeStats returns the statistics of code access. +func (r *reader) GetCodeStats() ContractCodeReaderStats { + if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok { + return stater.GetCodeStats() + } + return ContractCodeReaderStats{} +} + +// GetStateStats returns the statistics of state access. +func (r *reader) GetStateStats() StateReaderStats { + if stater, ok := r.StateReader.(StateReaderStater); ok { + return stater.GetStateStats() + } + return StateReaderStats{} +} + +// GetStats returns the aggregated statistics for both state and code access. +func (r *reader) GetStats() ReaderStats { + return ReaderStats{ + CodeStats: r.GetCodeStats(), + StateStats: r.GetStateStats(), } } diff --git a/core/state/reader_stater.go b/core/state/reader_stater.go new file mode 100644 index 0000000000..5294275953 --- /dev/null +++ b/core/state/reader_stater.go @@ -0,0 +1,82 @@ +// 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 . + +package state + +// ContractCodeReaderStats aggregates statistics for the contract code reader. +type ContractCodeReaderStats struct { + CacheHit int64 // Number of cache hits + CacheMiss int64 // Number of cache misses + CacheHitBytes int64 // Total bytes served from cache + CacheMissBytes int64 // Total bytes read on cache misses +} + +// HitRate returns the cache hit rate in percentage. +func (s ContractCodeReaderStats) HitRate() float64 { + total := s.CacheHit + s.CacheMiss + if total == 0 { + return 0 + } + return float64(s.CacheHit) / float64(total) * 100 +} + +// ContractCodeReaderStater wraps the method to retrieve the statistics of +// contract code reader. +type ContractCodeReaderStater interface { + GetCodeStats() ContractCodeReaderStats +} + +// StateReaderStats aggregates statistics for the state reader. +type StateReaderStats struct { + AccountCacheHit int64 // Number of account cache hits + AccountCacheMiss int64 // Number of account cache misses + StorageCacheHit int64 // Number of storage cache hits + StorageCacheMiss int64 // Number of storage cache misses +} + +// AccountCacheHitRate returns the cache hit rate of account requests in percentage. +func (s StateReaderStats) AccountCacheHitRate() float64 { + total := s.AccountCacheHit + s.AccountCacheMiss + if total == 0 { + return 0 + } + return float64(s.AccountCacheHit) / float64(total) * 100 +} + +// StorageCacheHitRate returns the cache hit rate of storage requests in percentage. +func (s StateReaderStats) StorageCacheHitRate() float64 { + total := s.StorageCacheHit + s.StorageCacheMiss + if total == 0 { + return 0 + } + return float64(s.StorageCacheHit) / float64(total) * 100 +} + +// StateReaderStater wraps the method to retrieve the statistics of state reader. +type StateReaderStater interface { + GetStateStats() StateReaderStats +} + +// ReaderStats wraps the statistics of reader. +type ReaderStats struct { + CodeStats ContractCodeReaderStats + StateStats StateReaderStats +} + +// ReaderStater defines the capability to retrieve aggregated statistics. +type ReaderStater interface { + GetStats() ReaderStats +} From a362a1077995b6c026d6f6d71dbed1f0d0c8d6c1 Mon Sep 17 00:00:00 2001 From: Gary Rong Date: Wed, 11 Feb 2026 11:18:44 +0800 Subject: [PATCH 2/2] core: implement code database --- core/blockchain.go | 25 ++-- core/blockchain_reader.go | 18 +-- core/blockchain_sethead_test.go | 2 - core/blockchain_stats.go | 8 +- core/blockchain_test.go | 2 +- core/state/database.go | 105 +++++++++------ core/state/database_code.go | 231 ++++++++++++++++++++++++++++++++ core/state/database_history.go | 26 ++-- core/state/iterator.go | 5 +- core/state/reader.go | 101 +------------- core/state/state_object.go | 10 +- core/state/statedb.go | 45 ++----- core/state/statedb_fuzz_test.go | 2 +- core/state/statedb_test.go | 4 +- core/state/stateupdate.go | 6 +- core/state/sync_test.go | 20 +-- miner/miner_test.go | 2 +- tests/state_test_util.go | 2 +- triedb/hashdb/database.go | 3 + 19 files changed, 367 insertions(+), 250 deletions(-) create mode 100644 core/state/database_code.go diff --git a/core/blockchain.go b/core/blockchain.go index 1f81eae78a..fc354316af 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -91,9 +91,7 @@ var ( accountReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/account/single/reads", nil) storageReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/storage/single/reads", nil) codeReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/code/single/reads", nil) - - snapshotCommitTimer = metrics.NewRegisteredResettingTimer("chain/snapshot/commits", nil) - triedbCommitTimer = metrics.NewRegisteredResettingTimer("chain/triedb/commits", nil) + triedbCommitTimer = metrics.NewRegisteredResettingTimer("chain/triedb/commits", nil) blockInsertTimer = metrics.NewRegisteredResettingTimer("chain/inserts", nil) blockValidationTimer = metrics.NewRegisteredResettingTimer("chain/validation", nil) @@ -320,7 +318,7 @@ type BlockChain struct { lastWrite uint64 // Last block when the state was flushed flushInterval atomic.Int64 // Time interval (processing time) after which to flush a state triedb *triedb.Database // The database handler for maintaining trie nodes. - statedb *state.CachingDB // State database to reuse between imports (contains state cache) + codedb *state.CodeDB // The database handler for maintaining contract codes. txIndexer *txIndexer // Transaction indexer, might be nil if not enabled hc *HeaderChain @@ -402,6 +400,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, cfg: cfg, db: db, triedb: triedb, + codedb: state.NewCodeDB(db), triegc: prque.New[int64, common.Hash](nil), chainmu: syncx.NewClosableMutex(), bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit), @@ -418,7 +417,6 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, return nil, err } bc.flushInterval.Store(int64(cfg.TrieTimeLimit)) - bc.statedb = state.NewDatabase(bc.triedb, nil) bc.validator = NewBlockValidator(chainConfig, bc) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc) @@ -595,9 +593,6 @@ func (bc *BlockChain) setupSnapshot() { AsyncBuild: !bc.cfg.SnapshotWait, } bc.snaps, _ = snapshot.New(snapconfig, bc.db, bc.triedb, head.Root) - - // Re-initialize the state database with snapshot - bc.statedb = state.NewDatabase(bc.triedb, bc.snaps) } } @@ -2079,11 +2074,12 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s startTime = time.Now() statedb *state.StateDB interrupt atomic.Bool + sdb = state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps) ) defer interrupt.Store(true) // terminate the prefetch at the end if bc.cfg.NoPrefetch { - statedb, err = state.New(parentRoot, bc.statedb) + statedb, err = state.New(parentRoot, sdb) if err != nil { return nil, err } @@ -2093,15 +2089,15 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s // // Note: the main processor and prefetcher share the same reader with a local // cache for mitigating the overhead of state access. - prefetch, process, err := bc.statedb.ReadersWithCacheStats(parentRoot) + prefetch, process, err := sdb.ReadersWithCacheStats(parentRoot) if err != nil { return nil, err } - throwaway, err := state.NewWithReader(parentRoot, bc.statedb, prefetch) + throwaway, err := state.NewWithReader(parentRoot, sdb, prefetch) if err != nil { return nil, err } - statedb, err = state.NewWithReader(parentRoot, bc.statedb, process) + statedb, err = state.NewWithReader(parentRoot, sdb, process) if err != nil { return nil, err } @@ -2262,9 +2258,8 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s // Update the metrics touched during block commit stats.AccountCommits = statedb.AccountCommits // Account commits are complete, we can mark them stats.StorageCommits = statedb.StorageCommits // Storage commits are complete, we can mark them - stats.SnapshotCommit = statedb.SnapshotCommits // Snapshot commits are complete, we can mark them - stats.TrieDBCommit = statedb.TrieDBCommits // Trie database commits are complete, we can mark them - stats.BlockWrite = time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.SnapshotCommits - statedb.TrieDBCommits + 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 elapsed := time.Since(startTime) + 1 // prevent zero division stats.TotalTime = elapsed diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index ee15c152c4..f1b40d0d0c 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -371,7 +371,7 @@ func (bc *BlockChain) TxIndexDone() bool { // HasState checks if state trie is fully present in the database or not. func (bc *BlockChain) HasState(hash common.Hash) bool { - _, err := bc.statedb.OpenTrie(hash) + _, err := bc.triedb.NodeReader(hash) return err == nil } @@ -403,7 +403,7 @@ func (bc *BlockChain) stateRecoverable(root common.Hash) bool { func (bc *BlockChain) ContractCodeWithPrefix(hash common.Hash) []byte { // TODO(rjl493456442) The associated account address is also required // in Verkle scheme. Fix it once snap-sync is supported for Verkle. - return bc.statedb.ContractCodeWithPrefix(common.Address{}, hash) + return bc.codedb.Reader().CodeWithPrefix(common.Address{}, hash) } // State returns a new mutable state based on the current HEAD block. @@ -413,14 +413,14 @@ func (bc *BlockChain) State() (*state.StateDB, error) { // StateAt returns a new mutable state based on a particular point in time. func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) { - return state.New(root, bc.statedb) + return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)) } // HistoricState returns a historic state specified by the given root. // Live states are not available and won't be served, please use `State` // or `StateAt` instead. func (bc *BlockChain) HistoricState(root common.Hash) (*state.StateDB, error) { - return state.New(root, state.NewHistoricDatabase(bc.db, bc.triedb)) + return state.New(root, state.NewHistoricDatabase(bc.triedb, bc.codedb)) } // Config retrieves the chain's fork configuration. @@ -444,11 +444,6 @@ func (bc *BlockChain) Processor() Processor { return bc.processor } -// StateCache returns the caching database underpinning the blockchain instance. -func (bc *BlockChain) StateCache() state.Database { - return bc.statedb -} - // GasLimit returns the gas limit of the current HEAD block. func (bc *BlockChain) GasLimit() uint64 { return bc.CurrentBlock().GasLimit @@ -492,6 +487,11 @@ func (bc *BlockChain) TrieDB() *triedb.Database { return bc.triedb } +// CodeDB retrieves the low level contract code database used for data storage. +func (bc *BlockChain) CodeDB() *state.CodeDB { + return bc.codedb +} + // HeaderChain returns the underlying header chain. func (bc *BlockChain) HeaderChain() *HeaderChain { return bc.hc diff --git a/core/blockchain_sethead_test.go b/core/blockchain_sethead_test.go index 72ca15d7f6..f2fbc003f1 100644 --- a/core/blockchain_sethead_test.go +++ b/core/blockchain_sethead_test.go @@ -30,7 +30,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb/pebble" "github.com/ethereum/go-ethereum/params" @@ -2041,7 +2040,6 @@ func testSetHeadWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme dbconfig.HashDB = hashdb.Defaults } chain.triedb = triedb.NewDatabase(chain.db, dbconfig) - chain.statedb = state.NewDatabase(chain.triedb, chain.snaps) // Force run a freeze cycle type freezer interface { diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index 5f207ccfdf..d753b0b700 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -52,8 +52,7 @@ type ExecuteStats struct { 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 - SnapshotCommit time.Duration // Time spent on snapshot commit - TrieDBCommit time.Duration // Time spent on database commit + 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 @@ -87,8 +86,7 @@ func (s *ExecuteStats) reportMetrics() { blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing blockValidationTimer.Update(s.Validation) // The time spent on block validation blockCrossValidationTimer.Update(s.CrossValidation) // The time spent on stateless cross validation - snapshotCommitTimer.Update(s.SnapshotCommit) // Snapshot commits are complete, we can mark them - triedbCommitTimer.Update(s.TrieDBCommit) // Trie database commits are complete, we can mark them + triedbCommitTimer.Update(s.DatabaseCommit) // Trie database commits are complete, we can mark them blockWriteTimer.Update(s.BlockWrite) // The time spent on block write blockInsertTimer.Update(s.TotalTime) // The total time spent on block execution chainMgaspsMeter.Update(time.Duration(s.MgasPerSecond)) // TODO(rjl493456442) generalize the ResettingTimer @@ -208,7 +206,7 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat ExecutionMs: durationToMs(s.Execution), StateReadMs: durationToMs(s.AccountReads + s.StorageReads + s.CodeReads), StateHashMs: durationToMs(s.AccountHashes + s.AccountUpdates + s.StorageUpdates), - CommitMs: durationToMs(max(s.AccountCommits, s.StorageCommits) + s.TrieDBCommit + s.SnapshotCommit + s.BlockWrite), + CommitMs: durationToMs(max(s.AccountCommits, s.StorageCommits) + s.DatabaseCommit + s.BlockWrite), TotalMs: durationToMs(s.TotalTime), }, Throughput: slowBlockThru{ diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 73ffce93fb..89fd19d29f 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -156,7 +156,7 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { } return err } - statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), blockchain.statedb) + statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), state.NewDatabase(blockchain.triedb, blockchain.codedb)) if err != nil { return err } diff --git a/core/state/database.go b/core/state/database.go index 16f30dadf1..002ce57fbc 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -20,13 +20,13 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/bintrie" "github.com/ethereum/go-ethereum/trie/transitiontrie" @@ -34,14 +34,6 @@ import ( "github.com/ethereum/go-ethereum/triedb" ) -const ( - // Number of codehash->size associations to keep. - codeSizeCacheSize = 1_000_000 // 4 megabytes in total - - // Cache size granted for caching clean code. - codeCacheSize = 256 * 1024 * 1024 -) - // Database wraps access to tries and contract code. type Database interface { // Reader returns a state reader associated with the specified state root. @@ -58,6 +50,11 @@ type Database interface { // Snapshot returns the underlying state snapshot. Snapshot() *snapshot.Tree + + // Commit flushes all pending writes and finalizes the state transition, + // committing the changes to the underlying storage. It returns an error + // if the commit fails. + Commit(update *stateUpdate) error } // Trie is a Ethereum Merkle Patricia trie. @@ -149,32 +146,34 @@ type Trie interface { // state snapshot to provide functionalities for state access. It's meant to be a // long-live object and has a few caches inside for sharing between blocks. type CachingDB struct { - disk ethdb.KeyValueStore - triedb *triedb.Database - snap *snapshot.Tree - codeCache *lru.SizeConstrainedCache[common.Hash, []byte] - codeSizeCache *lru.Cache[common.Hash, int] - - // Transition-specific fields - TransitionStatePerRoot *lru.Cache[common.Hash, *overlay.TransitionState] + triedb *triedb.Database + codedb *CodeDB + snap *snapshot.Tree } // NewDatabase creates a state database with the provided data sources. -func NewDatabase(triedb *triedb.Database, snap *snapshot.Tree) *CachingDB { +func NewDatabase(triedb *triedb.Database, codedb *CodeDB) *CachingDB { + if codedb == nil { + codedb = NewCodeDB(triedb.Disk()) + } return &CachingDB{ - disk: triedb.Disk(), - triedb: triedb, - snap: snap, - codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), - codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), - TransitionStatePerRoot: lru.NewCache[common.Hash, *overlay.TransitionState](1000), + triedb: triedb, + codedb: codedb, } } // NewDatabaseForTesting is similar to NewDatabase, but it initializes the caching // db by using an ephemeral memory db with default config for testing. func NewDatabaseForTesting() *CachingDB { - return NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil) + db := rawdb.NewMemoryDatabase() + return NewDatabase(triedb.NewDatabase(db, nil), NewCodeDB(db)) +} + +// WithSnapshot configures the provided contract code cache. Note that this +// registration must be performed before the cachingDB is used. +func (db *CachingDB) WithSnapshot(snapshot *snapshot.Tree) *CachingDB { + db.snap = snapshot + return db } // StateReader returns a state reader associated with the specified state root. @@ -218,7 +217,7 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) { if err != nil { return nil, err } - return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), sr), nil + return newReader(db.codedb.Reader(), sr), nil } // ReadersWithCacheStats creates a pair of state readers that share the same @@ -230,8 +229,8 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reade return nil, nil, err } sr := newStateReaderWithCache(r) - ra := newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newStateReaderWithStats(sr)) - rb := newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newStateReaderWithStats(sr)) + ra := newReader(db.codedb.Reader(), newStateReaderWithStats(sr)) + rb := newReader(db.codedb.Reader(), newStateReaderWithStats(sr)) return ra, rb, nil } @@ -267,22 +266,6 @@ func (db *CachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Addre return tr, nil } -// ContractCodeWithPrefix retrieves a particular contract's code. If the -// code can't be found in the cache, then check the existence with **new** -// db scheme. -func (db *CachingDB) ContractCodeWithPrefix(address common.Address, codeHash common.Hash) []byte { - code, _ := db.codeCache.Get(codeHash) - if len(code) > 0 { - return code - } - code = rawdb.ReadCodeWithPrefix(db.disk, codeHash) - if len(code) > 0 { - db.codeCache.Add(codeHash, code) - db.codeSizeCache.Add(codeHash, len(code)) - } - return code -} - // TrieDB retrieves any intermediate trie-node caching layer. func (db *CachingDB) TrieDB() *triedb.Database { return db.triedb @@ -293,6 +276,40 @@ func (db *CachingDB) Snapshot() *snapshot.Tree { return db.snap } +// Commit flushes all pending writes and finalizes the state transition, +// committing the changes to the underlying storage. It returns an error +// if the commit fails. +func (db *CachingDB) Commit(update *stateUpdate) error { + // Short circuit if nothing to commit + if update.empty() { + return nil + } + // Commit dirty contract code if any exists + if len(update.codes) > 0 { + batch := db.codedb.NewBatchWithSize(len(update.codes)) + for _, code := range update.codes { + batch.Put(code.hash, code.blob) + } + if err := batch.Commit(); err != nil { + return err + } + } + // If snapshotting is enabled, update the snapshot tree with this new version + if db.snap != nil && db.snap.Snapshot(update.originRoot) != nil { + if err := db.snap.Update(update.root, update.originRoot, update.accounts, update.storages); err != nil { + log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err) + } + // Keep 128 diff layers in the memory, persistent layer is 129th. + // - head layer is paired with HEAD state + // - head-1 layer is paired with HEAD-1 state + // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state + if err := db.snap.Cap(update.root, TriesInMemory); err != nil { + log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err) + } + } + return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet()) +} + // mustCopyTrie returns a deep-copied trie. func mustCopyTrie(t Trie) Trie { switch t := t.(type) { diff --git a/core/state/database_code.go b/core/state/database_code.go new file mode 100644 index 0000000000..820c9c1168 --- /dev/null +++ b/core/state/database_code.go @@ -0,0 +1,231 @@ +// 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 . + +package state + +import ( + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/lru" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" +) + +const ( + // Number of codeHash->size associations to keep. + codeSizeCacheSize = 1_000_000 + + // Cache size granted for caching clean code. + codeCacheSize = 256 * 1024 * 1024 +) + +// CodeCache maintains cached contract code that is shared across blocks, enabling +// fast access for external calls such as RPCs and state transitions. +// +// It is thread-safe and has a bounded size. +type codeCache struct { + codeCache *lru.SizeConstrainedCache[common.Hash, []byte] + codeSizeCache *lru.Cache[common.Hash, int] +} + +// newCodeCache initializes the contract code cache with the predefined capacity. +func newCodeCache() *codeCache { + return &codeCache{ + codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), + codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), + } +} + +// Get returns the contract code associated with the provided code hash. +func (c *codeCache) Get(hash common.Hash) ([]byte, bool) { + return c.codeCache.Get(hash) +} + +// GetSize returns the contract code size associated with the provided code hash. +func (c *codeCache) GetSize(hash common.Hash) (int, bool) { + return c.codeSizeCache.Get(hash) +} + +// Put adds the provided contract code along with its size information into the cache. +func (c *codeCache) Put(hash common.Hash, code []byte) { + c.codeCache.Add(hash, code) + c.codeSizeCache.Add(hash, len(code)) +} + +// CodeReader implements state.ContractCodeReader, accessing contract code either in +// local key-value store or the shared code cache. +// +// Reader is safe for concurrent access. +type CodeReader struct { + db ethdb.KeyValueReader + cache *codeCache + + // Cache statistics + hit atomic.Int64 // Number of code lookups found in the cache + miss atomic.Int64 // Number of code lookups not found in the cache + hitBytes atomic.Int64 // Total number of bytes read from cache + missBytes atomic.Int64 // Total number of bytes read from database +} + +// newCodeReader constructs the code reader with provided key value store and the cache. +func newCodeReader(db ethdb.KeyValueReader, cache *codeCache) *CodeReader { + return &CodeReader{ + db: db, + cache: cache, + } +} + +// Has returns the flag indicating whether the contract code with +// specified address and hash exists or not. +func (r *CodeReader) Has(addr common.Address, codeHash common.Hash) bool { + return len(r.Code(addr, codeHash)) > 0 +} + +// Code implements state.ContractCodeReader, retrieving a particular contract's code. +// Null is returned if the contract code is not present. +func (r *CodeReader) Code(addr common.Address, codeHash common.Hash) []byte { + code, _ := r.cache.Get(codeHash) + if len(code) > 0 { + r.hit.Add(1) + r.hitBytes.Add(int64(len(code))) + return code + } + r.miss.Add(1) + + code = rawdb.ReadCode(r.db, codeHash) + if len(code) > 0 { + r.cache.Put(codeHash, code) + r.missBytes.Add(int64(len(code))) + } + return code +} + +// CodeSize implements state.ContractCodeReader, retrieving a particular contract +// code's size. Zero is returned if the contract code is not present. +func (r *CodeReader) CodeSize(addr common.Address, codeHash common.Hash) int { + if cached, ok := r.cache.GetSize(codeHash); ok { + r.hit.Add(1) + return cached + } + return len(r.Code(addr, codeHash)) +} + +// CodeWithPrefix retrieves the contract code for the specified account address +// and code hash. It is almost identical to Code, but uses rawdb.ReadCodeWithPrefix +// for database lookups. The intention is to gradually deprecate the old +// contract code scheme. +func (r *CodeReader) CodeWithPrefix(addr common.Address, codeHash common.Hash) []byte { + code, _ := r.cache.Get(codeHash) + if len(code) > 0 { + r.hit.Add(1) + r.hitBytes.Add(int64(len(code))) + return code + } + r.miss.Add(1) + + code = rawdb.ReadCodeWithPrefix(r.db, codeHash) + if len(code) > 0 { + r.cache.Put(codeHash, code) + r.missBytes.Add(int64(len(code))) + } + return code +} + +// GetCodeStats implements ContractCodeReaderStater, returning the statistics +// of the code reader. +func (r *CodeReader) GetCodeStats() ContractCodeReaderStats { + return ContractCodeReaderStats{ + CacheHit: r.hit.Load(), + CacheMiss: r.miss.Load(), + CacheHitBytes: r.hitBytes.Load(), + CacheMissBytes: r.missBytes.Load(), + } +} + +type CodeBatch struct { + db *CodeDB + codes [][]byte + codeHashes []common.Hash +} + +// newCodeBatch constructs the batch for writing contract code. +func newCodeBatch(db *CodeDB) *CodeBatch { + return &CodeBatch{ + db: db, + } +} + +// newCodeBatchWithSize constructs the batch with a pre-allocated capacity. +func newCodeBatchWithSize(db *CodeDB, size int) *CodeBatch { + return &CodeBatch{ + db: db, + codes: make([][]byte, 0, size), + codeHashes: make([]common.Hash, 0, size), + } +} + +// Put inserts the given contract code into the writer, waiting for commit. +func (b *CodeBatch) Put(codeHash common.Hash, code []byte) { + b.codes = append(b.codes, code) + b.codeHashes = append(b.codeHashes, codeHash) +} + +// Commit flushes the accumulated dirty contract code into the database and +// also place them in the cache. +func (b *CodeBatch) Commit() error { + batch := b.db.db.NewBatch() + for i, code := range b.codes { + rawdb.WriteCode(batch, b.codeHashes[i], code) + b.db.cache.Put(b.codeHashes[i], code) + } + if err := batch.Write(); err != nil { + return err + } + b.codes = b.codes[:0] + b.codeHashes = b.codeHashes[:0] + return nil +} + +// CodeDB is responsible for managing the contract code and provides the access +// to it. It can be used as a global object, sharing it between multiple entities. +type CodeDB struct { + db ethdb.KeyValueStore + cache *codeCache +} + +// NewCodeDB constructs the contract code database with the provided key value store. +func NewCodeDB(db ethdb.KeyValueStore) *CodeDB { + return &CodeDB{ + db: db, + cache: newCodeCache(), + } +} + +// Reader returns the contract code reader. +func (d *CodeDB) Reader() *CodeReader { + return newCodeReader(d.db, d.cache) +} + +// NewBatch returns the batch for flushing contract codes. +func (d *CodeDB) NewBatch() *CodeBatch { + return newCodeBatch(d) +} + +// NewBatchWithSize returns the batch with pre-allocated capacity. +func (d *CodeDB) NewBatchWithSize(size int) *CodeBatch { + return newCodeBatchWithSize(d, size) +} diff --git a/core/state/database_history.go b/core/state/database_history.go index 7a2be8fe4f..c25c4eae4b 100644 --- a/core/state/database_history.go +++ b/core/state/database_history.go @@ -17,15 +17,14 @@ package state import ( + "errors" "fmt" "sync" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/triedb" @@ -221,19 +220,15 @@ func (r *historicalTrieReader) Storage(addr common.Address, key common.Hash) (co // HistoricDB is the implementation of Database interface, with the ability to // access historical state. type HistoricDB struct { - disk ethdb.KeyValueStore - triedb *triedb.Database - codeCache *lru.SizeConstrainedCache[common.Hash, []byte] - codeSizeCache *lru.Cache[common.Hash, int] + triedb *triedb.Database + codedb *CodeDB } // NewHistoricDatabase creates a historic state database. -func NewHistoricDatabase(disk ethdb.KeyValueStore, triedb *triedb.Database) *HistoricDB { +func NewHistoricDatabase(triedb *triedb.Database, codedb *CodeDB) *HistoricDB { return &HistoricDB{ - disk: disk, - triedb: triedb, - codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), - codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), + triedb: triedb, + codedb: codedb, } } @@ -258,7 +253,7 @@ func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) { if err != nil { return nil, err } - return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), combined), nil + return newReader(db.codedb.Reader(), combined), nil } // OpenTrie opens the main account trie. It's not supported by historic database. @@ -298,3 +293,10 @@ func (db *HistoricDB) TrieDB() *triedb.Database { func (db *HistoricDB) Snapshot() *snapshot.Tree { return nil } + +// Commit flushes all pending writes and finalizes the state transition, +// committing the changes to the underlying storage. It returns an error +// if the commit fails. +func (db *HistoricDB) Commit(update *stateUpdate) error { + return errors.New("not implemented") +} diff --git a/core/state/iterator.go b/core/state/iterator.go index 0abae091d9..0050a840d8 100644 --- a/core/state/iterator.go +++ b/core/state/iterator.go @@ -144,10 +144,7 @@ func (it *nodeIterator) step() error { } if !bytes.Equal(account.CodeHash, types.EmptyCodeHash.Bytes()) { it.codeHash = common.BytesToHash(account.CodeHash) - it.code, err = it.state.reader.Code(address, common.BytesToHash(account.CodeHash)) - if err != nil { - return fmt.Errorf("code %x: %v", account.CodeHash, err) - } + it.code = it.state.reader.Code(address, common.BytesToHash(account.CodeHash)) if len(it.code) == 0 { return fmt.Errorf("code is not found: %x", account.CodeHash) } diff --git a/core/state/reader.go b/core/state/reader.go index 0835d9a0ef..49375c467c 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -22,12 +22,9 @@ import ( "sync/atomic" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/overlay" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie/bintrie" @@ -44,19 +41,13 @@ type ContractCodeReader interface { // specified address and hash exists or not. Has(addr common.Address, codeHash common.Hash) bool - // Code retrieves a particular contract's code. - // - // - Returns nil code along with nil error if the requested contract code - // doesn't exist - // - Returns an error only if an unexpected issue occurs - Code(addr common.Address, codeHash common.Hash) ([]byte, error) + // Code retrieves a particular contract's code. Returns nil code if the + // requested contract code doesn't exist. + Code(addr common.Address, codeHash common.Hash) []byte - // CodeSize retrieves a particular contracts code's size. - // - // - Returns zero code size along with nil error if the requested contract code - // doesn't exist - // - Returns an error only if an unexpected issue occurs - CodeSize(addr common.Address, codeHash common.Hash) (int, error) + // CodeSize retrieves a particular contracts code's size. Returns zero code + // size if the requested contract code doesn't exist. + CodeSize(addr common.Address, codeHash common.Hash) int } // StateReader defines the interface for accessing accounts and storage slots @@ -90,86 +81,6 @@ type Reader interface { StateReader } -// cachingCodeReader implements ContractCodeReader, accessing contract code either in -// local key-value store or the shared code cache. -// -// cachingCodeReader is safe for concurrent access. -type cachingCodeReader struct { - db ethdb.KeyValueReader - - // These caches could be shared by multiple code reader instances, - // they are natively thread-safe. - codeCache *lru.SizeConstrainedCache[common.Hash, []byte] - codeSizeCache *lru.Cache[common.Hash, int] - - // Cache statistics - hit atomic.Int64 // Number of code lookups found in the cache - miss atomic.Int64 // Number of code lookups not found in the cache - hitBytes atomic.Int64 // Total number of bytes read from cache - missBytes atomic.Int64 // Total number of bytes read from database -} - -// newCachingCodeReader constructs the code reader. -func newCachingCodeReader(db ethdb.KeyValueReader, codeCache *lru.SizeConstrainedCache[common.Hash, []byte], codeSizeCache *lru.Cache[common.Hash, int]) *cachingCodeReader { - return &cachingCodeReader{ - db: db, - codeCache: codeCache, - codeSizeCache: codeSizeCache, - } -} - -// Code implements ContractCodeReader, retrieving a particular contract's code. -// If the contract code doesn't exist, no error will be returned. -func (r *cachingCodeReader) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { - code, _ := r.codeCache.Get(codeHash) - if len(code) > 0 { - r.hit.Add(1) - r.hitBytes.Add(int64(len(code))) - return code, nil - } - r.miss.Add(1) - - code = rawdb.ReadCode(r.db, codeHash) - if len(code) > 0 { - r.codeCache.Add(codeHash, code) - r.codeSizeCache.Add(codeHash, len(code)) - r.missBytes.Add(int64(len(code))) - } - return code, nil -} - -// CodeSize implements ContractCodeReader, retrieving a particular contracts code's size. -// If the contract code doesn't exist, no error will be returned. -func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { - if cached, ok := r.codeSizeCache.Get(codeHash); ok { - r.hit.Add(1) - return cached, nil - } - code, err := r.Code(addr, codeHash) - if err != nil { - return 0, err - } - return len(code), nil -} - -// Has implements ContractCodeReader, returning the flag indicating whether -// the contract code with specified address and hash exists or not. -func (r *cachingCodeReader) Has(addr common.Address, codeHash common.Hash) bool { - code, _ := r.Code(addr, codeHash) - return len(code) > 0 -} - -// GetCodeStats implements ContractCodeReaderStater, returning the statistics -// of the code reader. -func (r *cachingCodeReader) GetCodeStats() ContractCodeReaderStats { - return ContractCodeReaderStats{ - CacheHit: r.hit.Load(), - CacheMiss: r.miss.Load(), - CacheHitBytes: r.hitBytes.Load(), - CacheMissBytes: r.missBytes.Load(), - } -} - // flatReader wraps a database state reader and is safe for concurrent access. type flatReader struct { reader database.StateReader diff --git a/core/state/state_object.go b/core/state/state_object.go index eecb72ce5d..d7c7cb31f1 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -551,10 +551,7 @@ func (s *stateObject) Code() []byte { s.db.CodeLoadBytes += len(s.code) }(time.Now()) - code, err := s.db.reader.Code(s.address, common.BytesToHash(s.CodeHash())) - if err != nil { - s.db.setError(fmt.Errorf("can't load code hash %x: %v", s.CodeHash(), err)) - } + code := s.db.reader.Code(s.address, common.BytesToHash(s.CodeHash())) if len(code) == 0 { s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash())) } @@ -577,10 +574,7 @@ func (s *stateObject) CodeSize() int { s.db.CodeReads += time.Since(start) }(time.Now()) - size, err := s.db.reader.CodeSize(s.address, common.BytesToHash(s.CodeHash())) - if err != nil { - s.db.setError(fmt.Errorf("can't load code size %x: %v", s.CodeHash(), err)) - } + size := s.db.reader.CodeSize(s.address, common.BytesToHash(s.CodeHash())) if size == 0 { s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash())) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 3a2d9c9ac2..2477242eb5 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -28,7 +28,6 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/tracing" @@ -148,8 +147,7 @@ type StateDB struct { StorageReads time.Duration StorageUpdates time.Duration StorageCommits time.Duration - SnapshotCommits time.Duration - TrieDBCommits time.Duration + DatabaseCommits time.Duration CodeReads time.Duration AccountLoaded int // Number of accounts retrieved from the database during the state transition @@ -1333,41 +1331,14 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag return nil, err } } - // Commit dirty contract code if any exists - if db := s.db.TrieDB().Disk(); db != nil && len(ret.codes) > 0 { - batch := db.NewBatch() - for _, code := range ret.codes { - rawdb.WriteCode(batch, code.hash, code.blob) - } - if err := batch.Write(); err != nil { - return nil, err - } - } - if !ret.empty() { - // If snapshotting is enabled, update the snapshot tree with this new version - if snap := s.db.Snapshot(); snap != nil && snap.Snapshot(ret.originRoot) != nil { - start := time.Now() - if err := snap.Update(ret.root, ret.originRoot, ret.accounts, ret.storages); err != nil { - log.Warn("Failed to update snapshot tree", "from", ret.originRoot, "to", ret.root, "err", err) - } - // Keep 128 diff layers in the memory, persistent layer is 129th. - // - head layer is paired with HEAD state - // - head-1 layer is paired with HEAD-1 state - // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state - if err := snap.Cap(ret.root, TriesInMemory); err != nil { - log.Warn("Failed to cap snapshot tree", "root", ret.root, "layers", TriesInMemory, "err", err) - } - s.SnapshotCommits += time.Since(start) - } - // If trie database is enabled, commit the state update as a new layer - if db := s.db.TrieDB(); db != nil { - start := time.Now() - if err := db.Update(ret.root, ret.originRoot, block, ret.nodes, ret.stateSet()); err != nil { - return nil, err - } - s.TrieDBCommits += time.Since(start) - } + start := time.Now() + if err := s.db.Commit(ret); err != nil { + return nil, err } + s.DatabaseCommits = time.Since(start) + + // The reader update must be performed as the final step, otherwise, + // the new state would not be visible before db.commit. s.reader, _ = s.db.Reader(s.originalRoot) return ret, err } diff --git a/core/state/statedb_fuzz_test.go b/core/state/statedb_fuzz_test.go index 8b6ac0ba64..3582185344 100644 --- a/core/state/statedb_fuzz_test.go +++ b/core/state/statedb_fuzz_test.go @@ -209,7 +209,7 @@ func (test *stateTest) run() bool { if i != 0 { root = roots[len(roots)-1] } - state, err := New(root, NewDatabase(tdb, snaps)) + state, err := New(root, NewDatabase(tdb, nil).WithSnapshot(snaps)) if err != nil { panic(err) } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 661d17bb7b..8d1f93ca1b 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -1276,7 +1276,7 @@ func TestDeleteStorage(t *testing.T) { disk = rawdb.NewMemoryDatabase() tdb = triedb.NewDatabase(disk, nil) snaps, _ = snapshot.New(snapshot.Config{CacheSize: 10}, disk, tdb, types.EmptyRootHash) - db = NewDatabase(tdb, snaps) + db = NewDatabase(tdb, nil).WithSnapshot(snaps) state, _ = New(types.EmptyRootHash, db) addr = common.HexToAddress("0x1") ) @@ -1290,7 +1290,7 @@ func TestDeleteStorage(t *testing.T) { } root, _ := state.Commit(0, true, false) // Init phase done, create two states, one with snap and one without - fastState, _ := New(root, NewDatabase(tdb, snaps)) + fastState, _ := New(root, NewDatabase(tdb, nil).WithSnapshot(snaps)) slowState, _ := New(root, NewDatabase(tdb, nil)) obj := fastState.getOrNewStateObject(addr) diff --git a/core/state/stateupdate.go b/core/state/stateupdate.go index 0c1b76b4f8..1c171cbd5e 100644 --- a/core/state/stateupdate.go +++ b/core/state/stateupdate.go @@ -211,9 +211,9 @@ func (sc *stateUpdate) deriveCodeFields(reader ContractCodeReader) error { cache := make(map[common.Hash]bool) for addr, code := range sc.codes { if code.originHash != types.EmptyCodeHash { - blob, err := reader.Code(addr, code.originHash) - if err != nil { - return err + blob := reader.Code(addr, code.originHash) + if len(blob) == 0 { + return fmt.Errorf("original code of %x is empty", addr) } code.originBlob = blob } diff --git a/core/state/sync_test.go b/core/state/sync_test.go index cae0e0a936..e5e22deae5 100644 --- a/core/state/sync_test.go +++ b/core/state/sync_test.go @@ -222,8 +222,8 @@ func testIterativeStateSync(t *testing.T, count int, commit bool, bypath bool, s codeResults = make([]trie.CodeSyncResult, len(codeElements)) ) for i, element := range codeElements { - data, err := cReader.Code(common.Address{}, element.code) - if err != nil || len(data) == 0 { + data := cReader.Code(common.Address{}, element.code) + if len(data) == 0 { t.Fatalf("failed to retrieve contract bytecode for hash %x", element.code) } codeResults[i] = trie.CodeSyncResult{Hash: element.code, Data: data} @@ -346,8 +346,8 @@ func testIterativeDelayedStateSync(t *testing.T, scheme string) { if len(codeElements) > 0 { codeResults := make([]trie.CodeSyncResult, len(codeElements)/2+1) for i, element := range codeElements[:len(codeResults)] { - data, err := cReader.Code(common.Address{}, element.code) - if err != nil || len(data) == 0 { + data := cReader.Code(common.Address{}, element.code) + if len(data) == 0 { t.Fatalf("failed to retrieve contract bytecode for %x", element.code) } codeResults[i] = trie.CodeSyncResult{Hash: element.code, Data: data} @@ -452,8 +452,8 @@ func testIterativeRandomStateSync(t *testing.T, count int, scheme string) { if len(codeQueue) > 0 { results := make([]trie.CodeSyncResult, 0, len(codeQueue)) for hash := range codeQueue { - data, err := cReader.Code(common.Address{}, hash) - if err != nil || len(data) == 0 { + data := cReader.Code(common.Address{}, hash) + if len(data) == 0 { t.Fatalf("failed to retrieve node data for %x", hash) } results = append(results, trie.CodeSyncResult{Hash: hash, Data: data}) @@ -551,8 +551,8 @@ func testIterativeRandomDelayedStateSync(t *testing.T, scheme string) { for hash := range codeQueue { delete(codeQueue, hash) - data, err := cReader.Code(common.Address{}, hash) - if err != nil || len(data) == 0 { + data := cReader.Code(common.Address{}, hash) + if len(data) == 0 { t.Fatalf("failed to retrieve node data for %x", hash) } results = append(results, trie.CodeSyncResult{Hash: hash, Data: data}) @@ -671,8 +671,8 @@ func testIncompleteStateSync(t *testing.T, scheme string) { if len(codeQueue) > 0 { results := make([]trie.CodeSyncResult, 0, len(codeQueue)) for hash := range codeQueue { - data, err := cReader.Code(common.Address{}, hash) - if err != nil || len(data) == 0 { + data := cReader.Code(common.Address{}, hash) + if len(data) == 0 { t.Fatalf("failed to retrieve node data for %x", hash) } results = append(results, trie.CodeSyncResult{Hash: hash, Data: data}) diff --git a/miner/miner_test.go b/miner/miner_test.go index 575ee4d0fd..13475a19b6 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -155,7 +155,7 @@ func createMiner(t *testing.T) *Miner { if err != nil { t.Fatalf("can't create new chain %v", err) } - statedb, _ := state.New(bc.Genesis().Root(), bc.StateCache()) + statedb, _ := state.New(bc.Genesis().Root(), state.NewDatabase(bc.TrieDB(), bc.CodeDB())) blockchain := &testBlockChain{bc.Genesis().Root(), chainConfig, statedb, 10000000, new(event.Feed)} pool := legacypool.New(testTxPoolConfig, blockchain) diff --git a/tests/state_test_util.go b/tests/state_test_util.go index 7525081f84..787e297016 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -546,7 +546,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo } snaps, _ = snapshot.New(snapconfig, db, triedb, root) } - sdb = state.NewDatabase(triedb, snaps) + sdb = state.NewDatabase(triedb, nil).WithSnapshot(snaps) statedb, _ = state.New(root, sdb) return StateTestState{statedb, triedb, snaps} } diff --git a/triedb/hashdb/database.go b/triedb/hashdb/database.go index 38392aa519..90d0514290 100644 --- a/triedb/hashdb/database.go +++ b/triedb/hashdb/database.go @@ -612,6 +612,9 @@ func (db *Database) Close() error { // NodeReader returns a reader for accessing trie nodes within the specified state. // An error will be returned if the specified state is not available. func (db *Database) NodeReader(root common.Hash) (database.NodeReader, error) { + if root == types.EmptyRootHash { + return &reader{db: db}, nil + } if _, err := db.node(root); err != nil { return nil, fmt.Errorf("state %#x is not available, %v", root, err) }