Skip to content

Commit 94da734

Browse files
committed
fix(eth): check events are indexed within in requested range
Complete work from #12728
1 parent 5d208e0 commit 94da734

File tree

2 files changed

+119
-65
lines changed

2 files changed

+119
-65
lines changed

chain/index/events.go

Lines changed: 90 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ import (
1717
"github.com/filecoin-project/go-address"
1818
amt4 "github.com/filecoin-project/go-amt-ipld/v4"
1919
"github.com/filecoin-project/go-state-types/abi"
20-
"github.com/filecoin-project/lotus/chain/types"
2120
blockadt "github.com/filecoin-project/specs-actors/actors/util/adt"
21+
22+
"github.com/filecoin-project/lotus/chain/types"
2223
)
2324

2425
var (
25-
ErrMaxResultsReached = fmt.Errorf("filter matches too many events, try a more restricted filter")
26-
ErrRangeInFuture = fmt.Errorf("range end is in the future")
26+
ErrMaxResultsReached = xerrors.New("filter matches too many events, try a more restricted filter")
27+
ErrRangeInFuture = xerrors.New("range end is in the future")
2728
)
2829

2930
const maxLookBackForWait = 120 // one hour of tipsets
@@ -238,101 +239,135 @@ func loadExecutedMessages(ctx context.Context, cs ChainStore, recomputeTipSetSta
238239
return ems, nil
239240
}
240241

