mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
Merge ee67a19018 into dddbaa4bf3
This commit is contained in:
commit
cf75c0fb9e
7 changed files with 404 additions and 8 deletions
|
|
@ -142,6 +142,13 @@ func (db *Database) HistoricStateReader(root common.Hash) (*pathdb.HistoricalSta
|
||||||
return pdb.HistoricReader(root)
|
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.
|
// HistoricNodeReader constructs a reader for accessing the historical trie node.
|
||||||
func (db *Database) HistoricNodeReader(root common.Hash) (*pathdb.HistoricalNodeReader, error) {
|
func (db *Database) HistoricNodeReader(root common.Hash) (*pathdb.HistoricalNodeReader, error) {
|
||||||
pdb, ok := db.backend.(*pathdb.Database)
|
pdb, ok := db.backend.(*pathdb.Database)
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,51 @@ func (r *indexReader) refresh() error {
|
||||||
return nil
|
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) {
|
||||||
|
if br, ok := r.readers[id]; ok {
|
||||||
|
return br, nil
|
||||||
|
}
|
||||||
|
br, err := newBlockReader(readStateIndexBlock(r.state, r.db, id), r.bitmapSize != 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.readers[id] = br
|
||||||
|
return br, nil
|
||||||
|
}
|
||||||
|
|
||||||
// readGreaterThan locates the first element that is greater than the specified
|
// readGreaterThan locates the first element that is greater than the specified
|
||||||
// id. If no such element is found, MaxUint64 is returned.
|
// id. If no such element is found, MaxUint64 is returned.
|
||||||
func (r *indexReader) readGreaterThan(id uint64) (uint64, error) {
|
func (r *indexReader) readGreaterThan(id uint64) (uint64, error) {
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,53 @@ func newBlockReader(blob []byte, hasExt bool) (*blockReader, error) {
|
||||||
}, nil
|
}, 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
|
// readGreaterThan locates the first element in the block that is greater than
|
||||||
// the specified value. If no such element is found, MaxUint64 is returned.
|
// the specified value. If no such element is found, MaxUint64 is returned.
|
||||||
func (br *blockReader) readGreaterThan(id uint64) (uint64, error) {
|
func (br *blockReader) readGreaterThan(id uint64) (uint64, error) {
|
||||||
|
|
|
||||||
|
|
@ -446,14 +446,9 @@ type indexIterator struct {
|
||||||
|
|
||||||
// newBlockIter initializes the block iterator with the specified block ID.
|
// newBlockIter initializes the block iterator with the specified block ID.
|
||||||
func (r *indexReader) newBlockIter(id uint32, filter *extFilter) (*blockIterator, error) {
|
func (r *indexReader) newBlockIter(id uint32, filter *extFilter) (*blockIterator, error) {
|
||||||
br, ok := r.readers[id]
|
br, err := r.blockReader(id)
|
||||||
if !ok {
|
if err != nil {
|
||||||
var err error
|
return nil, err
|
||||||
br, err = newBlockReader(readStateIndexBlock(r.state, r.db, id), r.bitmapSize != 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
r.readers[id] = br
|
|
||||||
}
|
}
|
||||||
return br.newIterator(filter), nil
|
return br.newIterator(filter), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestIndexDeleterBasic(t *testing.T) {
|
||||||
testIndexDeleterBasic(t, 0)
|
testIndexDeleterBasic(t, 0)
|
||||||
testIndexDeleterBasic(t, 2)
|
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