Skip to content

Commit a92f2b8

Browse files
authored
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
1 parent ce63bba commit a92f2b8

File tree

6 files changed

+185
-7
lines changed

6 files changed

+185
-7
lines changed

core/blockchain_reader.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,13 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
370370
return state.New(root, bc.statedb)
371371
}
372372

373+
// HistoricState returns a historic state specified by the given root.
374+
// Live states are not available and won't be served, please use `State`
375+
// or `StateAt` instead.
376+
func (bc *BlockChain) HistoricState(root common.Hash) (*state.StateDB, error) {
377+
return state.New(root, state.NewHistoricDatabase(bc.db, bc.triedb))
378+
}
379+
373380
// Config retrieves the chain's fork configuration.
374381
func (bc *BlockChain) Config() *params.ChainConfig { return bc.chainConfig }
375382

core/state/database_history.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package state
18+
19+
import (
20+
"errors"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
"github.com/ethereum/go-ethereum/common/lru"
24+
"github.com/ethereum/go-ethereum/core/state/snapshot"
25+
"github.com/ethereum/go-ethereum/core/types"
26+
"github.com/ethereum/go-ethereum/ethdb"
27+
"github.com/ethereum/go-ethereum/rlp"
28+
"github.com/ethereum/go-ethereum/trie/utils"
29+
"github.com/ethereum/go-ethereum/triedb"
30+
"github.com/ethereum/go-ethereum/triedb/pathdb"
31+
)
32+
33+
// historicReader wraps a historical state reader defined in path database,
34+
// providing historic state serving over the path scheme.
35+
//
36+
// TODO(rjl493456442): historicReader is not thread-safe and does not fully
37+
// comply with the StateReader interface requirements, needs to be fixed.
38+
// Currently, it is only used in a non-concurrent context, so it is safe for now.
39+
type historicReader struct {
40+
reader *pathdb.HistoricalStateReader
41+
}
42+
43+
// newHistoricReader constructs a reader for historic state serving.
44+
func newHistoricReader(r *pathdb.HistoricalStateReader) *historicReader {
45+
return &historicReader{reader: r}
46+
}
47+
48+
// Account implements StateReader, retrieving the account specified by the address.
49+
//
50+
// An error will be returned if the associated snapshot is already stale or
51+
// the requested account is not yet covered by the snapshot.
52+
//
53+
// The returned account might be nil if it's not existent.
54+
func (r *historicReader) Account(addr common.Address) (*types.StateAccount, error) {
55+
account, err := r.reader.Account(addr)
56+
if err != nil {
57+
return nil, err
58+
}
59+
if account == nil {
60+
return nil, nil
61+
}
62+
acct := &types.StateAccount{
63+
Nonce: account.Nonce,
64+
Balance: account.Balance,
65+
CodeHash: account.CodeHash,
66+
Root: common.BytesToHash(account.Root),
67+
}
68+
if len(acct.CodeHash) == 0 {
69+
acct.CodeHash = types.EmptyCodeHash.Bytes()
70+
}
71+
if acct.Root == (common.Hash{}) {
72+
acct.Root = types.EmptyRootHash
73+
}
74+
return acct, nil
75+
}
76+
77+
// Storage implements StateReader, retrieving the storage slot specified by the
78+
// address and slot key.
79+
//
80+
// An error will be returned if the associated snapshot is already stale or
81+
// the requested storage slot is not yet covered by the snapshot.
82+
//
83+
// The returned storage slot might be empty if it's not existent.
84+
func (r *historicReader) Storage(addr common.Address, key common.Hash) (common.Hash, error) {
85+
blob, err := r.reader.Storage(addr, key)
86+
if err != nil {
87+
return common.Hash{}, err
88+
}
89+
if len(blob) == 0 {
90+
return common.Hash{}, nil
91+
}
92+
_, content, _, err := rlp.Split(blob)
93+
if err != nil {
94+
return common.Hash{}, err
95+
}
96+
var slot common.Hash
97+
slot.SetBytes(content)
98+
return slot, nil
99+
}
100+
101+
// HistoricDB is the implementation of Database interface, with the ability to
102+
// access historical state.
103+
type HistoricDB struct {
104+
disk ethdb.KeyValueStore
105+
triedb *triedb.Database
106+
codeCache *lru.SizeConstrainedCache[common.Hash, []byte]
107+
codeSizeCache *lru.Cache[common.Hash, int]
108+
pointCache *utils.PointCache
109+
}
110+
111+
// NewHistoricDatabase creates a historic state database.
112+
func NewHistoricDatabase(disk ethdb.KeyValueStore, triedb *triedb.Database) *HistoricDB {
113+
return &HistoricDB{
114+
disk: disk,
115+
triedb: triedb,
116+
codeCache: lru.NewSizeConstrainedCache[common.Hash, []byte](codeCacheSize),
117+
codeSizeCache: lru.NewCache[common.Hash, int](codeSizeCacheSize),
118+
pointCache: utils.NewPointCache(pointCacheSize),
119+
}
120+
}
121+
122+
// Reader implements Database interface, returning a reader of the specific state.
123+
func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) {
124+
hr, err := db.triedb.HistoricReader(stateRoot)
125+
if err != nil {
126+
return nil, err
127+
}
128+
return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), newHistoricReader(hr)), nil
129+
}
130+
131+
// OpenTrie opens the main account trie. It's not supported by historic database.
132+
func (db *HistoricDB) OpenTrie(root common.Hash) (Trie, error) {
133+
return nil, errors.New("not implemented")
134+
}
135+
136+
// OpenStorageTrie opens the storage trie of an account. It's not supported by
137+
// historic database.
138+
func (db *HistoricDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, trie Trie) (Trie, error) {
139+
return nil, errors.New("not implemented")
140+
}
141+
142+
// PointCache returns the cache holding points used in verkle tree key computation
143+
func (db *HistoricDB) PointCache() *utils.PointCache {
144+
return db.pointCache
145+
}
146+
147+
// TrieDB returns the underlying trie database for managing trie nodes.
148+
func (db *HistoricDB) TrieDB() *triedb.Database {
149+
return db.triedb
150+
}
151+
152+
// Snapshot returns the underlying state snapshot.
153+
func (db *HistoricDB) Snapshot() *snapshot.Tree {
154+
return nil
155+
}

