Skip to content

Commit d9e4e4f

Browse files
authored
Add inverted range early return + tests (#1628)
1 parent 7626259 commit d9e4e4f

2 files changed

Lines changed: 250 additions & 5 deletions

File tree

api/events_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ func ConvertRange(chain *chain.Chain, r *Range) (*logdb.Range, error) {
201201
}
202202
}
203203

204+
// A window that falls between two consecutive blocks yields fromBlock > toBlock.
205+
// logdb drops the upper bound on an inverted range and returns every event from
206+
// fromBlock onward (results outside the requested window), so collapse it to
207+
// emptyRange instead.
208+
if fromHeader.Number() > toHeader.Number() {
209+
return &emptyRange, nil
210+
}
211+
204212
return &logdb.Range{
205213
From: fromHeader.Number(),
206214
To: toHeader.Number(),

api/events_types_test.go

Lines changed: 242 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,22 @@
66
package api
77

88
import (
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"math/big"
13+
"slices"
914
"testing"
1015

1116
"github.com/ethereum/go-ethereum/common/hexutil"
1217
"github.com/stretchr/testify/assert"
1318
"github.com/stretchr/testify/require"
1419

20+
"github.com/vechain/thor/v2/genesis"
1521
"github.com/vechain/thor/v2/logdb"
1622
"github.com/vechain/thor/v2/test/testchain"
1723
"github.com/vechain/thor/v2/thor"
24+
"github.com/vechain/thor/v2/tx"
1825
)
1926

2027
func newRange(unit RangeType, from uint64, to uint64) *Range {
@@ -63,16 +70,14 @@ func testConvertRangeWithTimeRangeTypeWithSwitchedFromAndTo(t *testing.T, chain
6370
genesis := chain.GenesisBlock().Header()
6471
bestBlock := chain.Repo().BestBlockSummary()
6572

73+
// Swapped timestamps (from later than to) describe an empty window, so the
74+
// converted range must be empty rather than an inverted range.
6675
rng := newRange(TimeRangeType, bestBlock.Header.Timestamp(), genesis.Timestamp())
67-
expectedRange := &logdb.Range{
68-
From: bestBlock.Header.Number(),
69-
To: genesis.Number(),
70-
}
7176

7277
convRng, err := ConvertRange(chain.Repo().NewBestChain(), rng)
7378

7479
assert.NoError(t, err)
75-
assert.Equal(t, expectedRange, convRng)
80+
assert.Equal(t, &emptyRange, convRng)
7681
}
7782

7883
func testConvertRangeWithBlockRangeType(t *testing.T, chain *testchain.Chain) {
@@ -159,6 +164,125 @@ func initChain(t *testing.T) *testchain.Chain {
159164
return thorChain
160165
}
161166

167+
func TestConvertRange_Matrix(t *testing.T) {
168+
chain := initChain(t)
169+
bestChain := chain.Repo().NewBestChain()
170+
genesis := chain.GenesisBlock().Header()
171+
bestBlock := chain.Repo().BestBlockSummary().Header
172+
173+
// Cases here focus on what TestEventsTypes (which builds ranges via newRange
174+
// with non-nil fields) does not cover: the JSON path, omitted-field defaulting,
175+
// and boundary capping. Explicit from/to ranges are exercised by TestEventsTypes.
176+
tests := []struct {
177+
name string
178+
json string
179+
expected *logdb.Range
180+
}{
181+
{
182+
name: "null JSON returns nil",
183+
json: `null`,
184+
expected: nil,
185+
},
186+
{
187+
name: "empty object returns full block range",
188+
json: `{}`,
189+
expected: &logdb.Range{From: 0, To: uint32(logdb.MaxBlockNumber)},
190+
},
191+
192+
// Block range - omitted-field defaulting and boundary capping.
193+
{
194+
name: "block range with omitted from defaults to 0",
195+
json: `{"unit": "block", "to": 10}`,
196+
expected: &logdb.Range{From: 0, To: 10},
197+
},
198+
{
199+
name: "block range with omitted to defaults to MaxBlockNumber",
200+
json: `{"unit": "block", "from": 5}`,
201+
expected: &logdb.Range{From: 5, To: uint32(logdb.MaxBlockNumber)},
202+
},
203+
{
204+
name: "block range at MaxBlockNumber",
205+
json: `{"unit": "block", "from": 268435455, "to": 268435455}`,
206+
expected: &logdb.Range{From: uint32(logdb.MaxBlockNumber), To: uint32(logdb.MaxBlockNumber)},
207+
},
208+
{
209+
name: "block range to exceeds MaxBlockNumber is capped",
210+
json: `{"unit": "block", "from": 5, "to": 999999999999}`,
211+
expected: &logdb.Range{From: 5, To: uint32(logdb.MaxBlockNumber)},
212+
},
213+
214+
// Time range - omitted-field defaulting.
215+
{
216+
name: "time range with omitted from defaults to genesis",
217+
json: fmt.Sprintf(`{"unit": "time", "to": %d}`, bestBlock.Timestamp()),
218+
expected: &logdb.Range{
219+
From: genesis.Number(),
220+
To: bestBlock.Number(),
221+
},
222+
},
223+
{
224+
name: "time range with omitted to defaults to head",
225+
json: fmt.Sprintf(`{"unit": "time", "from": %d}`, genesis.Timestamp()),
226+
expected: &logdb.Range{
227+
From: genesis.Number(),
228+
To: bestBlock.Number(),
229+
},
230+
},
231+
232+
// Time range - swapped timestamps describe an empty window.
233+
{
234+
name: "time range with from > to (swapped timestamps) returns empty",
235+
json: fmt.Sprintf(`{"unit": "time", "from": %d, "to": %d}`, bestBlock.Timestamp(), genesis.Timestamp()),
236+
expected: &emptyRange,
237+
},
238+
}
239+
240+
for _, tt := range tests {
241+
t.Run(tt.name, func(t *testing.T) {
242+
var rng *Range
243+
if tt.json != "null" {
244+
rng = &Range{}
245+
err := json.Unmarshal([]byte(tt.json), rng)
246+
require.NoError(t, err, "failed to unmarshal JSON: %s", tt.json)
247+
}
248+
249+
result, err := ConvertRange(bestChain, rng)
250+
require.NoError(t, err)
251+
252+
if tt.expected == nil {
253+
assert.Nil(t, result)
254+
} else {
255+
require.NotNil(t, result)
256+
assert.Equal(t, tt.expected.From, result.From, "From mismatch")
257+
assert.Equal(t, tt.expected.To, result.To, "To mismatch")
258+
}
259+
})
260+
}
261+
}
262+
263+
// TestConvertRange_TimeWindowBetweenBlocks is a regression test for a time
264+
// window that falls entirely between two consecutive blocks. The from timestamp
265+
// rounds up to the next block while the to timestamp rounds down to the previous
266+
// block, so the naive conversion produced an inverted range (fromBlock > toBlock)
267+
// which logdb interpreted as "from that block to the chain tip", returning
268+
// wildly over-broad results. It must instead resolve to an empty range.
269+
func TestConvertRange_TimeWindowBetweenBlocks(t *testing.T) {
270+
chain := initChain(t)
271+
bestChain := chain.Repo().NewBestChain()
272+
genesis := chain.GenesisBlock().Header()
273+
274+
// Blocks are spaced by thor.BlockInterval seconds, so [genesis+1, genesis+interval-1]
275+
// contains no block: block 0 is at genesis, block 1 is at genesis+interval.
276+
from := genesis.Timestamp() + 1
277+
to := genesis.Timestamp() + thor.BlockInterval() - 1
278+
require.Less(t, from, to, "test requires a non-empty, valid time window")
279+
280+
result, err := ConvertRange(bestChain, newRange(TimeRangeType, from, to))
281+
require.NoError(t, err)
282+
assert.Equal(t, &emptyRange, result,
283+
"a time window between two blocks must convert to an empty range, not an inverted one")
284+
}
285+
162286
func TestConvertEvent(t *testing.T) {
163287
event := &logdb.Event{
164288
Address: thor.Address{0x01},
@@ -200,3 +324,116 @@ func TestConvertEvent(t *testing.T) {
200324
assert.Equal(t, event.ClauseIndex, result.Meta.ClauseIndex)
201325
assert.Equal(t, expectedTopics, result.Topics)
202326
}
327+
328+
// TestConvertRange_WithEvents exercises ConvertRange end-to-end against a chain
329+
// with events spread across non-contiguous blocks, verifying that the converted
330+
// range actually selects the expected blocks when fed to logdb.
331+
func TestConvertRange_WithEvents(t *testing.T) {
332+
chain, err := testchain.NewDefault()
333+
require.NoError(t, err)
334+
335+
// Block layout:
336+
// Block 0: genesis (no events)
337+
// Blocks 1-2: empty
338+
// Block 3: transfer tx (event)
339+
// Block 4: empty
340+
// Block 5: transfer tx (event)
341+
// Block 6: empty
342+
// Block 7: transfer tx (event)
343+
// Blocks 8-10: empty
344+
for range 2 {
345+
require.NoError(t, chain.MintBlock())
346+
}
347+
348+
acc := genesis.DevAccounts()[0]
349+
recipient := genesis.DevAccounts()[1].Address
350+
clause := tx.NewClause(&recipient).WithValue(big.NewInt(1000))
351+
352+
require.NoError(t, chain.MintClauses(acc, []*tx.Clause{clause}))
353+
eventBlock1 := chain.Repo().BestBlockSummary().Header.Number()
354+
355+
require.NoError(t, chain.MintBlock())
356+
357+
require.NoError(t, chain.MintClauses(acc, []*tx.Clause{clause}))
358+
eventBlock2 := chain.Repo().BestBlockSummary().Header.Number()
359+
360+
require.NoError(t, chain.MintBlock())
361+
362+
require.NoError(t, chain.MintClauses(acc, []*tx.Clause{clause}))
363+
eventBlock3 := chain.Repo().BestBlockSummary().Header.Number()
364+
365+
for range 3 {
366+
require.NoError(t, chain.MintBlock())
367+
}
368+
369+
bestChain := chain.Repo().NewBestChain()
370+
genesisHeader := chain.GenesisBlock().Header()
371+
bestBlock := chain.Repo().BestBlockSummary().Header
372+
373+
filterEvents := func(t *testing.T, rng *logdb.Range) []*logdb.Transfer {
374+
filter := &logdb.TransferFilter{
375+
Range: rng,
376+
Options: &logdb.Options{Limit: 1000},
377+
}
378+
events, err := chain.LogDB().FilterTransfers(context.Background(), filter)
379+
require.NoError(t, err)
380+
return events
381+
}
382+
383+
tests := []struct {
384+
name string
385+
json string
386+
expectedBlocksWithEvents []uint32
387+
}{
388+
{
389+
name: "block range gets all events",
390+
json: fmt.Sprintf(`{"unit": "block", "from": 0, "to": %d}`, bestBlock.Number()),
391+
expectedBlocksWithEvents: []uint32{eventBlock1, eventBlock2, eventBlock3},
392+
},
393+
{
394+
name: "block range covering only first event block",
395+
json: fmt.Sprintf(`{"unit": "block", "from": %d, "to": %d}`, eventBlock1, eventBlock1),
396+
expectedBlocksWithEvents: []uint32{eventBlock1},
397+
},
398+
{
399+
name: "block range covering first two event blocks",
400+
json: fmt.Sprintf(`{"unit": "block", "from": %d, "to": %d}`, eventBlock1, eventBlock2),
401+
expectedBlocksWithEvents: []uint32{eventBlock1, eventBlock2},
402+
},
403+
{
404+
name: "block range in gap between transfers returns nothing",
405+
json: fmt.Sprintf(`{"unit": "block", "from": %d, "to": %d}`, eventBlock1+1, eventBlock2-1),
406+
expectedBlocksWithEvents: nil,
407+
},
408+
{
409+
name: "block range from middle event onwards",
410+
json: fmt.Sprintf(`{"unit": "block", "from": %d}`, eventBlock2),
411+
expectedBlocksWithEvents: []uint32{eventBlock2, eventBlock3},
412+
},
413+
{
414+
name: "time range from genesis to best gets all events",
415+
json: fmt.Sprintf(`{"unit": "time", "from": %d, "to": %d}`, genesisHeader.Timestamp(), bestBlock.Timestamp()),
416+
expectedBlocksWithEvents: []uint32{eventBlock1, eventBlock2, eventBlock3},
417+
},
418+
}
419+
420+
for _, tt := range tests {
421+
t.Run(tt.name, func(t *testing.T) {
422+
rng := &Range{}
423+
require.NoError(t, json.Unmarshal([]byte(tt.json), rng), "failed to unmarshal JSON: %s", tt.json)
424+
425+
convertedRange, err := ConvertRange(bestChain, rng)
426+
require.NoError(t, err)
427+
428+
var gotBlocks []uint32
429+
for _, ev := range filterEvents(t, convertedRange) {
430+
if !slices.Contains(gotBlocks, ev.BlockNumber) {
431+
gotBlocks = append(gotBlocks, ev.BlockNumber)
432+
}
433+
}
434+
435+
assert.Equal(t, tt.expectedBlocksWithEvents, gotBlocks,
436+
"expected events in blocks %v, got events in blocks %v", tt.expectedBlocksWithEvents, gotBlocks)
437+
})
438+
}
439+
}

0 commit comments

Comments
 (0)