mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
triedb, triedb/pathdb: add ordinal access and lookup primitives for history index
Read-side primitives over the per-account state-history index, needed by
the upcoming eth_getTransactionBySenderAndNonce RPC (see execution-apis
spec discussion):
- HistoryIndexReader with ordinal Count/At access, backed by
indexReader.count/at and blockReader.readAt which walk the
restart-encoded index blocks by position.
- AccountHistoryIndex, HistoricAccount, BlockNumberAt and
LastIndexedBlockNumber on *pathdb.Database, with
ErrStateHistoryNotIndexed and ErrStateHistoryPruned sentinels.
- PathDB accessor on *triedb.Database for typed backend access.
Adapted to the grouped freezer API (Tail now takes a history group).
This commit is contained in:
parent
edb398a102
commit
ee67a19018
6 changed files with 387 additions and 0 deletions
|
|
@ -142,6 +142,13 @@ func (db *Database) HistoricStateReader(root common.Hash) (*pathdb.HistoricalSta
|
|||
return pdb.HistoricReader(root)
|
||||
}
|
||||
|
||||
// PathDB returns the underlying pathdb-backed database if this database is
|
||||
// using the path-based scheme. Returns nil for hashdb or other backends.
|
||||
func (db *Database) PathDB() *pathdb.Database {
|
||||
pdb, _ := db.backend.(*pathdb.Database)
|
||||
return pdb
|
||||
}
|
||||
|
||||
// HistoricNodeReader constructs a reader for accessing the historical trie node.
|
||||
func (db *Database) HistoricNodeReader(root common.Hash) (*pathdb.HistoricalNodeReader, error) {
|
||||
pdb, ok := db.backend.(*pathdb.Database)
|
||||
|
|
|
|||
|
|
@ -124,6 +124,37 @@ func (r *indexReader) refresh() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// count returns the total number of indexed entries across all descriptor
|
||||
// blocks.
|
||||
func (r *indexReader) count() int {
|
||||
var n int
|
||||
for _, d := range r.descList {
|
||||
n += int(d.entries)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// at returns the i-th indexed history id in ascending order. The caller must
|
||||
// guarantee 0 <= i < r.count().
|
||||
func (r *indexReader) at(i int) (uint64, error) {
|
||||
if i < 0 {
|
||||
return 0, fmt.Errorf("negative ordinal %d", i)
|
||||
}
|
||||
remaining := i
|
||||
for _, desc := range r.descList {
|
||||
n := int(desc.entries)
|
||||
if remaining < n {
|
||||
br, err := r.blockReader(desc.id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return br.readAt(remaining, n)
|
||||
}
|
||||
remaining -= n
|
||||
}
|
||||
return 0, fmt.Errorf("ordinal out of range, i: %d, total: %d", i, r.count())
|
||||
}
|
||||
|
||||
// blockReader returns a cached block reader for the given block id, loading it
|
||||
// from disk on first access.
|
||||
func (r *indexReader) blockReader(id uint32) (*blockReader, error) {
|
||||
|
|
|
|||
|
|
@ -194,6 +194,53 @@ func newBlockReader(blob []byte, hasExt bool) (*blockReader, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// readAt returns the element at the given block-local ordinal position.
|
||||
// The caller must guarantee 0 <= i < entries; entries is the number of
|
||||
// elements stored in the block as recorded in its descriptor.
|
||||
func (br *blockReader) readAt(i int, entries int) (uint64, error) {
|
||||
if i < 0 || i >= entries {
|
||||
return 0, fmt.Errorf("ordinal out of range, i: %d, entries: %d", i, entries)
|
||||
}
|
||||
section := i / indexBlockRestartLen
|
||||
if section >= len(br.restarts) {
|
||||
return 0, fmt.Errorf("invalid restart section %d, restarts: %d", section, len(br.restarts))
|
||||
}
|
||||
var (
|
||||
pos = int(br.restarts[section])
|
||||
limit int
|
||||
skip = i % indexBlockRestartLen
|
||||
val uint64
|
||||
)
|
||||
if section == len(br.restarts)-1 {
|
||||
limit = len(br.data)
|
||||
} else {
|
||||
limit = int(br.restarts[section+1])
|
||||
}
|
||||
for step := 0; step <= skip; step++ {
|
||||
if pos >= limit {
|
||||
return 0, fmt.Errorf("ordinal walk past section end, pos: %d, limit: %d, step: %d", pos, limit, step)
|
||||
}
|
||||
x, n := binary.Uvarint(br.data[pos:])
|
||||
if n <= 0 {
|
||||
return 0, fmt.Errorf("failed to decode element at pos %d", pos)
|
||||
}
|
||||
if step == 0 {
|
||||
val = x
|
||||
} else {
|
||||
val += x
|
||||
}
|
||||
pos += n
|
||||
if br.hasExt {
|
||||
length, ln := binary.Uvarint(br.data[pos:])
|
||||
if ln <= 0 {
|
||||
return 0, fmt.Errorf("failed to decode extension length at pos %d", pos)
|
||||
}
|
||||
pos += ln + int(length)
|
||||
}
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// readGreaterThan locates the first element in the block that is greater than
|
||||
// the specified value. If no such element is found, MaxUint64 is returned.
|
||||
func (br *blockReader) readGreaterThan(id uint64) (uint64, error) {
|
||||
|
|
|
|||
|
|
@ -208,6 +208,52 @@ func testIndexWriterWithLimit(t *testing.T, bitmapSize int) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIndexReaderOrdinalAccess(t *testing.T) {
|
||||
testIndexReaderOrdinalAccess(t, 0)
|
||||
testIndexReaderOrdinalAccess(t, 2)
|
||||
testIndexReaderOrdinalAccess(t, 34)
|
||||
}
|
||||
|
||||
func testIndexReaderOrdinalAccess(t *testing.T, bitmapSize int) {
|
||||
// Build a sequence long enough to span multiple restart sections and
|
||||
// index blocks, with varying gaps to exercise the delta decoding.
|
||||
var (
|
||||
elements []uint64
|
||||
id uint64
|
||||
)
|
||||
for i := 0; i < 10_000; i++ {
|
||||
id += uint64(i%7) + 1
|
||||
elements = append(elements, id)
|
||||
}
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}), 0, bitmapSize)
|
||||
for _, v := range elements {
|
||||
if err := iw.append(v, randomExt(bitmapSize, 5)); err != nil {
|
||||
t.Fatalf("Failed to append %d: %v", v, err)
|
||||
}
|
||||
}
|
||||
batch := db.NewBatch()
|
||||
iw.finish(batch)
|
||||
batch.Write()
|
||||
|
||||
r, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}), bitmapSize)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct index reader: %v", err)
|
||||
}
|
||||
if got, want := r.count(), len(elements); got != want {
|
||||
t.Fatalf("count mismatch: got %d, want %d", got, want)
|
||||
}
|
||||
for i, want := range elements {
|
||||
got, err := r.at(i)
|
||||
if err != nil {
|
||||
t.Fatalf("at(%d) returned error: %v", i, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("at(%d) = %d, want %d", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexDeleterBasic(t *testing.T) {
|
||||
testIndexDeleterBasic(t, 0)
|
||||
testIndexDeleterBasic(t, 2)
|
||||
|
|
|
|||
156
triedb/pathdb/history_lookup.go
Normal file
156
triedb/pathdb/history_lookup.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"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/rlp"
|
||||
)
|
||||
|
||||
// ErrStateHistoryNotIndexed is returned when the state history index is not
|
||||
// available on this database, either because the indexer is disabled, the
|
||||
// freezer is not configured, or the initial build has not yet finished.
|
||||
var ErrStateHistoryNotIndexed = errors.New("state history is not indexed")
|
||||
|
||||
// ErrStateHistoryPruned is returned when the requested state history id has
|
||||
// been pruned from the freezer tail.
|
||||
var ErrStateHistoryPruned = errors.New("state history has been pruned")
|
||||
|
||||
// HistoryIndexReader provides random-access reads over the sorted list of
|
||||
// state-history ids at which a single state element was modified. The
|
||||
// sequence is strictly increasing.
|
||||
type HistoryIndexReader interface {
|
||||
// Count returns the total number of indexed modifications.
|
||||
Count() int
|
||||
|
||||
// At returns the i-th history id. The caller must ensure 0 <= i < Count().
|
||||
At(i int) (uint64, error)
|
||||
}
|
||||
|
||||
// accountHistoryIndexReader exposes ordinal access over an account's history
|
||||
// index. Not safe for concurrent use.
|
||||
type accountHistoryIndexReader struct {
|
||||
reader *indexReader
|
||||
}
|
||||
|
||||
// Count implements HistoryIndexReader.
|
||||
func (r *accountHistoryIndexReader) Count() int {
|
||||
return r.reader.count()
|
||||
}
|
||||
|
||||
// At implements HistoryIndexReader.
|
||||
func (r *accountHistoryIndexReader) At(i int) (uint64, error) {
|
||||
return r.reader.at(i)
|
||||
}
|
||||
|
||||
// AccountHistoryIndex returns a random-access reader over the state-history
|
||||
// ids at which the given account was modified, or ErrStateHistoryNotIndexed
|
||||
// if the index is unavailable. The returned reader is not safe for
|
||||
// concurrent use.
|
||||
func (db *Database) AccountHistoryIndex(addr common.Address) (HistoryIndexReader, error) {
|
||||
if err := db.checkStateIndexerReady(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ident := newAccountIdent(crypto.Keccak256Hash(addr.Bytes()))
|
||||
r, err := newIndexReader(db.diskdb, ident, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &accountHistoryIndexReader{reader: r}, nil
|
||||
}
|
||||
|
||||
// LastIndexedBlockNumber returns the block number of the most recently
|
||||
// indexed state history; blocks above it are not yet covered by the index.
|
||||
// Returns 0 with nil error if the indexer is ready but has produced no
|
||||
// entries yet.
|
||||
func (db *Database) LastIndexedBlockNumber() (uint64, error) {
|
||||
if err := db.checkStateIndexerReady(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
metadata := loadIndexMetadata(db.diskdb, typeStateHistory)
|
||||
if metadata == nil || metadata.Last == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
m, err := readStateHistoryMeta(db.stateFreezer, metadata.Last)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m.block, nil
|
||||
}
|
||||
|
||||
// BlockNumberAt returns the block number associated with the given
|
||||
// state-history id. Returns ErrStateHistoryPruned if the id falls at or below
|
||||
// the current freezer tail.
|
||||
func (db *Database) BlockNumberAt(historyID uint64) (uint64, error) {
|
||||
if db.stateFreezer == nil {
|
||||
return 0, ErrStateHistoryNotIndexed
|
||||
}
|
||||
tail, err := db.stateFreezer.Tail(rawdb.DefaultHistoryGroup)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if historyID <= tail {
|
||||
return 0, ErrStateHistoryPruned
|
||||
}
|
||||
m, err := readStateHistoryMeta(db.stateFreezer, historyID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m.block, nil
|
||||
}
|
||||
|
||||
// HistoricAccount returns the pre-state of addr at the given history id,
|
||||
// i.e., the account state at the start of the block recorded by that
|
||||
// history. Returns nil if the account did not exist at that point. The id
|
||||
// must correspond to an entry where addr was actually modified (typically
|
||||
// one drawn from AccountHistoryIndex(addr)).
|
||||
func (db *Database) HistoricAccount(addr common.Address, historyID uint64) (*types.SlimAccount, error) {
|
||||
if err := db.checkStateIndexerReady(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := newStateHistoryReader(db.diskdb, db.stateFreezer)
|
||||
blob, err := r.readAccount(addr, historyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(blob) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
account := new(types.SlimAccount)
|
||||
if err := rlp.DecodeBytes(blob, account); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode account: %w", err)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// checkStateIndexerReady returns nil if the state-history indexer is
|
||||
// available and the initial build has completed.
|
||||
func (db *Database) checkStateIndexerReady() error {
|
||||
if db.stateIndexer == nil || db.stateFreezer == nil {
|
||||
return ErrStateHistoryNotIndexed
|
||||
}
|
||||
if !db.stateIndexer.inited() {
|
||||
return ErrStateHistoryNotIndexed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
100
triedb/pathdb/history_lookup_test.go
Normal file
100
triedb/pathdb/history_lookup_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
func TestAccountHistoryIndex_NotIndexed(t *testing.T) {
|
||||
// Indexing disabled: every accessor should bail with ErrStateHistoryNotIndexed.
|
||||
env := newTester(t, &testerConfig{layers: 4, enableIndex: false})
|
||||
defer env.release()
|
||||
|
||||
var addr common.Address
|
||||
for h := range env.accounts {
|
||||
addr = env.accountPreimage(h)
|
||||
break
|
||||
}
|
||||
|
||||
if _, err := env.db.AccountHistoryIndex(addr); !errors.Is(err, ErrStateHistoryNotIndexed) {
|
||||
t.Fatalf("AccountHistoryIndex: want ErrStateHistoryNotIndexed, got %v", err)
|
||||
}
|
||||
if _, err := env.db.HistoricAccount(addr, 1); !errors.Is(err, ErrStateHistoryNotIndexed) {
|
||||
t.Fatalf("HistoricAccount: want ErrStateHistoryNotIndexed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountHistoryIndex_Indexed(t *testing.T) {
|
||||
// Force diff-layer flushes so state history is actually written.
|
||||
maxDiffLayers = 4
|
||||
defer func() { maxDiffLayers = 128 }()
|
||||
|
||||
env := newTester(t, &testerConfig{layers: 32, enableIndex: true})
|
||||
defer env.release()
|
||||
waitIndexing(env.db)
|
||||
|
||||
// Pick any account with at least one indexed entry.
|
||||
var (
|
||||
addr common.Address
|
||||
idx HistoryIndexReader
|
||||
)
|
||||
for h := range env.accounts {
|
||||
a := env.accountPreimage(h)
|
||||
r, err := env.db.AccountHistoryIndex(a)
|
||||
if err != nil {
|
||||
t.Fatalf("AccountHistoryIndex(%x): %v", a, err)
|
||||
}
|
||||
if r.Count() > 0 {
|
||||
addr = a
|
||||
idx = r
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == nil {
|
||||
t.Fatal("no indexed account found across all current-state accounts")
|
||||
}
|
||||
|
||||
// Every id reported by the index must yield a HistoricAccount read.
|
||||
for i := 0; i < idx.Count(); i++ {
|
||||
hid, err := idx.At(i)
|
||||
if err != nil {
|
||||
t.Fatalf("idx.At(%d): %v", i, err)
|
||||
}
|
||||
if _, err := env.db.HistoricAccount(addr, hid); err != nil {
|
||||
t.Fatalf("HistoricAccount(addr, %d): %v", hid, err)
|
||||
}
|
||||
blockNum, err := env.db.BlockNumberAt(hid)
|
||||
if err != nil {
|
||||
t.Fatalf("BlockNumberAt(%d): %v", hid, err)
|
||||
}
|
||||
if blockNum >= uint64(len(env.roots)) {
|
||||
t.Fatalf("BlockNumberAt(%d) = %d, beyond generated range %d", hid, blockNum, len(env.roots))
|
||||
}
|
||||
}
|
||||
|
||||
last, err := env.db.LastIndexedBlockNumber()
|
||||
if err != nil {
|
||||
t.Fatalf("LastIndexedBlockNumber: %v", err)
|
||||
}
|
||||
if last == 0 {
|
||||
t.Fatal("LastIndexedBlockNumber returned 0 after waitIndexing")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue