From cafa5e6c1263d63b6b13040c0fa100acd97364d7 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 25 Jun 2025 09:42:11 +0800 Subject: [PATCH 1/4] core, consensus/beacon: defer trie resolution (#31725) Previously, the account trie for a given state root was resolved immediately when the stateDB was created, implying that the trie was always required by the stateDB. However, this assumption no longer holds, especially for path archive nodes, where historical states can be accessed even if the corresponding trie data does not exist. --- consensus/beacon/consensus.go | 6 +++++- core/chain_makers.go | 2 +- core/state/dump.go | 23 ++++++++++++++++------- core/state/iterator.go | 20 ++++++++++++++------ core/state/state_object.go | 1 + core/state/state_test.go | 4 ---- core/state/statedb.go | 25 ++++++++++++++++++------- core/state/statedb_test.go | 11 +---------- core/state_processor.go | 3 +-- 9 files changed, 57 insertions(+), 38 deletions(-) diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index f9a5a3233b..196cbc857c 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -391,8 +391,12 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea if err != nil { return nil, fmt.Errorf("error opening pre-state tree root: %w", err) } + postTrie := state.GetTrie() + if postTrie == nil { + return nil, errors.New("post-state tree is not available") + } vktPreTrie, okpre := preTrie.(*trie.VerkleTrie) - vktPostTrie, okpost := state.GetTrie().(*trie.VerkleTrie) + vktPostTrie, okpost := postTrie.(*trie.VerkleTrie) // The witness is only attached iff both parent and current block are // using verkle tree. diff --git a/core/chain_makers.go b/core/chain_makers.go index fe2e25d2d7..b2559495a1 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -124,7 +124,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti } // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. - if b.statedb.GetTrie().IsVerkle() { + if b.statedb.Database().TrieDB().IsVerkle() { b.statedb.AccessEvents().Merge(evm.AccessEvents) } b.txs = append(b.txs, tx) diff --git a/core/state/dump.go b/core/state/dump.go index 3af9133f5b..a4abc33733 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -112,6 +112,9 @@ func (d iterativeDump) OnRoot(root common.Hash) { // DumpToCollector iterates the state according to the given options and inserts // the items into a collector for aggregation or serialization. +// +// The state iterator is still trie-based and can be converted to snapshot-based +// once the state snapshot is fully integrated into database. TODO(rjl493456442). func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) { // Sanitize the input to allow nil configs if conf == nil { @@ -123,15 +126,20 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] start = time.Now() logged = time.Now() ) - log.Info("Trie dumping started", "root", s.trie.Hash()) - c.OnRoot(s.trie.Hash()) + log.Info("Trie dumping started", "root", s.originalRoot) + c.OnRoot(s.originalRoot) - trieIt, err := s.trie.NodeIterator(conf.Start) + tr, err := s.db.OpenTrie(s.originalRoot) + if err != nil { + return nil + } + trieIt, err := tr.NodeIterator(conf.Start) if err != nil { log.Error("Trie dumping error", "err", err) return nil } it := trie.NewIterator(trieIt) + for it.Next() { var data types.StateAccount if err := rlp.DecodeBytes(it.Value, &data); err != nil { @@ -147,7 +155,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] } address *common.Address addr common.Address - addrBytes = s.trie.GetKey(it.Key) + addrBytes = tr.GetKey(it.Key) ) if addrBytes == nil { missingPreimages++ @@ -165,12 +173,13 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] } if !conf.SkipStorage { account.Storage = make(map[common.Hash]string) - tr, err := obj.getTrie() + + storageTr, err := s.db.OpenStorageTrie(s.originalRoot, addr, obj.Root(), tr) if err != nil { log.Error("Failed to load storage trie", "err", err) continue } - trieIt, err := tr.NodeIterator(nil) + trieIt, err := storageTr.NodeIterator(nil) if err != nil { log.Error("Failed to create trie iterator", "err", err) continue @@ -182,7 +191,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] log.Error("Failed to decode the value returned by iterator", "error", err) continue } - key := s.trie.GetKey(storageIt.Key) + key := storageTr.GetKey(storageIt.Key) if key == nil { continue } diff --git a/core/state/iterator.go b/core/state/iterator.go index 5ea52c6183..0abae091d9 100644 --- a/core/state/iterator.go +++ b/core/state/iterator.go @@ -32,6 +32,7 @@ import ( // required in order to resolve the contract address. type nodeIterator struct { state *StateDB // State being iterated + tr Trie // Primary account trie for traversal stateIt trie.NodeIterator // Primary iterator for the global state trie dataIt trie.NodeIterator // Secondary iterator for the data trie of a contract @@ -75,13 +76,20 @@ func (it *nodeIterator) step() error { if it.state == nil { return nil } - // Initialize the iterator if we've just started - var err error - if it.stateIt == nil { - it.stateIt, err = it.state.trie.NodeIterator(nil) + if it.tr == nil { + tr, err := it.state.db.OpenTrie(it.state.originalRoot) if err != nil { return err } + it.tr = tr + } + // Initialize the iterator if we've just started + if it.stateIt == nil { + stateIt, err := it.tr.NodeIterator(nil) + if err != nil { + return err + } + it.stateIt = stateIt } // If we had data nodes previously, we surely have at least state nodes if it.dataIt != nil { @@ -116,14 +124,14 @@ func (it *nodeIterator) step() error { return err } // Lookup the preimage of account hash - preimage := it.state.trie.GetKey(it.stateIt.LeafKey()) + preimage := it.tr.GetKey(it.stateIt.LeafKey()) if preimage == nil { return errors.New("account address is not available") } address := common.BytesToAddress(preimage) // Traverse the storage slots belong to the account - dataTrie, err := it.state.db.OpenStorageTrie(it.state.originalRoot, address, account.Root, it.state.trie) + dataTrie, err := it.state.db.OpenStorageTrie(it.state.originalRoot, address, account.Root, it.tr) if err != nil { return err } diff --git a/core/state/state_object.go b/core/state/state_object.go index 73a3b5146c..767f469bfd 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -124,6 +124,7 @@ func (s *stateObject) touch() { // subsequent reads to expand the same trie instead of reloading from disk. func (s *stateObject) getTrie() (Trie, error) { if s.trie == nil { + // Assumes the primary account trie is already loaded tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie) if err != nil { return nil, err diff --git a/core/state/state_test.go b/core/state/state_test.go index b443411f1b..eeeb7fa2df 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -54,8 +54,6 @@ func TestDump(t *testing.T) { obj3.SetBalance(uint256.NewInt(44)) // write some of them to the trie - s.state.updateStateObject(obj1) - s.state.updateStateObject(obj2) root, _ := s.state.Commit(0, false, false) // check that DumpToCollector contains the state objects that are in trie @@ -114,8 +112,6 @@ func TestIterativeDump(t *testing.T) { obj4.AddBalance(uint256.NewInt(1337)) // write some of them to the trie - s.state.updateStateObject(obj1) - s.state.updateStateObject(obj2) root, _ := s.state.Commit(0, false, false) s.state, _ = New(root, tdb) diff --git a/core/state/statedb.go b/core/state/statedb.go index 6b474ea0a4..0d76e8c61c 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -79,8 +79,8 @@ func (m *mutation) isDelete() bool { type StateDB struct { db Database prefetcher *triePrefetcher - trie Trie reader Reader + trie Trie // it's resolved on first access // originalRoot is the pre-state root, before any changes were made. // It will be updated when the Commit is called. @@ -169,13 +169,8 @@ func New(root common.Hash, db Database) (*StateDB, error) { // NewWithReader creates a new state for the specified state root. Unlike New, // this function accepts an additional Reader which is bound to the given root. func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) { - tr, err := db.OpenTrie(root) - if err != nil { - return nil, err - } sdb := &StateDB{ db: db, - trie: tr, originalRoot: root, reader: reader, stateObjects: make(map[common.Address]*stateObject), @@ -664,7 +659,6 @@ func (s *StateDB) Copy() *StateDB { // Copy all the basic fields, initialize the memory ones state := &StateDB{ db: s.db, - trie: mustCopyTrie(s.trie), reader: s.reader, originalRoot: s.originalRoot, stateObjects: make(map[common.Address]*stateObject, len(s.stateObjects)), @@ -688,6 +682,9 @@ func (s *StateDB) Copy() *StateDB { transientStorage: s.transientStorage.Copy(), journal: s.journal.copy(), } + if s.trie != nil { + state.trie = mustCopyTrie(s.trie) + } if s.witness != nil { state.witness = s.witness.Copy() } @@ -783,6 +780,20 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // Finalise all the dirty storage states and write them into the tries s.Finalise(deleteEmptyObjects) + // Initialize the trie if it's not constructed yet. If the prefetch + // is enabled, the trie constructed below will be replaced by the + // prefetched one. + // + // This operation must be done before state object storage hashing, + // as it assumes the main trie is already loaded. + if s.trie == nil { + tr, err := s.db.OpenTrie(s.originalRoot) + if err != nil { + s.setError(err) + return common.Hash{} + } + s.trie = tr + } // If there was a trie prefetcher operating, terminate it async so that the // individual storage tries can be updated as soon as the disk load finishes. if s.prefetcher != nil { diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 061ac59752..67e27cc832 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -171,7 +171,6 @@ func TestCopy(t *testing.T) { for i := byte(0); i < 255; i++ { obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) obj.AddBalance(uint256.NewInt(uint64(i))) - orig.updateStateObject(obj) } orig.Finalise(false) @@ -190,10 +189,6 @@ func TestCopy(t *testing.T) { origObj.AddBalance(uint256.NewInt(2 * uint64(i))) copyObj.AddBalance(uint256.NewInt(3 * uint64(i))) ccopyObj.AddBalance(uint256.NewInt(4 * uint64(i))) - - orig.updateStateObject(origObj) - copy.updateStateObject(copyObj) - ccopy.updateStateObject(copyObj) } // Finalise the changes on all concurrently @@ -238,7 +233,6 @@ func TestCopyWithDirtyJournal(t *testing.T) { obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) obj.AddBalance(uint256.NewInt(uint64(i))) obj.data.Root = common.HexToHash("0xdeadbeef") - orig.updateStateObject(obj) } root, _ := orig.Commit(0, true, false) orig, _ = New(root, db) @@ -248,8 +242,6 @@ func TestCopyWithDirtyJournal(t *testing.T) { obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) amount := uint256.NewInt(uint64(i)) obj.SetBalance(new(uint256.Int).Sub(obj.Balance(), amount)) - - orig.updateStateObject(obj) } cpy := orig.Copy() @@ -284,7 +276,6 @@ func TestCopyObjectState(t *testing.T) { obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) obj.AddBalance(uint256.NewInt(uint64(i))) obj.data.Root = common.HexToHash("0xdeadbeef") - orig.updateStateObject(obj) } orig.Finalise(true) cpy := orig.Copy() @@ -573,7 +564,7 @@ func forEachStorage(s *StateDB, addr common.Address, cb func(key, value common.H ) for it.Next() { - key := common.BytesToHash(s.trie.GetKey(it.Key)) + key := common.BytesToHash(tr.GetKey(it.Key)) visited[key] = true if value, dirty := so.dirtyStorage[key]; dirty { if !cb(key, value) { diff --git a/core/state_processor.go b/core/state_processor.go index b778befd77..ee98326467 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -161,10 +161,9 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB, // Merge the tx-local access event into the "block-local" one, in order to collect // all values, so that the witness can be built. - if statedb.GetTrie().IsVerkle() { + if statedb.Database().TrieDB().IsVerkle() { statedb.AccessEvents().Merge(evm.AccessEvents) } - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, *usedGas, root), nil } From 429e821fa2cbd094ee81e18bc43228693e4384c2 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Wed, 25 Jun 2025 04:42:45 +0200 Subject: [PATCH 2/4] cmd/utils: fix formatting for beacon flag errors to fit Fatalf form (#32090) Noticed that the errors for the blsync flags were not formatted correctly for `Fatalf(..)`. --- cmd/utils/flags.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index ba98ec4169..3747e2c70b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1891,11 +1891,11 @@ func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig { if c, err := hexutil.Decode(ctx.String(BeaconGenesisRootFlag.Name)); err == nil && len(c) <= 32 { copy(config.GenesisValidatorsRoot[:len(c)], c) } else { - Fatalf("Invalid hex string", "beacon.genesis.gvroot", ctx.String(BeaconGenesisRootFlag.Name), "error", err) + Fatalf("Could not parse --%s: %v", BeaconGenesisRootFlag.Name, err) } configFile := ctx.String(BeaconConfigFlag.Name) if err := config.ChainConfig.LoadForks(configFile); err != nil { - Fatalf("Could not load beacon chain config", "file", configFile, "error", err) + Fatalf("Could not load beacon chain config '%s': %v", configFile, err) } log.Info("Using custom beacon chain config", "file", configFile) } else { @@ -1912,17 +1912,17 @@ func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig { // are saved to the specified file. if ctx.IsSet(BeaconCheckpointFileFlag.Name) { if _, err := config.SetCheckpointFile(ctx.String(BeaconCheckpointFileFlag.Name)); err != nil { - Fatalf("Could not load beacon checkpoint file", "beacon.checkpoint.file", ctx.String(BeaconCheckpointFileFlag.Name), "error", err) + Fatalf("Could not load beacon checkpoint file '%s': %v", ctx.String(BeaconCheckpointFileFlag.Name), err) } } if ctx.IsSet(BeaconCheckpointFlag.Name) { hex := ctx.String(BeaconCheckpointFlag.Name) c, err := hexutil.Decode(hex) if err != nil { - Fatalf("Invalid hex string", "beacon.checkpoint", hex, "error", err) + Fatalf("Could not parse --%s: %v", BeaconCheckpointFlag.Name, err) } if len(c) != 32 { - Fatalf("Invalid hex string length", "beacon.checkpoint", hex, "length", len(c)) + Fatalf("Could not parse --%s: invalid length %d, want 32", BeaconCheckpointFlag.Name, len(c)) } copy(config.Checkpoint[:len(c)], c) } From ce63bba361f9b76c326dfeb078872839a8aef06c Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 25 Jun 2025 16:49:09 +0800 Subject: [PATCH 3/4] eth, triedb/pathdb: permit write buffer allowance in PBSS archive mode (#32091) This pull request fixes a flaw in PBSS archive mode that significantly degrades performance when the mode is enabled. Originally, in hash mode, the dirty trie cache is completely disabled when archive mode is active, in order to disable the in-memory garbage collection mechanism. However, the internal logic in path mode differs significantly, and the dirty trie node cache is essential for maintaining chain insertion performance. Therefore, the cache is now retained in path mode. --- eth/backend.go | 2 +- triedb/pathdb/history_indexer.go | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/eth/backend.go b/eth/backend.go index 5561657ea8..4b08707d2f 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -140,7 +140,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { log.Warn("Sanitizing invalid miner gas price", "provided", config.Miner.GasPrice, "updated", ethconfig.Defaults.Miner.GasPrice) config.Miner.GasPrice = new(big.Int).Set(ethconfig.Defaults.Miner.GasPrice) } - if config.NoPruning && config.TrieDirtyCache > 0 { + if config.NoPruning && config.TrieDirtyCache > 0 && config.StateScheme == rawdb.HashScheme { if config.SnapshotCache > 0 { config.TrieCleanCache += config.TrieDirtyCache * 3 / 5 config.SnapshotCache += config.TrieDirtyCache * 2 / 5 diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index b09804ce9d..75bba5399a 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -128,9 +128,11 @@ func (b *batchIndexer) finish(force bool) error { return nil } var ( - batch = b.db.NewBatch() - batchMu sync.RWMutex - eg errgroup.Group + batch = b.db.NewBatch() + batchMu sync.RWMutex + storages int + start = time.Now() + eg errgroup.Group ) eg.SetLimit(runtime.NumCPU()) @@ -167,6 +169,7 @@ func (b *batchIndexer) finish(force bool) error { }) } for addrHash, slots := range b.storages { + storages += len(slots) for storageHash, idList := range slots { eg.Go(func() error { if !b.delete { @@ -216,6 +219,7 @@ func (b *batchIndexer) finish(force bool) error { if err := batch.Write(); err != nil { return err } + log.Debug("Committed batch indexer", "accounts", len(b.accounts), "storages", storages, "records", b.counter, "elapsed", common.PrettyDuration(time.Since(start))) b.counter = 0 b.accounts = make(map[common.Hash][]uint64) b.storages = make(map[common.Hash]map[common.Hash][]uint64) @@ -224,9 +228,10 @@ func (b *batchIndexer) finish(force bool) error { // indexSingle processes the state history with the specified ID for indexing. func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader) error { - defer func(start time.Time) { + start := time.Now() + defer func() { indexHistoryTimer.UpdateSince(start) - }(time.Now()) + }() metadata := loadIndexMetadata(db) if metadata == nil || metadata.Last+1 != historyID { @@ -247,15 +252,16 @@ func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancient if err := b.finish(true); err != nil { return err } - log.Debug("Indexed state history", "id", historyID) + log.Debug("Indexed state history", "id", historyID, "elapsed", common.PrettyDuration(time.Since(start))) return nil } // unindexSingle processes the state history with the specified ID for unindexing. func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader) error { - defer func(start time.Time) { + start := time.Now() + defer func() { unindexHistoryTimer.UpdateSince(start) - }(time.Now()) + }() metadata := loadIndexMetadata(db) if metadata == nil || metadata.Last != historyID { @@ -276,7 +282,7 @@ func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancie if err := b.finish(true); err != nil { return err } - log.Debug("Unindexed state history", "id", historyID) + log.Debug("Unindexed state history", "id", historyID, "elapsed", common.PrettyDuration(time.Since(start))) return nil } From a92f2b86e33e49ec23e67fa0e17eaabf99cc9645 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 25 Jun 2025 16:50:54 +0800 Subject: [PATCH 4/4] core, eth, triedb: serve historical states over RPC (#31161) This is the part-2 for archive node over path mode, which ultimately ships the functionality to serve the historical states --- core/blockchain_reader.go | 7 ++ core/state/database_history.go | 155 +++++++++++++++++++++++++++++++ eth/api_backend.go | 10 +- eth/state_accessor.go | 9 +- triedb/database.go | 9 ++ triedb/pathdb/history_indexer.go | 2 +- 6 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 core/state/database_history.go diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 6a0cfd6704..046d96063d 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -370,6 +370,13 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) { return state.New(root, bc.statedb) } +// 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)) +} + // Config retrieves the chain's fork configuration. func (bc *BlockChain) Config() *params.ChainConfig { return bc.chainConfig } diff --git a/core/state/database_history.go b/core/state/database_history.go new file mode 100644 index 0000000000..314c56c470 --- /dev/null +++ b/core/state/database_history.go @@ -0,0 +1,155 @@ +// 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 state + +import ( + "errors" + + "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/ethdb" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/utils" + "github.com/ethereum/go-ethereum/triedb" + "github.com/ethereum/go-ethereum/triedb/pathdb" +) + +// historicReader wraps a historical state reader defined in path database, +// providing historic state serving over the path scheme. +// +// TODO(rjl493456442): historicReader is not thread-safe and does not fully +// comply with the StateReader interface requirements, needs to be fixed. +// Currently, it is only used in a non-concurrent context, so it is safe for now. +type historicReader struct { + reader *pathdb.HistoricalStateReader +} + +// newHistoricReader constructs a reader for historic state serving. +func newHistoricReader(r *pathdb.HistoricalStateReader) *historicReader { + return &historicReader{reader: r} +} + +// Account implements StateReader, retrieving the account specified by the address. +// +// An error will be returned if the associated snapshot is already stale or +// the requested account is not yet covered by the snapshot. +// +// The returned account might be nil if it's not existent. +func (r *historicReader) Account(addr common.Address) (*types.StateAccount, error) { + account, err := r.reader.Account(addr) + if err != nil { + return nil, err + } + if account == nil { + return nil, nil + } + acct := &types.StateAccount{ + Nonce: account.Nonce, + Balance: account.Balance, + CodeHash: account.CodeHash, + Root: common.BytesToHash(account.Root), + } + if len(acct.CodeHash) == 0 { + acct.CodeHash = types.EmptyCodeHash.Bytes() + } + if acct.Root == (common.Hash{}) { + acct.Root = types.EmptyRootHash + } + return acct, nil +} + +// Storage implements StateReader, retrieving the storage slot specified by the +// address and slot key. +// +// An error will be returned if the associated snapshot is already stale or +// the requested storage slot is not yet covered by the snapshot. +// +// The returned storage slot might be empty if it's not existent. +func (r *historicReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) { + blob, err := r.reader.Storage(addr, key) + if err != nil { + return common.Hash{}, err + } + if len(blob) == 0 { + return common.Hash{}, nil + } + _, content, _, err := rlp.Split(blob) + if err != nil { + return common.Hash{}, err + } + var slot common.Hash + slot.SetBytes(content) + return slot, nil +} + +// 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] + pointCache *utils.PointCache +} + +// NewHistoricDatabase creates a historic state database. +func NewHistoricDatabase(disk ethdb.KeyValueStore, triedb *triedb.Database) *HistoricDB { + return &HistoricDB{ + disk: disk, + triedb: triedb, + codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize), + codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize), + pointCache: utils.NewPointCache(pointCacheSize), + } +} + +// Reader implements Database interface, returning a reader of the specific state. +func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) { + hr, err := db.triedb.HistoricReader(stateRoot) + if err != nil { + return nil, err + } + return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newHistoricReader(hr)), nil +} + +// OpenTrie opens the main account trie. It's not supported by historic database. +func (db *HistoricDB) OpenTrie(root common.Hash) (Trie, error) { + return nil, errors.New("not implemented") +} + +// OpenStorageTrie opens the storage trie of an account. It's not supported by +// historic database. +func (db *HistoricDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, trie Trie) (Trie, error) { + return nil, errors.New("not implemented") +} + +// PointCache returns the cache holding points used in verkle tree key computation +func (db *HistoricDB) PointCache() *utils.PointCache { + return db.pointCache +} + +// TrieDB returns the underlying trie database for managing trie nodes. +func (db *HistoricDB) TrieDB() *triedb.Database { + return db.triedb +} + +// Snapshot returns the underlying state snapshot. +func (db *HistoricDB) Snapshot() *snapshot.Tree { + return nil +} diff --git a/eth/api_backend.go b/eth/api_backend.go index 8ec19308f9..99fd4c0aa0 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -238,7 +238,10 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B } stateDb, err := b.eth.BlockChain().StateAt(header.Root) if err != nil { - return nil, nil, err + stateDb, err = b.eth.BlockChain().HistoricState(header.Root) + if err != nil { + return nil, nil, err + } } return stateDb, header, nil } @@ -260,7 +263,10 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN } stateDb, err := b.eth.BlockChain().StateAt(header.Root) if err != nil { - return nil, nil, err + stateDb, err = b.eth.BlockChain().HistoricState(header.Root) + if err != nil { + return nil, nil, err + } } return stateDb, header, nil } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 3c3e79a584..79c91043a3 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -182,10 +182,11 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro if err == nil { return statedb, noopReleaser, nil } - // TODO historic state is not supported in path-based scheme. - // Fully archive node in pbss will be implemented by relying - // on state history, but needs more work on top. - return nil, nil, errors.New("historical state not available in path scheme yet") + statedb, err = eth.blockchain.HistoricState(block.Root()) + if err == nil { + return statedb, noopReleaser, nil + } + return nil, nil, errors.New("historical state is not available") } // stateAtBlock retrieves the state database associated with a certain block. diff --git a/triedb/database.go b/triedb/database.go index 03d07c1c1b..12b8856d32 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -129,6 +129,15 @@ func (db *Database) StateReader(blockRoot common.Hash) (database.StateReader, er return db.backend.StateReader(blockRoot) } +// HistoricReader constructs a reader for accessing the requested historic state. +func (db *Database) HistoricReader(root common.Hash) (*pathdb.HistoricalStateReader, error) { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return nil, errors.New("not supported") + } + return pdb.HistoricReader(root) +} + // Update performs a state transition by committing dirty nodes contained in the // given set in order to update state from the specified parent to the specified // root. The held pre-images accumulated up to this point will be flushed in case diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go index 75bba5399a..577eb86b78 100644 --- a/triedb/pathdb/history_indexer.go +++ b/triedb/pathdb/history_indexer.go @@ -504,7 +504,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID ) // Override the ETA if larger than the largest until now eta := time.Duration(left/speed) * time.Millisecond - log.Info("Indexing state history", "processed", done, "left", left, "eta", common.PrettyDuration(eta)) + log.Info("Indexing state history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta)) } } // Check interruption signal and abort process if it's fired