Skip to content

Commit cd166e8

Browse files
zsfelfoldisivaratrisrinivas
authored andcommitted
core/filtermaps: fix map renderer reorg issue (ethereum#31642)
This PR fixes a bug in the map renderer that sometimes used an obsolete block log value pointer to initialize the iterator for rendering from a snapshot. This bug was triggered by chain reorgs and sometimes caused indexing errors and invalid search results. A few other conditions are also made safer that were not reported to cause issues yet but could potentially be unsafe in some corner cases. A new unit test is also added that reproduced the bug but passes with the new fixes. Fixes ethereum#31593 Might also fix ethereum#31589 though this issue has not been reproduced yet, but it appears to be related to a log index database corruption around a specific block, similarly to the other issue. Note that running this branch resets and regenerates the log index database. For this purpose a `Version` field has been added to `rawdb.FilterMapsRange` which will also make this easier in the future if a breaking database change is needed or the existing one is considered potentially broken due to a bug, like in this case.
1 parent 85ce3b7 commit cd166e8

File tree

5 files changed

+153
-33
lines changed

5 files changed

+153
-33
lines changed

core/filtermaps/filtermaps.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ var (
5050
)
5151

5252
const (
53+
databaseVersion = 1 // reindexed if database version does not match
5354
cachedLastBlocks = 1000 // last block of map pointers
5455
cachedLvPointers = 1000 // first log value pointer of block pointers
5556
cachedBaseRows = 100 // groups of base layer filter row data
@@ -138,13 +139,25 @@ type FilterMaps struct {
138139
// as transparent (uncached/unchanged).
139140
type filterMap []FilterRow
140141

141-
// copy returns a copy of the given filter map. Note that the row slices are
142-
// copied but their contents are not. This permits extending the rows further
142+
// fastCopy returns a copy of the given filter map. Note that the row slices are
143+
// copied but their contents are not. This permits appending to the rows further
143144
// (which happens during map rendering) without affecting the validity of
144145
// copies made for snapshots during rendering.
145-
func (fm filterMap) copy() filterMap {
146+
// Appending to the rows of both the original map and the fast copy, or two fast
147+
// copies of the same map would result in data corruption, therefore a fast copy
148+
// should always be used in a read only way.
149+
func (fm filterMap) fastCopy() filterMap {
150+
return slices.Clone(fm)
151+
}
152+
153+
// fullCopy returns a copy of the given filter map, also making a copy of each
154+
// individual filter row, ensuring that a modification to either one will never
155+
// affect the other.
156+
func (fm filterMap) fullCopy() filterMap {
146157
c := make(filterMap, len(fm))
147-
copy(c, fm)
158+
for i, row := range fm {
159+
c[i] = slices.Clone(row)
160+
}
148161
return c
149162
}
150163

@@ -207,8 +220,9 @@ type Config struct {
207220
// NewFilterMaps creates a new FilterMaps and starts the indexer.
208221
func NewFilterMaps(db ethdb.KeyValueStore, initView *ChainView, historyCutoff, finalBlock uint64, params Params, config Config) *FilterMaps {
209222
rs, initialized, err := rawdb.ReadFilterMapsRange(db)
210-
if err != nil {
211-
log.Error("Error reading log index range", "error", err)
223+
if err != nil || rs.Version != databaseVersion {
224+
rs, initialized = rawdb.FilterMapsRange{}, false
225+
log.Warn("Invalid log index database version; resetting log index")
212226
}
213227
params.deriveFields()
214228
f := &FilterMaps{
@@ -437,6 +451,7 @@ func (f *FilterMaps) setRange(batch ethdb.KeyValueWriter, newView *ChainView, ne
437451
f.updateMatchersValidRange()
438452
if newRange.initialized {
439453
rs := rawdb.FilterMapsRange{
454+
Version: databaseVersion,
440455
HeadIndexed: newRange.headIndexed,
441456
HeadDelimiter: newRange.headDelimiter,
442457
BlocksFirst: newRange.blocks.First(),

core/filtermaps/indexer_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package filtermaps
1818

1919
import (
20+
"context"
2021
crand "crypto/rand"
2122
"crypto/sha256"
23+
"encoding/binary"
2224
"math/big"
2325
"math/rand"
2426
"sync"
@@ -31,6 +33,7 @@ import (
3133
"github.com/ethereum/go-ethereum/core/types"
3234
"github.com/ethereum/go-ethereum/ethdb"
3335
"github.com/ethereum/go-ethereum/params"
36+
"github.com/ethereum/go-ethereum/rlp"
3437
)
3538

3639
var testParams = Params{
@@ -104,6 +107,7 @@ func TestIndexerRandomRange(t *testing.T) {
104107
fork, head = rand.Intn(len(forks)), rand.Intn(1001)
105108
ts.chain.setCanonicalChain(forks[fork][:head+1])
106109
case 2:
110+
checkSnapshot = false
107111
if head < 1000 {
108112
checkSnapshot = !noHistory && head != 0 // no snapshot generated for block 0
109113
// add blocks after the current head
@@ -158,6 +162,63 @@ func TestIndexerRandomRange(t *testing.T) {
158162
}
159163
}
160164

165+
func TestIndexerMatcherView(t *testing.T) {
166+
testIndexerMatcherView(t, false)
167+
}
168+
169+
func TestIndexerMatcherViewWithConcurrentRead(t *testing.T) {
170+
testIndexerMatcherView(t, true)
171+
}
172+
173+
func testIndexerMatcherView(t *testing.T, concurrentRead bool) {
174+
ts := newTestSetup(t)
175+
defer ts.close()
176+
177+
forks := make([][]common.Hash, 20)
178+
hashes := make([]common.Hash, 20)
179+
ts.chain.addBlocks(100, 5, 2, 4, true)
180+
ts.setHistory(0, false)
181+
for i := range forks {
182+
if i != 0 {
183+
ts.chain.setHead(100 - i)
184+
ts.chain.addBlocks(i, 5, 2, 4, true)
185+
}
186+
ts.fm.WaitIdle()
187+
forks[i] = ts.chain.getCanonicalChain()
188+
hashes[i] = ts.matcherViewHash()
189+
}
190+
fork := len(forks) - 1
191+
for i := 0; i < 5000; i++ {
192+
oldFork := fork
193+
fork = rand.Intn(len(forks))
194+
stopCh := make(chan chan struct{})
195+
if concurrentRead {
196+
go func() {
197+
for {
198+
ts.matcherViewHash()
199+
select {
200+
case ch := <-stopCh:
201+
close(ch)
202+
return
203+
default:
204+
}
205+
}
206+
}()
207+
}
208+
ts.chain.setCanonicalChain(forks[fork])
209+
ts.fm.WaitIdle()
210+
if concurrentRead {
211+
ch := make(chan struct{})
212+
stopCh <- ch
213+
<-ch
214+
}
215+
hash := ts.matcherViewHash()
216+
if hash != hashes[fork] {
217+
t.Fatalf("Matcher view hash mismatch when switching from for %d to %d", oldFork, fork)
218+
}
219+
}
220+
}
221+
161222
func TestIndexerCompareDb(t *testing.T) {
162223
ts := newTestSetup(t)
163224
defer ts.close()
@@ -291,6 +352,55 @@ func (ts *testSetup) fmDbHash() common.Hash {
291352
return result
292353
}
293354

355+
func (ts *testSetup) matcherViewHash() common.Hash {
356+
mb := ts.fm.NewMatcherBackend()
357+
defer mb.Close()
358+
359+
ctx := context.Background()
360+
params := mb.GetParams()
361+
hasher := sha256.New()
362+
var headPtr uint64
363+
for b := uint64(0); ; b++ {
364+
lvptr, err := mb.GetBlockLvPointer(ctx, b)
365+
if err != nil || (b > 0 && lvptr == headPtr) {
366+
break
367+
}
368+
var enc [8]byte
369+
binary.LittleEndian.PutUint64(enc[:], lvptr)
370+
hasher.Write(enc[:])
371+
headPtr = lvptr
372+
}
373+
headMap := uint32(headPtr >> params.logValuesPerMap)
374+
var enc [12]byte
375+
for r := uint32(0); r < params.mapHeight; r++ {
376+
binary.LittleEndian.PutUint32(enc[:4], r)
377+
for m := uint32(0); m <= headMap; m++ {
378+
binary.LittleEndian.PutUint32(enc[4:8], m)
379+
row, _ := mb.GetFilterMapRow(ctx, m, r, false)
380+
for _, v := range row {
381+
binary.LittleEndian.PutUint32(enc[8:], v)
382+
hasher.Write(enc[:])
383+
}
384+
}
385+
}
386+
var hash common.Hash
387+
hasher.Sum(hash[:0])
388+
for i := 0; i < 50; i++ {
389+
hasher.Reset()
390+
hasher.Write(hash[:])
391+
lvptr := binary.LittleEndian.Uint64(hash[:8]) % headPtr
392+
if log, _ := mb.GetLogByLvIndex(ctx, lvptr); log != nil {
393+
enc, err := rlp.EncodeToBytes(log)
394+
if err != nil {
395+
panic(err)
396+
}
397+
hasher.Write(enc)
398+
}
399+
hasher.Sum(hash[:0])
400+
}
401+
return hash
402+
}
403+
294404
func (ts *testSetup) close() {
295405
if ts.fm != nil {
296406
ts.fm.Stop()

core/filtermaps/map_renderer.go

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (f *FilterMaps) renderMapsBefore(renderBefore uint32) (*mapRenderer, error)
8484
if err != nil {
8585
return nil, err
8686
}
87-
if snapshot := f.lastCanonicalSnapshotBefore(renderBefore); snapshot != nil && snapshot.mapIndex >= nextMap {
87+
if snapshot := f.lastCanonicalSnapshotOfMap(nextMap); snapshot != nil {
8888
return f.renderMapsFromSnapshot(snapshot)
8989
}
9090
if nextMap >= renderBefore {
@@ -97,14 +97,14 @@ func (f *FilterMaps) renderMapsBefore(renderBefore uint32) (*mapRenderer, error)
9797
// snapshot made at a block boundary.
9898
func (f *FilterMaps) renderMapsFromSnapshot(cp *renderedMap) (*mapRenderer, error) {
9999
f.testSnapshotUsed = true
100-
iter, err := f.newLogIteratorFromBlockDelimiter(cp.lastBlock)
100+
iter, err := f.newLogIteratorFromBlockDelimiter(cp.lastBlock, cp.headDelimiter)
101101
if err != nil {
102102
return nil, fmt.Errorf("failed to create log iterator from block delimiter %d: %v", cp.lastBlock, err)
103103
}
104104
return &mapRenderer{
105105
f: f,
106106
currentMap: &renderedMap{
107-
filterMap: cp.filterMap.copy(),
107+
filterMap: cp.filterMap.fullCopy(),
108108
mapIndex: cp.mapIndex,
109109
lastBlock: cp.lastBlock,
110110
blockLvPtrs: cp.blockLvPtrs,
@@ -137,14 +137,14 @@ func (f *FilterMaps) renderMapsFromMapBoundary(firstMap, renderBefore uint32, st
137137
}, nil
138138
}
139139

140-
// lastCanonicalSnapshotBefore returns the latest cached snapshot that matches
141-
// the current targetView.
142-
func (f *FilterMaps) lastCanonicalSnapshotBefore(renderBefore uint32) *renderedMap {
140+
// lastCanonicalSnapshotOfMap returns the latest cached snapshot of the given map
141+
// that is also consistent with the current targetView.
142+
func (f *FilterMaps) lastCanonicalSnapshotOfMap(mapIndex uint32) *renderedMap {
143143
var best *renderedMap
144144
for _, blockNumber := range f.renderSnapshots.Keys() {
145145
if cp, _ := f.renderSnapshots.Get(blockNumber); cp != nil && blockNumber < f.indexedRange.blocks.AfterLast() &&
146146
blockNumber <= f.targetView.headNumber && f.targetView.getBlockId(blockNumber) == cp.lastBlockId &&
147-
cp.mapIndex < renderBefore && (best == nil || blockNumber > best.lastBlock) {
147+
cp.mapIndex == mapIndex && (best == nil || blockNumber > best.lastBlock) {
148148
best = cp
149149
}
150150
}
@@ -171,10 +171,9 @@ func (f *FilterMaps) lastCanonicalMapBoundaryBefore(renderBefore uint32) (nextMa
171171
if err != nil {
172172
return 0, 0, 0, fmt.Errorf("failed to retrieve last block of reverse iterated map %d: %v", mapIndex, err)
173173
}
174-
if lastBlock >= f.indexedView.headNumber || lastBlock >= f.targetView.headNumber ||
175-
lastBlockId != f.targetView.getBlockId(lastBlock) {
176-
// map is not full or inconsistent with targetView; roll back
177-
continue
174+
if (f.indexedRange.headIndexed && mapIndex >= f.indexedRange.maps.Last()) ||
175+
lastBlock >= f.targetView.headNumber || lastBlockId != f.targetView.getBlockId(lastBlock) {
176+
continue // map is not full or inconsistent with targetView; roll back
178177
}
179178
lvPtr, err := f.getBlockLvPointer(lastBlock)
180179
if err != nil {
@@ -257,11 +256,14 @@ func (f *FilterMaps) loadHeadSnapshot() error {
257256

258257
// makeSnapshot creates a snapshot of the current state of the rendered map.
259258
func (r *mapRenderer) makeSnapshot() {
260-
r.f.renderSnapshots.Add(r.iterator.blockNumber, &renderedMap{
261-
filterMap: r.currentMap.filterMap.copy(),
259+
if r.iterator.blockNumber != r.currentMap.lastBlock || r.iterator.chainView != r.f.targetView {
260+
panic("iterator state inconsistent with current rendered map")
261+
}
262+
r.f.renderSnapshots.Add(r.currentMap.lastBlock, &renderedMap{
263+
filterMap: r.currentMap.filterMap.fastCopy(),
262264
mapIndex: r.currentMap.mapIndex,
263-
lastBlock: r.iterator.blockNumber,
264-
lastBlockId: r.f.targetView.getBlockId(r.currentMap.lastBlock),
265+
lastBlock: r.currentMap.lastBlock,
266+
lastBlockId: r.iterator.chainView.getBlockId(r.currentMap.lastBlock),
265267
blockLvPtrs: r.currentMap.blockLvPtrs,
266268
finished: true,
267269
headDelimiter: r.iterator.lvIndex,
@@ -661,24 +663,13 @@ var errUnindexedRange = errors.New("unindexed range")
661663
// newLogIteratorFromBlockDelimiter creates a logIterator starting at the
662664
// given block's first log value entry (the block delimiter), according to the
663665
// current targetView.
664-
func (f *FilterMaps) newLogIteratorFromBlockDelimiter(blockNumber uint64) (*logIterator, error) {
666+
func (f *FilterMaps) newLogIteratorFromBlockDelimiter(blockNumber, lvIndex uint64) (*logIterator, error) {
665667
if blockNumber > f.targetView.headNumber {
666668
return nil, fmt.Errorf("iterator entry point %d after target chain head block %d", blockNumber, f.targetView.headNumber)
667669
}
668670
if !f.indexedRange.blocks.Includes(blockNumber) {
669671
return nil, errUnindexedRange
670672
}
671-
var lvIndex uint64
672-
if f.indexedRange.headIndexed && blockNumber+1 == f.indexedRange.blocks.AfterLast() {
673-
lvIndex = f.indexedRange.headDelimiter
674-
} else {
675-
var err error
676-
lvIndex, err = f.getBlockLvPointer(blockNumber + 1)
677-
if err != nil {
678-
return nil, fmt.Errorf("failed to retrieve log value pointer of block %d after delimiter: %v", blockNumber+1, err)
679-
}
680-
lvIndex--
681-
}
682673
finished := blockNumber == f.targetView.headNumber
683674
l := &logIterator{
684675
chainView: f.targetView,

core/filtermaps/matcher_backend.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ func (fm *FilterMapsMatcherBackend) Close() {
7575
// on write.
7676
// GetFilterMapRow implements MatcherBackend.
7777
func (fm *FilterMapsMatcherBackend) GetFilterMapRow(ctx context.Context, mapIndex, rowIndex uint32, baseLayerOnly bool) (FilterRow, error) {
78+
fm.f.indexLock.RLock()
79+
defer fm.f.indexLock.RUnlock()
80+
7881
return fm.f.getFilterMapRow(mapIndex, rowIndex, baseLayerOnly)
7982
}
8083

core/rawdb/accessors_indexes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ func DeleteBlockLvPointers(db ethdb.KeyValueStore, blocks common.Range[uint64],
434434
// FilterMapsRange is a storage representation of the block range covered by the
435435
// filter maps structure and the corresponting log value index range.
436436
type FilterMapsRange struct {
437+
Version uint32
437438
HeadIndexed bool
438439
HeadDelimiter uint64
439440
BlocksFirst, BlocksAfterLast uint64

0 commit comments

Comments
 (0)