241-
// checkRangeIndexedStatus verifies if a range of heights is indexed.
242-
// It checks for the existence of non-null rounds at the range boundaries.
243-
func (si *SqliteIndexer) checkRangeIndexedStatus(ctx context.Context, f *EventFilter) error {
244-
minHeight := f.MinHeight
245-
maxHeight := f.MaxHeight
242+
// checkFilterTipsetsIndexed verifies if a tipset, or a range of tipsets, specified by a given
243+
// filter is indexed. It checks for the existence of non-null rounds at the range boundaries.
244+
func (si *SqliteIndexer) checkFilterTipsetsIndexed(ctx context.Context, f *EventFilter) error {
245+
// Three cases to consider:
246+
// 1. Specific tipset is provided
247+
// 2. Single tipset is specified by the height range (min=max)
248+
// 3. Range of tipsets is specified by the height range (min!=max)
249+
// We'll handle the first two cases here and the third case in checkRangeIndexedStatus
250+
251+
var tipsetKeyCid []byte
252+
var err error
253+
254+
switch {
255+
case f.TipsetCid != cid.Undef:
256+
tipsetKeyCid = f.TipsetCid.Bytes()
257+
case f.MinHeight >= 0 && f.MinHeight == f.MaxHeight:
258+
tipsetKeyCid, err = si.getTipsetKeyCidByHeight(ctx, f.MinHeight)
259+
if err != nil {
260+
if err == ErrNotFound {
261+
// this means that this is a null round and there exist no events for this epoch
262+
return nil
263+
}
264+
return xerrors.Errorf("failed to get tipset key cid by height: %w", err)
265+
}
266+
default:
267+
return si.checkRangeIndexedStatus(ctx, f.MinHeight, f.MaxHeight)
268+
}
269+
270+
// If we couldn't determine a specific tipset, return ErrNotFound
271+
if tipsetKeyCid == nil {
272+
return ErrNotFound
273+
}
274+
275+
// Check if the determined tipset is indexed
276+
if exists, err := si.isTipsetIndexed(ctx, tipsetKeyCid); err != nil {
277+
return xerrors.Errorf("failed to check if tipset is indexed: %w", err)
278+
} else if exists {
279+
return nil // Tipset is indexed
280+
}
281+
282+
return ErrNotFound // Tipset is not indexed
283+
}
284+
285+
// checkRangeIndexedStatus verifies if a range of tipsets specified by the given height range is
286+
// indexed. It checks for the existence of non-null rounds at the range boundaries.
287+
func (si *SqliteIndexer) checkRangeIndexedStatus(ctx context.Context, minHeight abi.ChainEpoch, maxHeight abi.ChainEpoch) error {
288+
head := si.cs.GetHeaviestTipSet()
289+
if minHeight > head.Height() || maxHeight > head.Height() {
290+
return ErrRangeInFuture
291+
}
246292

247293
// Find the first non-null round in the range
248-
startCid, err := si.findFirstNonNullRound(ctx, &minHeight, maxHeight)
294+
startCid, startHeight, err := si.findFirstNonNullRound(ctx, minHeight, maxHeight)
249295
if err != nil {
250296
return xerrors.Errorf("failed to find first non-null round: %w", err)
251297
}
252-
253298
// If all rounds are null, consider the range valid
254299
if startCid == nil {
255300
return nil
256301
}
257302

258303
// Find the last non-null round in the range
259-
endCid, err := si.findLastNonNullRound(ctx, &maxHeight, minHeight)
304+
endCid, endHeight, err := si.findLastNonNullRound(ctx, maxHeight, minHeight)
260305
if err != nil {
261-
if errors.Is(err, ErrRangeInFuture) {
262-
return xerrors.Errorf("range end is in the future: %w", err)
263-
}
264306
return xerrors.Errorf("failed to find last non-null round: %w", err)
265307
}
266-
267308
// If all rounds are null, consider the range valid
268309
if endCid == nil {
269-
return nil
310+
return xerrors.Errorf("unexpected error finding last non-null round: all rounds are null but start round is not (%d to %d)", minHeight, maxHeight)
270311
}
271312

272-
// Check indexing for start and end tipsets
273-
if err := si.checkTipsetByKeyCid(ctx, startCid, minHeight); err != nil {
313+
// Check indexing status for start and end tipsets
314+
if err := si.checkTipsetIndexedStatus(ctx, startCid, startHeight); err != nil {
274315
return err
275316
}
276-
277-
if err := si.checkTipsetByKeyCid(ctx, endCid, maxHeight); err != nil {
317+
if err := si.checkTipsetIndexedStatus(ctx, endCid, endHeight); err != nil {
278318
return err
279319
}
320+
// Assume (not necessarily correctly, but likely) that all tipsets within the range are indexed
280321

281322
return nil
282323
}
283324

284-
// checkTipsetByKeyCid checks if a tipset identified by its key CID is indexed.
285-
func (si *SqliteIndexer) checkTipsetByKeyCid(ctx context.Context, tipsetKeyCid []byte, height abi.ChainEpoch) error {
325+
func (si *SqliteIndexer) checkTipsetIndexedStatus(ctx context.Context, tipsetKeyCid []byte, height abi.ChainEpoch) error {
286326
exists, err := si.isTipsetIndexed(ctx, tipsetKeyCid)
287327
if err != nil {
288-
return xerrors.Errorf("failed to check if tipset at height %d is indexed: %w", height, err)
328+
return xerrors.Errorf("failed to check if tipset at epoch %d is indexed: %w", height, err)
329+
} else if exists {
330+
return nil // has been indexed
289331
}
290-
291-
if exists {
292-
return nil // null round
293-
}
294-
295-
return ErrNotFound // tipset is not indexed
332+
return ErrNotFound
296333
}
297334

298-
// findFirstNonNullRound finds the first non-null round starting from minHeight up to maxHeight
299-
func (si *SqliteIndexer) findFirstNonNullRound(ctx context.Context, minHeight *abi.ChainEpoch, maxHeight abi.ChainEpoch) ([]byte, error) {
300-
for height := *minHeight; height <= maxHeight; height++ {
335+
// findFirstNonNullRound finds the first non-null round starting from minHeight up to maxHeight.
336+
// It updates the minHeight to the found height and returns the tipset key CID.
337+
func (si *SqliteIndexer) findFirstNonNullRound(ctx context.Context, minHeight abi.ChainEpoch, maxHeight abi.ChainEpoch) ([]byte, abi.ChainEpoch, error) {
338+
for height := minHeight; height <= maxHeight; height++ {
301339
cid, err := si.getTipsetKeyCidByHeight(ctx, height)
302-
if err == nil {
303-
*minHeight = height // Update the minHeight to the found height
304-
return cid, nil
305-
}
306-
if !errors.Is(err, ErrNotFound) {
307-
return nil, xerrors.Errorf("failed to get tipset key cid for height %d: %w", height, err)
340+
if err != nil {
341+
if !errors.Is(err, ErrNotFound) {
342+
return nil, 0, xerrors.Errorf("failed to get tipset key cid for height %d: %w", height, err)
343+
}
344+
// else null round, keep searching
345+
continue
308346
}
347+
minHeight = height // Update the minHeight to the found height
348+
return cid, minHeight, nil
309349
}
310-
311-
return nil, nil
350+
// All rounds are null
351+
return nil, 0, nil
312352
}
313353

314354
// findLastNonNullRound finds the last non-null round starting from maxHeight down to minHeight
315-
func (si *SqliteIndexer) findLastNonNullRound(ctx context.Context, maxHeight *abi.ChainEpoch, minHeight abi.ChainEpoch) ([]byte, error) {
316-
head := si.cs.GetHeaviestTipSet()
317-
if head == nil || *maxHeight > head.Height() {
318-
return nil, ErrRangeInFuture
319-
}
320-
321-
for height := *maxHeight; height >= minHeight; height-- {
355+
func (si *SqliteIndexer) findLastNonNullRound(ctx context.Context, maxHeight abi.ChainEpoch, minHeight abi.ChainEpoch) ([]byte, abi.ChainEpoch, error) {
356+
for height := maxHeight; height >= minHeight; height-- {
322357
cid, err := si.getTipsetKeyCidByHeight(ctx, height)
323358
if err == nil {
324-
*maxHeight = height // Update the maxHeight to the found height
325-
return cid, nil
359+
maxHeight = height // Update the maxHeight to the found height
360+
return cid, maxHeight, nil
326361
}
327362
if !errors.Is(err, ErrNotFound) {
328-
return nil, xerrors.Errorf("failed to get tipset key cid for height %d: %w", height, err)
363+
return nil, 0, xerrors.Errorf("failed to get tipset key cid for height %d: %w", height, err)
329364
}
330365
}
331366

332-
return nil, nil
367+
return nil, 0, nil
333368
}
334369

335-
// getTipsetKeyCidByHeight retrieves the tipset key CID for a given height.
370+
// getTipsetKeyCidByHeight retrieves the tipset key CID for a given height from the ChainStore
336371
func (si *SqliteIndexer) getTipsetKeyCidByHeight(ctx context.Context, height abi.ChainEpoch) ([]byte, error) {
337372
ts, err := si.cs.GetTipsetByHeight(ctx, height, nil, false)
338373
if err != nil {
@@ -504,15 +539,15 @@ func (si *SqliteIndexer) GetEventsForFilter(ctx context.Context, f *EventFilter)
504539
if height > 0 {
505540
head := si.cs.GetHeaviestTipSet()
506541
if head == nil {
507-
return nil, errors.New("failed to get head: head is nil")
542+
return nil, xerrors.New("failed to get head: head is nil")
508543
}
509544
headHeight := head.Height()
510545
maxLookBackHeight := headHeight - maxLookBackForWait
511546

512547
// if the height is old enough, we'll assume the index is caught up to it and not bother
513548
// waiting for it to be indexed
514549
if height <= maxLookBackHeight {
515-
return nil, si.checkRangeIndexedStatus(ctx, f)
550+
return nil, si.checkFilterTipsetsIndexed(ctx, f)
516551
}
517552
}
518553

@@ -526,7 +561,7 @@ func (si *SqliteIndexer) GetEventsForFilter(ctx context.Context, f *EventFilter)
526561
}
527562

528563
if len(ces) == 0 {
529-
return nil, si.checkRangeIndexedStatus(ctx, f)
564+
return nil, si.checkFilterTipsetsIndexed(ctx, f)
530565
}
531566
}
532567

chain/index/events_test.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ func TestGetEventsForFilterNoEvents(t *testing.T) {
3030
si, _, cs := setupWithHeadIndexed(t, headHeight, rng)
3131
t.Cleanup(func() { _ = si.Close() })
3232

33-
// Create a fake tipset at height 1
34-
fakeTipSet1 := fakeTipSet(t, rng, 1, nil)
35-
36-
// Set the dummy chainstore to return this tipset for height 1
37-
cs.SetTipsetByHeightAndKey(1, fakeTipSet1.Key(), fakeTipSet1) // empty DB
38-
cs.SetTipSetByCid(t, fakeTipSet1)
33+
// Create a fake tipset at various heights used in the test
34+
fakeTipsets := make(map[abi.ChainEpoch]*types.TipSet)
35+
for _, ts := range []abi.ChainEpoch{1, 10, 20} {
36+
fakeTipsets[ts] = fakeTipSet(t, rng, ts, nil)
37+
cs.SetTipsetByHeightAndKey(ts, fakeTipsets[ts].Key(), fakeTipsets[ts])
38+
cs.SetTipSetByCid(t, fakeTipsets[ts])
39+
}
3940

4041
// tipset is not indexed
4142
f := &EventFilter{
@@ -46,7 +47,7 @@ func TestGetEventsForFilterNoEvents(t *testing.T) {
4647
require.True(t, errors.Is(err, ErrNotFound))
4748
require.Equal(t, 0, len(ces))
4849

49-
tsCid, err := fakeTipSet1.Key().Cid()
50+
tsCid, err := fakeTipsets[1].Key().Cid()
5051
require.NoError(t, err)
5152
f = &EventFilter{
5253
TipsetCid: tsCid,
@@ -58,7 +59,7 @@ func TestGetEventsForFilterNoEvents(t *testing.T) {
5859

5960
// tipset is indexed but has no events
6061
err = withTx(ctx, si.db, func(tx *sql.Tx) error {
61-
return si.indexTipset(ctx, tx, fakeTipSet1)
62+
return si.indexTipset(ctx, tx, fakeTipsets[1])
6263
})
6364
require.NoError(t, err)
6465

@@ -73,13 +74,31 @@ func TestGetEventsForFilterNoEvents(t *testing.T) {
7374
require.NoError(t, err)
7475
require.Equal(t, 0, len(ces))
7576

76-
// search for a range that is absent
77+
// search for a range that is not indexed
78+
f = &EventFilter{
79+
MinHeight: 10,
80+
MaxHeight: 20,
81+
}
82+
ces, err = si.GetEventsForFilter(ctx, f)
83+
require.ErrorIs(t, err, ErrNotFound)
84+
require.Equal(t, 0, len(ces))
85+
86+
// search for a range (end) that is in the future
87+
f = &EventFilter{
88+
MinHeight: 10,
89+
MaxHeight: 200,
90+
}
91+
ces, err = si.GetEventsForFilter(ctx, f)
92+
require.ErrorIs(t, err, ErrRangeInFuture)
93+
require.Equal(t, 0, len(ces))
94+
95+
// search for a range (start too) that is in the future
7796
f = &EventFilter{
7897
MinHeight: 100,
7998
MaxHeight: 200,
8099
}
81100
ces, err = si.GetEventsForFilter(ctx, f)
82-
require.NoError(t, err)
101+
require.ErrorIs(t, err, ErrRangeInFuture)
83102
require.Equal(t, 0, len(ces))
84103
}
85104

0 commit comments

Comments
 (0)