|
6 | 6 | package api |
7 | 7 |
|
8 | 8 | import ( |
| 9 | + "context" |
| 10 | + "encoding/json" |
| 11 | + "fmt" |
| 12 | + "math/big" |
| 13 | + "slices" |
9 | 14 | "testing" |
10 | 15 |
|
11 | 16 | "github.com/ethereum/go-ethereum/common/hexutil" |
12 | 17 | "github.com/stretchr/testify/assert" |
13 | 18 | "github.com/stretchr/testify/require" |
14 | 19 |
|
| 20 | + "github.com/vechain/thor/v2/genesis" |
15 | 21 | "github.com/vechain/thor/v2/logdb" |
16 | 22 | "github.com/vechain/thor/v2/test/testchain" |
17 | 23 | "github.com/vechain/thor/v2/thor" |
| 24 | + "github.com/vechain/thor/v2/tx" |
18 | 25 | ) |
19 | 26 |
|
20 | 27 | func newRange(unit RangeType, from uint64, to uint64) *Range { |
@@ -63,16 +70,14 @@ func testConvertRangeWithTimeRangeTypeWithSwitchedFromAndTo(t *testing.T, chain |
63 | 70 | genesis := chain.GenesisBlock().Header() |
64 | 71 | bestBlock := chain.Repo().BestBlockSummary() |
65 | 72 |
|
| 73 | + // Swapped timestamps (from later than to) describe an empty window, so the |
| 74 | + // converted range must be empty rather than an inverted range. |
66 | 75 | rng := newRange(TimeRangeType, bestBlock.Header.Timestamp(), genesis.Timestamp()) |
67 | | - expectedRange := &logdb.Range{ |
68 | | - From: bestBlock.Header.Number(), |
69 | | - To: genesis.Number(), |
70 | | - } |
71 | 76 |
|
72 | 77 | convRng, err := ConvertRange(chain.Repo().NewBestChain(), rng) |
73 | 78 |
|
74 | 79 | assert.NoError(t, err) |
75 | | - assert.Equal(t, expectedRange, convRng) |
| 80 | + assert.Equal(t, &emptyRange, convRng) |
76 | 81 | } |
77 | 82 |
|
78 | 83 | func testConvertRangeWithBlockRangeType(t *testing.T, chain *testchain.Chain) { |
@@ -159,6 +164,125 @@ func initChain(t *testing.T) *testchain.Chain { |
159 | 164 | return thorChain |
160 | 165 | } |
161 | 166 |
|
| 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 | + |
162 | 286 | func TestConvertEvent(t *testing.T) { |
163 | 287 | event := &logdb.Event{ |
164 | 288 | Address: thor.Address{0x01}, |
@@ -200,3 +324,116 @@ func TestConvertEvent(t *testing.T) { |
200 | 324 | assert.Equal(t, event.ClauseIndex, result.Meta.ClauseIndex) |
201 | 325 | assert.Equal(t, expectedTopics, result.Topics) |
202 | 326 | } |
| 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