Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions evmrpc/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import (

var logger = seilog.NewLogger("evmrpc")

const TxSearchPerPage = 10

const (
// DB Concurrency Read Limit
MaxDBReadConcurrency = 16
Expand Down Expand Up @@ -388,6 +386,8 @@ func (a *FilterAPI) updateFilterAccess(filterID ethrpc.ID) {
}
}

const NewFilterMethod = "newFilter"

func (a *FilterAPI) NewFilter(
ctx context.Context,
crit filters.FilterCriteria,
Expand Down Expand Up @@ -437,6 +437,8 @@ func (a *FilterAPI) NewPendingTransactionFilter(
return "", &ErrEVMNotSupported{Msg: "eth_newPendingTransactionFilter is not supported on Sei EVM RPC"}
}

const GetFilterChangesMethod = "getFilterChanges"

func (a *FilterAPI) GetFilterChanges(
ctx context.Context,
filterID ethrpc.ID,
Expand All @@ -455,6 +457,7 @@ func (a *FilterAPI) GetFilterChanges(
// Update access time
a.updateFilterAccess(filterID)

result := []*ethtypes.Log{}
switch filter.typ {
case BlocksSubscription:
hashes, cursor, err := a.getBlockHeadersAfter(ctx, filter.blockCursor)
Expand All @@ -474,16 +477,19 @@ func (a *FilterAPI) GetFilterChanges(
case LogsSubscription:
// filter by hash would have no updates if it has previously queried for this crit
if filter.fc.BlockHash != nil && filter.lastToHeight > 0 {
return nil, nil
return result, nil
}
// filter with a ToBlock would have no updates if it has previously queried for this crit
if filter.fc.ToBlock != nil && filter.lastToHeight >= filter.fc.ToBlock.Int64() {
return nil, nil
return result, nil
}
logs, lastToHeight, err := a.logFetcher.GetLogsByFilters(ctx, filter.fc, filter.lastToHeight)
if err != nil {
return nil, err
}
if logs == nil {
logs = result
}

// Update filter with write lock
a.filtersMu.Lock()
Expand All @@ -499,6 +505,8 @@ func (a *FilterAPI) GetFilterChanges(
}
}

const GetFilterLogsMethod = "getFilterLogs"

func (a *FilterAPI) GetFilterLogs(
ctx context.Context,
filterID ethrpc.ID,
Expand All @@ -521,6 +529,9 @@ func (a *FilterAPI) GetFilterLogs(
if err != nil {
return nil, err
}
if logs == nil {
logs = []*ethtypes.Log{}
}

// Update filter with write lock
a.filtersMu.Lock()
Expand Down Expand Up @@ -942,7 +953,7 @@ func (f *LogFetcher) tryFilterLogsRange(ctx context.Context, fromBlock, toBlock
}

if len(logs) == 0 {
return logs, nil
return []*ethtypes.Log{}, nil
}

return f.normalizeRangeQueryLogs(ctx, logs, crit)
Expand Down
86 changes: 69 additions & 17 deletions evmrpc/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestFilterNew(t *testing.T) {
filterCriteria["fromBlock"] = tt.fromBlock
filterCriteria["toBlock"] = tt.toBlock
}
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
_, errExists := resObj["error"]

if tt.wantErr {
Expand All @@ -72,7 +72,7 @@ func TestFilterNew(t *testing.T) {
require.False(t, errExists, "error should not exist")
got := resObj["result"].(string)
// make sure next filter id is not equal to this one
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
got2 := resObj["result"].(string)
require.NotEqual(t, got, got2)
}
Expand All @@ -86,7 +86,7 @@ func TestFilterUninstall(t *testing.T) {
filterCriteria := map[string]interface{}{
"fromBlock": "0x1",
}
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)
require.NotEmpty(t, filterId)

Expand Down Expand Up @@ -305,10 +305,10 @@ func TestFilterGetFilterLogs(t *testing.T) {
"fromBlock": "0x2",
"toBlock": "0x2",
}
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

resObj = sendRequest(t, TestPort, "getFilterLogs", filterId)
resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, filterId)
logs := resObj["result"].([]interface{})
require.Equal(t, 4, len(logs))
for _, log := range logs {
Expand All @@ -318,7 +318,7 @@ func TestFilterGetFilterLogs(t *testing.T) {

// error: filter id does not exist
nonexistentFilterId := 1000
resObj = sendRequest(t, TestPort, "getFilterLogs", nonexistentFilterId)
resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, nonexistentFilterId)
_, ok := resObj["error"]
require.True(t, ok)
}
Expand All @@ -328,10 +328,10 @@ func TestFilterGetFilterChanges(t *testing.T) {
filterCriteria := map[string]interface{}{
"fromBlock": "0x2",
}
resObj := sendRequest(t, TestPort, "newFilter", filterCriteria)
resObj := sendRequest(t, TestPort, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

resObj = sendRequest(t, TestPort, "getFilterChanges", filterId)
resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId)
logs := resObj["result"].([]interface{})
// After tightening block/receipt matching, fromBlock=0x2 now yields 5 logs total
require.Equal(t, 5, len(logs))
Expand All @@ -340,7 +340,7 @@ func TestFilterGetFilterChanges(t *testing.T) {

// error: filter id does not exist
nonExistingFilterId := 1000
resObj = sendRequest(t, TestPort, "getFilterChanges", nonExistingFilterId)
resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, nonExistingFilterId)
_, ok := resObj["error"]
require.True(t, ok)
}
Expand All @@ -349,15 +349,15 @@ func TestFilterBlockFilter(t *testing.T) {
t.Parallel()
resObj := sendRequestGood(t, "newBlockFilter")
blockFilterId := resObj["result"].(string)
resObj = sendRequestGood(t, "getFilterChanges", blockFilterId)
resObj = sendRequestGood(t, evmrpc.GetFilterChangesMethod, blockFilterId)
hashesInterface := resObj["result"].([]interface{})
for _, hashInterface := range hashesInterface {
hash := hashInterface.(string)
require.Equal(t, 66, len(hash))
require.Equal(t, "0x", hash[:2])
}
// query again to make sure cursor is updated
resObj = sendRequestGood(t, "getFilterChanges", blockFilterId)
resObj = sendRequestGood(t, evmrpc.GetFilterChangesMethod, blockFilterId)
hashesInterface = resObj["result"].([]interface{})
for _, hashInterface := range hashesInterface {
hash := hashInterface.(string)
Expand All @@ -371,13 +371,13 @@ func TestFilterExpiration(t *testing.T) {
filterCriteria := map[string]interface{}{
"fromBlock": "0x1",
}
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

// wait for filter to expire
time.Sleep(2 * filterTimeoutDuration)

resObj = sendRequest(t, TestPort, "getFilterLogs", filterId)
resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, filterId)
_, ok := resObj["error"]
require.True(t, ok)
}
Expand All @@ -387,12 +387,12 @@ func TestFilterGetFilterLogsKeepsFilterAlive(t *testing.T) {
filterCriteria := map[string]interface{}{
"fromBlock": "0x1",
}
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

for i := 0; i < 5; i++ {
// should keep filter alive
resObj = sendRequestGood(t, "getFilterLogs", filterId)
resObj = sendRequestGood(t, evmrpc.GetFilterLogsMethod, filterId)
_, ok := resObj["error"]
require.False(t, ok)
time.Sleep(filterTimeoutDuration / 2)
Expand All @@ -404,12 +404,12 @@ func TestFilterGetFilterChangesKeepsFilterAlive(t *testing.T) {
filterCriteria := map[string]interface{}{
"fromBlock": "0x1",
}
resObj := sendRequestGood(t, "newFilter", filterCriteria)
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

for i := 0; i < 5; i++ {
// should keep filter alive
resObj = sendRequestGood(t, "getFilterChanges", filterId)
resObj = sendRequestGood(t, evmrpc.GetFilterChangesMethod, filterId)
_, ok := resObj["error"]
require.False(t, ok)
time.Sleep(filterTimeoutDuration / 2)
Expand Down Expand Up @@ -594,3 +594,55 @@ func TestCollectLogsEvmTransactionIndex(t *testing.T) {

require.Len(t, seen, len(expectedByHash), "should observe logs for all expected EVM txs in block 2")
}

// TestFilterGetFilterChangesEmptyOnExhaustedBoundedFilter asserts that
// eth_getFilterChanges returns [] (not null) once a bounded filter's range is
// fully consumed (Ethereum JSON-RPC spec requires an array, never null).
func TestFilterGetFilterChangesEmptyOnExhaustedBoundedFilter(t *testing.T) {
t.Parallel()
filterCriteria := map[string]interface{}{
"fromBlock": "0x2",
"toBlock": "0x2",
}
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

// First call: consumes the range; result may be non-empty, but must be an array.
resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId)
_, hasErr := resObj["error"]
require.False(t, hasErr)
require.NotNil(t, resObj["result"], "first getFilterChanges should return [] not null")
_, ok := resObj["result"].([]interface{})
require.True(t, ok, "first getFilterChanges result should be an array")

// Second call: range is exhausted; must return [] not null.
resObj = sendRequest(t, TestPort, evmrpc.GetFilterChangesMethod, filterId)
_, hasErr = resObj["error"]
require.False(t, hasErr)
require.NotNil(t, resObj["result"], "exhausted getFilterChanges should return [] not null")
logs, ok := resObj["result"].([]interface{})
require.True(t, ok, "exhausted getFilterChanges result should be an array")
require.Empty(t, logs)
}

// TestFilterGetFilterLogsEmptyResultIsArray asserts that eth_getFilterLogs
// returns [] (not null) when no logs match the filter criteria.
func TestFilterGetFilterLogsEmptyResultIsArray(t *testing.T) {
t.Parallel()
// Use an address that is guaranteed to have no logs in the mock data.
filterCriteria := map[string]interface{}{
"fromBlock": "0x1",
"toBlock": "0x1",
"address": "0x0000000000000000000000000000000000000000",
}
resObj := sendRequestGood(t, evmrpc.NewFilterMethod, filterCriteria)
filterId := resObj["result"].(string)

resObj = sendRequest(t, TestPort, evmrpc.GetFilterLogsMethod, filterId)
_, hasErr := resObj["error"]
require.False(t, hasErr)
require.NotNil(t, resObj["result"], "getFilterLogs with no matching logs should return [] not null")
logs, ok := resObj["result"].([]interface{})
require.True(t, ok, "getFilterLogs result should be an array")
require.Empty(t, logs)
}
Loading