diff --git a/core/blockchain.go b/core/blockchain.go
index d41f301243..de7b667ee8 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -93,9 +93,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)
@@ -322,7 +320,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
@@ -404,6 +402,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),
@@ -420,7 +419,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)
@@ -597,9 +595,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)
}
}
@@ -2081,11 +2076,12 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
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
}
@@ -2095,23 +2091,27 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
//
// 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
}
// 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) {
@@ -2265,9 +2265,8 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
// 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 adc66266c4..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,22 +86,21 @@ 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
// 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 +175,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 {
@@ -216,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{
@@ -238,19 +228,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/blockchain_test.go b/core/blockchain_test.go
index 13ce690518..ce592f0267 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -157,7 +157,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 4a5547d075..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,21 +217,20 @@ 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
// 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(db.codedb.Reader(), newStateReaderWithStats(sr))
+ rb := newReader(db.codedb.Reader(), newStateReaderWithStats(sr))
return ra, rb, nil
}
@@ -268,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
@@ -294,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 35b732173b..49375c467c 100644
--- a/core/state/reader.go
+++ b/core/state/reader.go
@@ -18,17 +18,13 @@ package state
import (
"errors"
- "fmt"
"sync"
"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"
@@ -38,55 +34,26 @@ 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.
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)
-}
-
-// 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
+ // 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
// 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,119 +81,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.
-//
-// 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 returns 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 {
- 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
@@ -495,20 +349,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 +459,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 +470,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 +481,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 +499,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 +512,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
+}
diff --git a/core/state/state_object.go b/core/state/state_object.go
index f7109bddee..dd30bb64a5 100644
--- a/core/state/state_object.go
+++ b/core/state/state_object.go
@@ -564,10 +564,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()))
}
@@ -590,10 +587,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)
}