diff --git a/core/blockchain.go b/core/blockchain.go index 3bb681d7c33a..7a85abe82b16 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -288,7 +288,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) + stateDB *stateDatabase // State database to reuse between imports (contains state cache) txIndexer *txIndexer // Transaction indexer, might be nil if not enabled hc *HeaderChain @@ -338,19 +338,18 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, if cfg == nil { cfg = DefaultConfig() } - - // Open trie database with provided config + // Open state database with provided config enableVerkle, err := EnableVerkleAtGenesis(db, genesis) if err != nil { return nil, err } - triedb := triedb.NewDatabase(db, cfg.triedbConfig(enableVerkle)) + stateDB := newStateDatabase(db, cfg) // Write the supplied genesis to the database if it has not been initialized // yet. The corresponding chain config will be returned, either from the // provided genesis or from the locally stored configuration if the genesis // has already been initialized. - chainConfig, genesisHash, compatErr, err := SetupGenesisBlockWithOverride(db, triedb, genesis, cfg.Overrides) + chainConfig, genesisHash, compatErr, err := SetupGenesisBlockWithOverride(db, stateDB.triedb(enableVerkle), genesis, cfg.Overrides) if err != nil { return nil, err } @@ -366,7 +365,8 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, chainConfig: chainConfig, cfg: cfg, db: db, - triedb: triedb, + stateDB: stateDB, + triedb: stateDB.triedb(false), // TODO(rjl493456442) support verkle triegc: prque.New[int64, common.Hash](nil), chainmu: syncx.NewClosableMutex(), bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit), @@ -382,7 +382,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(chainConfig, bc.hc) @@ -548,9 +547,7 @@ 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) + bc.stateDB.setSnapshot(bc.snaps) } } @@ -941,7 +938,7 @@ func (bc *BlockChain) rewindPathHead(head *types.Header, root common.Hash) (*typ // then block number zero is returned, indicating that snapshot recovery is disabled // and the whole snapshot should be auto-generated in case of head mismatch. func (bc *BlockChain) rewindHead(head *types.Header, root common.Hash) (*types.Header, uint64) { - if bc.triedb.Scheme() == rawdb.PathScheme { + if bc.cfg.StateScheme == rawdb.PathScheme { return bc.rewindPathHead(head, root) } return bc.rewindHashHead(head, root) @@ -1097,7 +1094,7 @@ func (bc *BlockChain) SnapSyncCommitHead(hash common.Hash) error { } // Reset the trie database with the fresh snap synced state. root := block.Root() - if bc.triedb.Scheme() == rawdb.PathScheme { + if bc.cfg.StateScheme == rawdb.PathScheme { if err := bc.triedb.Enable(root); err != nil { return err } @@ -1269,7 +1266,7 @@ func (bc *BlockChain) Stop() { } bc.snaps.Release() } - if bc.triedb.Scheme() == rawdb.PathScheme { + if bc.cfg.StateScheme == rawdb.PathScheme { // Ensure that the in-memory trie nodes are journaled to disk properly. if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil { log.Info("Failed to journal in-memory trie nodes", "err", err) @@ -1312,8 +1309,8 @@ func (bc *BlockChain) Stop() { bc.logger.OnClose() } // Close the trie database, release all the held resources as the last step. - if err := bc.triedb.Close(); err != nil { - log.Error("Failed to close trie database", "err", err) + if err := bc.stateDB.close(); err != nil { + log.Error("Failed to close state database", "err", err) } log.Info("Blockchain stopped") } @@ -1586,7 +1583,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. } // If node is running in path mode, skip explicit gc operation // which is unnecessary in this mode. - if bc.triedb.Scheme() == rawdb.PathScheme { + if bc.cfg.StateScheme == rawdb.PathScheme { return nil } // If we're running an archive node, always flush @@ -1956,7 +1953,8 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s defer interrupt.Store(true) // terminate the prefetch at the end if bc.cfg.NoPrefetch { - statedb, err = state.New(parentRoot, bc.statedb) + sdb := bc.stateDB.stateDB(bc.chainConfig.IsVerkle(block.Number(), block.Time())) + statedb, err = state.New(parentRoot, sdb) if err != nil { return nil, err } @@ -1966,15 +1964,16 @@ 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) + sdb := bc.stateDB.stateDB(bc.chainConfig.IsVerkle(block.Number(), block.Time())) + 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 } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 6a0cfd6704a9..1237ff2c50c2 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -325,8 +325,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) - return err == nil + return bc.stateDB.hasState(hash) } // HasBlockAndState checks if a block and associated state trie is fully present @@ -345,7 +344,7 @@ func (bc *BlockChain) HasBlockAndState(hash common.Hash, number uint64) bool { // state is not treated as recoverable if it's available, thus // false will be returned in this case. func (bc *BlockChain) stateRecoverable(root common.Hash) bool { - if bc.triedb.Scheme() == rawdb.HashScheme { + if bc.cfg.StateScheme == rawdb.HashScheme { return false } result, _ := bc.triedb.Recoverable(root) @@ -357,7 +356,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.stateDB.contractCode(common.Address{}, hash) } // State returns a new mutable state based on the current HEAD block. @@ -367,7 +366,8 @@ 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) + // TODO(rjl493456442) support verkle state accessing + return state.New(root, bc.stateDB.stateDB(false)) } // Config retrieves the chain's fork configuration. @@ -393,7 +393,8 @@ func (bc *BlockChain) Processor() Processor { // StateCache returns the caching database underpinning the blockchain instance. func (bc *BlockChain) StateCache() state.Database { - return bc.statedb + // TODO(rjl493456442) support verkle database + return bc.stateDB.stateDB(false) } // GasLimit returns the gas limit of the current HEAD block. diff --git a/core/blockchain_repair_test.go b/core/blockchain_repair_test.go index 55794c659605..b701a5f68ff0 100644 --- a/core/blockchain_repair_test.go +++ b/core/blockchain_repair_test.go @@ -1845,7 +1845,7 @@ func testRepairWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme s rawdb.WriteLastPivotNumber(db, *tt.pivotBlock) } // Pull the plug on the database, simulating a hard crash - chain.triedb.Close() + chain.stateDB.close() db.Close() chain.stopWithoutSaving() @@ -1971,7 +1971,7 @@ func testIssue23496(t *testing.T, scheme string) { } // Pull the plug on the database, simulating a hard crash - chain.triedb.Close() + chain.stateDB.close() db.Close() chain.stopWithoutSaving() diff --git a/core/blockchain_sethead_test.go b/core/blockchain_sethead_test.go index 72ca15d7f614..47696cbc5c45 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" @@ -2033,15 +2032,14 @@ func testSetHeadWithScheme(t *testing.T, tt *rewindTest, snapshots bool, scheme t.Fatalf("Failed to import canonical chain tail: %v", err) } // Reopen the trie database without persisting in-memory dirty nodes. - chain.triedb.Close() + chain.stateDB.close() dbconfig := &triedb.Config{} if scheme == rawdb.PathScheme { dbconfig.PathDB = pathdb.Defaults } else { dbconfig.HashDB = hashdb.Defaults } - chain.triedb = triedb.NewDatabase(chain.db, dbconfig) - chain.statedb = state.NewDatabase(chain.triedb, chain.snaps) + chain.stateDB = newStateDatabase(chain.db, chain.cfg) // Force run a freeze cycle type freezer interface { diff --git a/core/blockchain_snapshot_test.go b/core/blockchain_snapshot_test.go index ae9398b97d90..ca3675240be2 100644 --- a/core/blockchain_snapshot_test.go +++ b/core/blockchain_snapshot_test.go @@ -257,7 +257,7 @@ func (snaptest *crashSnapshotTest) test(t *testing.T) { db := chain.db db.Close() chain.stopWithoutSaving() - chain.triedb.Close() + chain.stateDB.close() // Start a new blockchain back up and see where the repair leads us pdb, err := pebble.New(snaptest.datadir, 0, 0, "", false) @@ -416,7 +416,7 @@ func (snaptest *wipeCrashSnapshotTest) test(t *testing.T) { } // Simulate the blockchain crash. - tmp.triedb.Close() + tmp.stateDB.close() tmp.stopWithoutSaving() newchain, err = NewBlockChain(snaptest.db, snaptest.gspec, snaptest.engine, DefaultConfig().WithStateScheme(snaptest.scheme)) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index f47e922e1870..457edfca4a14 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -153,7 +153,8 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { } return err } - statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), blockchain.statedb) + sdb := blockchain.stateDB.stateDB(false) + statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), sdb) if err != nil { return err } diff --git a/core/state/database.go b/core/state/database.go index 5fb198a6298d..d7af3760d0a9 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -33,14 +33,14 @@ import ( ) const ( - // Number of codehash->size associations to keep. - codeSizeCacheSize = 1_000_000 // 4 megabytes in total + // CodeSizeCacheSize is the 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 + // CodeCacheSize is the cache size granted for caching clean code. + CodeCacheSize = 256 * 1024 * 1024 - // Number of address->curve point associations to keep. - pointCacheSize = 4096 + // PointCacheSize is the number of address->curve point associations to keep. + PointCacheSize = 4096 ) // Database wraps access to tries and contract code. @@ -159,9 +159,22 @@ func NewDatabase(triedb *triedb.Database, snap *snapshot.Tree) *CachingDB { disk: triedb.Disk(), triedb: triedb, snap: snap, - codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), - codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), - pointCache: utils.NewPointCache(pointCacheSize), + codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](CodeCacheSize), + codeSizeCache: lru.NewCache[common.Hash, int](CodeSizeCacheSize), + pointCache: utils.NewPointCache(PointCacheSize), + } +} + +// NewDatabaseWithCache creates a state database with the provided database +// and relative caches. +func NewDatabaseWithCache(disk ethdb.KeyValueStore, triedb *triedb.Database, snap *snapshot.Tree, codeCache *lru.SizeConstrainedCache[common.Hash, []byte], codeSizeCache *lru.Cache[common.Hash, int], pointCache *utils.PointCache) *CachingDB { + return &CachingDB{ + disk: disk, + snap: snap, + triedb: triedb, + codeCache: codeCache, + codeSizeCache: codeSizeCache, + pointCache: pointCache, } } @@ -248,22 +261,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 diff --git a/core/state_database.go b/core/state_database.go new file mode 100644 index 000000000000..d966ec5a51d5 --- /dev/null +++ b/core/state_database.go @@ -0,0 +1,117 @@ +// Copyright 2025 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 core + +import ( + "errors" + + "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/core/state" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/trie/utils" + "github.com/ethereum/go-ethereum/triedb" +) + +// stateDatabase is the central structure for managing state data, +// including both the Merkle-Patricia Trie and the Verkle tree (post-transition). +type stateDatabase struct { + disk ethdb.Database + merkle *triedb.Database + verkle *triedb.Database + snap *snapshot.Tree + + // Various caches + codeCache *lru.SizeConstrainedCache[common.Hash, []byte] + codeSizeCache *lru.Cache[common.Hash, int] + pointCache *utils.PointCache +} + +// newStateDatabase initializes a new stateDatabase instance, creating +// both Merkle and Verkle trie databases. +func newStateDatabase(disk ethdb.Database, cfg *BlockChainConfig) *stateDatabase { + return &stateDatabase{ + disk: disk, + merkle: triedb.NewDatabase(disk, cfg.triedbConfig(false)), + verkle: triedb.NewDatabase(disk, cfg.triedbConfig(true)), + codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](state.CodeCacheSize), + codeSizeCache: lru.NewCache[common.Hash, int](state.CodeSizeCacheSize), + pointCache: utils.NewPointCache(state.PointCacheSize), + } +} + +// setSnapshot assigns a snapshot tree to the state database. +func (s *stateDatabase) setSnapshot(snap *snapshot.Tree) { + s.snap = snap +} + +// triedb returns the appropriate trie database based on the given flag. +func (s *stateDatabase) triedb(isVerkle bool) *triedb.Database { + if isVerkle { + return s.verkle + } + return s.merkle +} + +// stateDB returns the appropriate state database based on the given flag. +func (s *stateDatabase) stateDB(isVerkle bool) *state.CachingDB { + if isVerkle { + // Verkle is compatible only with path mode; snapshot is integrated natively. + return state.NewDatabaseWithCache(s.disk, s.verkle, nil, s.codeCache, s.codeSizeCache, s.pointCache) + } + return state.NewDatabaseWithCache(s.disk, s.merkle, s.snap, s.codeCache, s.codeSizeCache, s.pointCache) +} + +// contractCode retrieves the contract code by its hash. If not present in the +// cache, it falls back to reading from disk using the contract database prefix. +func (s *stateDatabase) contractCode(address common.Address, codeHash common.Hash) []byte { + code, _ := s.codeCache.Get(codeHash) + if len(code) > 0 { + return code + } + code = rawdb.ReadCodeWithPrefix(s.disk, codeHash) + if len(code) > 0 { + s.codeCache.Add(codeHash, code) + s.codeSizeCache.Add(codeHash, len(code)) + } + return code +} + +// hasState checks whether a trie state exists for the given root hash +// in either the Merkle or Verkle database. +func (s *stateDatabase) hasState(root common.Hash) bool { + _, err := s.merkle.NodeReader(root) + if err == nil { + return true + } + _, err = s.verkle.NodeReader(root) + return err == nil +} + +// close terminates the state database and closes two internal trie databases. +func (s *stateDatabase) close() error { + var errs []error + if err := s.merkle.Close(); err != nil { + errs = append(errs, err) + } + if err := s.verkle.Close(); err != nil { + errs = append(errs, err) + } + return errors.Join(errs...) +} diff --git a/triedb/hashdb/database.go b/triedb/hashdb/database.go index 38392aa5199b..90d051429049 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) }