diff --git a/api/api.go b/api/api.go index ddac3371..a80f6164 100644 --- a/api/api.go +++ b/api/api.go @@ -673,6 +673,16 @@ func (b *BlockChainAPI) GetLogs( // if filter provided specific block ID if criteria.BlockHash != nil { + // Check if the block exists, and return an error if not. + block, err := b.blocks.GetByID(*criteria.BlockHash) + if err != nil { + return nil, err + } + // If the block has no transactions, we can simply return an empty Logs array. + if len(block.TransactionHashes) == 0 { + return []*types.Log{}, nil + } + f, err := logs.NewIDFilter(*criteria.BlockHash, filter, b.blocks, b.receipts) if err != nil { return handleError[[]*types.Log](err, l, b.collector) @@ -687,7 +697,6 @@ func (b *BlockChainAPI) GetLogs( } // otherwise we use the block range as the filter - // assign default values to latest block number, unless provided from := models.LatestBlockNumber if criteria.FromBlock != nil { diff --git a/storage/index_test.go b/storage/index_test.go index 207873d9..fdf7f881 100644 --- a/storage/index_test.go +++ b/storage/index_test.go @@ -385,8 +385,8 @@ func (s *ReceiptTestSuite) TestGetReceiptByBlockHeight() { s.Run("non-existing block height", func() { retReceipt, err := s.ReceiptIndexer.GetByBlockHeight(1337) - s.Require().Nil(retReceipt) - s.Require().ErrorIs(err, errors.ErrEntityNotFound) + s.Require().NoError(err) + s.Require().Len(retReceipt, 0) }) } diff --git a/storage/pebble/receipts.go b/storage/pebble/receipts.go index 5a3ffec6..e1bc56fb 100644 --- a/storage/pebble/receipts.go +++ b/storage/pebble/receipts.go @@ -2,6 +2,7 @@ package pebble import ( "encoding/binary" + "errors" "fmt" "github.com/cockroachdb/pebble" @@ -113,9 +114,14 @@ func (r *Receipts) GetByBlockHeight(height uint64) ([]*models.Receipt, error) { } func (r *Receipts) getByBlockHeight(height []byte) ([]*models.Receipt, error) { - val, err := r.store.get(receiptHeightKey, height) if err != nil { + // For empty blocks, we do not store transactions & receipts. So when + // we encounter an `ErrEntityNotFound`, we should return an empty + // Receipts array, instead of an error. + if errors.Is(err, errs.ErrEntityNotFound) { + return []*models.Receipt{}, nil + } return nil, err } diff --git a/tests/e2e_web3js_test.go b/tests/e2e_web3js_test.go index 881c76ab..96ba5d9e 100644 --- a/tests/e2e_web3js_test.go +++ b/tests/e2e_web3js_test.go @@ -89,7 +89,23 @@ func TestWeb3_E2E(t *testing.T) { }) t.Run("logs emitting and filtering", func(t *testing.T) { - runWeb3Test(t, "eth_logs_filtering_test") + runWeb3TestWithSetup(t, "eth_logs_filtering_test", func(emu emulator.Emulator) { + // Run an arbitrary transaction, to form an empty EVM block + // through the system chunk transaction. This is needed + // to emulate the `eth_getLogs` by passing the block hash + // of a block without EVM transactions/receipts. + res, err := flowSendTransaction( + emu, + `transaction() { + prepare(signer: auth(Storage) &Account) { + let currentBlock = getCurrentBlock() + assert(currentBlock.height > 0, message: "current block is zero") + } + }`, + ) + require.NoError(t, err) + require.NoError(t, res.Error) + }) }) t.Run("test filter-related endpoints", func(t *testing.T) { diff --git a/tests/web3js/eth_logs_filtering_test.js b/tests/web3js/eth_logs_filtering_test.js index 7e96e3f4..672fec1f 100644 --- a/tests/web3js/eth_logs_filtering_test.js +++ b/tests/web3js/eth_logs_filtering_test.js @@ -1,11 +1,43 @@ const { assert } = require('chai') const conf = require('./config') const helpers = require('./helpers') +const web3 = conf.web3 it('emit logs and retrieve them using different filters', async () => { - setTimeout(() => process.exit(1), 19 * 1000) // hack if the ws connection is not closed + let latestBlockNumber = await web3.eth.getBlockNumber() + let latestBlock = await web3.eth.getBlock(latestBlockNumber) - let deployed = await helpers.deployContract("storage") + let blockHashFilter = { + blockHash: latestBlock.hash, + address: ['0x0000000071727de22e5e9d8baf0edac6f37da032'], + topics: ['0x2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c4'] + } + let response = await helpers.callRPCMethod('eth_getLogs', [blockHashFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual(response.body.result, []) + + blockHashFilter.blockHash = '0x048641726d25605a990c439b75fcfaa5f6b1691eaa718b72dd71e02a2264f5da' + response = await helpers.callRPCMethod('eth_getLogs', [blockHashFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body.error) + assert.equal( + response.body.error.message, + 'failed to get EVM block by ID: ' + blockHashFilter.blockHash + ', with: entity not found' + ) + + let blockRangeFilter = { + fromBlock: '0x1', + toBlock: 'latest', + address: ['0x0000000071727de22e5e9d8baf0edac6f37da032'], + topics: ['0x2da466a7b24304f47e87fa2e1e5a81b9831ce54fec19055ce277ca2f39ba42c4'] + } + response = await helpers.callRPCMethod('eth_getLogs', [blockRangeFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual(response.body.result, []) + + let deployed = await helpers.deployContract('storage') let contractAddress = deployed.receipt.contractAddress let repeatA = 10 @@ -81,4 +113,58 @@ it('emit logs and retrieve them using different filters', async () => { assert.lengthOf(events, 2) assert.equal(events[0].returnValues.numB, 2) assert.equal(events[1].returnValues.numB, -2) + + latestBlockNumber = await web3.eth.getBlockNumber() + latestBlock = await web3.eth.getBlock(latestBlockNumber) + + filterCriteria = { + blockHash: latestBlock.hash, + address: [contractAddress], + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ] + } + response = await helpers.callRPCMethod('eth_getLogs', [filterCriteria]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual( + response.body.result, + [ + { + address: '0x99a64c993965f8d69f985b5171bc20065cc32fab', + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ], + data: '0x000000000000000000000000000000000000000000000000000000000000019a', + blockNumber: '0xa', + transactionHash: '0x0c2b2477ab81c9132c5c4fd4f50935bc5807fbf4cf3bf3b69173491b68d2ca8b', + transactionIndex: '0x0', + blockHash: latestBlock.hash, + logIndex: '0x0', + removed: false + } + ] + ) + + blockRangeCriteria = { + fromBlock: web3.utils.numberToHex(latestBlock.number), + toBlock: web3.utils.numberToHex(latestBlock.number), + address: [contractAddress], + topics: [ + '0x76efea95e5da1fa661f235b2921ae1d89b99e457ec73fb88e34a1d150f95c64b', + '0x000000000000000000000000facf71692421039876a5bb4f10ef7a439d8ef61e', + '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000190' + ] + } + response = await helpers.callRPCMethod('eth_getLogs', [blockRangeFilter]) + assert.equal(response.status, 200) + assert.isDefined(response.body) + assert.deepEqual(response.body.result, []) })