eth/api_backend.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,10 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B
238238
}
239239
stateDb, err := b.eth.BlockChain().StateAt(header.Root)
240240
if err != nil {
241-
return nil, nil, err
241+
stateDb, err = b.eth.BlockChain().HistoricState(header.Root)
242+
if err != nil {
243+
return nil, nil, err
244+
}
242245
}
243246
return stateDb, header, nil
244247
}
@@ -260,7 +263,10 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN
260263
}
261264
stateDb, err := b.eth.BlockChain().StateAt(header.Root)
262265
if err != nil {
263-
return nil, nil, err
266+
stateDb, err = b.eth.BlockChain().HistoricState(header.Root)
267+
if err != nil {
268+
return nil, nil, err
269+
}
264270
}
265271
return stateDb, header, nil
266272
}

eth/state_accessor.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,11 @@ func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), erro
182182
if err == nil {
183183
return statedb, noopReleaser, nil
184184
}
185-
// TODO historic state is not supported in path-based scheme.
186-
// Fully archive node in pbss will be implemented by relying
187-
// on state history, but needs more work on top.
188-
return nil, nil, errors.New("historical state not available in path scheme yet")
185+
statedb, err = eth.blockchain.HistoricState(block.Root())
186+
if err == nil {
187+
return statedb, noopReleaser, nil
188+
}
189+
return nil, nil, errors.New("historical state is not available")
189190
}
190191

191192
// stateAtBlock retrieves the state database associated with a certain block.

triedb/database.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ func (db *Database) StateReader(blockRoot common.Hash) (database.StateReader, er
129129
return db.backend.StateReader(blockRoot)
130130
}
131131

132+
// HistoricReader constructs a reader for accessing the requested historic state.
133+
func (db *Database) HistoricReader(root common.Hash) (*pathdb.HistoricalStateReader, error) {
134+
pdb, ok := db.backend.(*pathdb.Database)
135+
if !ok {
136+
return nil, errors.New("not supported")
137+
}
138+
return pdb.HistoricReader(root)
139+
}
140+
132141
// Update performs a state transition by committing dirty nodes contained in the
133142
// given set in order to update state from the specified parent to the specified
134143
// root. The held pre-images accumulated up to this point will be flushed in case

triedb/pathdb/history_indexer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
504504
)
505505
// Override the ETA if larger than the largest until now
506506
eta := time.Duration(left/speed) * time.Millisecond
507-
log.Info("Indexing state history", "processed", done, "left", left, "eta", common.PrettyDuration(eta))
507+
log.Info("Indexing state history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta))
508508
}
509509
}
510510
// Check interruption signal and abort process if it's fired

0 commit comments

Comments
 (0)