diff --git a/packages/relay/src/lib/clients/mirrorNodeClient.ts b/packages/relay/src/lib/clients/mirrorNodeClient.ts index 66d68b243d..995f0305f0 100644 --- a/packages/relay/src/lib/clients/mirrorNodeClient.ts +++ b/packages/relay/src/lib/clients/mirrorNodeClient.ts @@ -153,7 +153,7 @@ export class MirrorNodeClient { return response.data; } catch (error: any) { ms = Date.now() - start; - const effectiveStatusCode = error.response !== undefined ? error.response.status : MirrorNodeClient.unknownServerErrorHttpStatusCode; + const effectiveStatusCode = error.response !== undefined ? error.response.status : MirrorNodeClient.unknownServerErrorHttpStatusCode; this.mirrorResponseHistogram.labels(pathLabel, effectiveStatusCode).observe(ms); this.handleError(error, path, effectiveStatusCode, allowedErrorStatuses, requestId); } @@ -346,10 +346,10 @@ export class MirrorNodeClient { public async getLatestContractResultsByAddress(address: string, blockEndTimestamp: string | undefined, limit: number) { // retrieve the timestamp of the contract - const contractResultsParams: IContractResultsParams = blockEndTimestamp - ? { timestamp: `lte:${blockEndTimestamp}` } + const contractResultsParams: IContractResultsParams = blockEndTimestamp + ? { timestamp: `lte:${blockEndTimestamp}` } : {}; - const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(limit, 'desc'); + const limitOrderParams: ILimitOrderParams = this.getLimitOrderQueryParam(limit, 'desc'); return this.getContractResultsByAddress(address, contractResultsParams, limitOrderParams); } diff --git a/packages/relay/src/lib/constants.ts b/packages/relay/src/lib/constants.ts index 8745c35123..a343eb0bd5 100644 --- a/packages/relay/src/lib/constants.ts +++ b/packages/relay/src/lib/constants.ts @@ -38,6 +38,7 @@ export default { DEFAULT_TINY_BAR_GAS: 72, // (853454 / 1000) * (1 / 12) ETH_FUNCTIONALITY_CODE: 84, + ETH_GET_LOGS_BLOCK_RANGE_LIMIT: Number(process.env.ETH_GET_LOGS_BLOCK_RANGE_LIMIT || 500), EXCHANGE_RATE_FILE_ID: "0.0.112", FEE_SCHEDULE_FILE_ID: '0.0.111', diff --git a/packages/relay/src/lib/errors/JsonRpcError.ts b/packages/relay/src/lib/errors/JsonRpcError.ts index b5593ae3a5..7a540fabb4 100644 --- a/packages/relay/src/lib/errors/JsonRpcError.ts +++ b/packages/relay/src/lib/errors/JsonRpcError.ts @@ -18,6 +18,8 @@ * */ +import constants from "../constants"; + const REQUEST_ID_STRING = `Request ID: `; export class JsonRpcError { public code: number; @@ -117,4 +119,9 @@ export const predefined = { code: -32001, message: 'Requested resource not found' }), + 'RANGE_TOO_LONG': new JsonRpcError({ + name: 'Block range too long', + code: -32000, + message: `Exceeded maximum block range: ${constants.ETH_GET_LOGS_BLOCK_RANGE_LIMIT}` + }) }; diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 693a098adc..c7538c5d53 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -483,7 +483,7 @@ export class EthImpl implements Eth { return result; } - + /** * Gets the balance of an account as of the given block. * @@ -704,11 +704,11 @@ export class EthImpl implements Eth { const result = await this.mirrorNodeClient.resolveEntityType(address, requestId); if (result?.type === constants.TYPE_ACCOUNT) { const accountInfo = await this.sdkClient.getAccountInfo(result?.entity.account, EthImpl.ethGetTransactionCount, requestId); - return EthImpl.numberTo0x(Number(accountInfo.ethereumNonce)); + return EthImpl.numberTo0x(Number(accountInfo.ethereumNonce)); } else if (result?.type === constants.TYPE_CONTRACT) { return EthImpl.numberTo0x(1); - } + } } catch (e: any) { this.logger.error(e, `${requestIdPrefix} Error raised during getTransactionCount for address ${address}, block number or tag ${blockNumOrTag}`); return predefined.INTERNAL_ERROR; @@ -792,7 +792,7 @@ export class EthImpl implements Eth { gas = call.gas; } } - + // Execute the call and get the response this.logger.debug(`${requestIdPrefix} Making eth_call on contract ${call.to} with gas ${gas} and call data "${call.data}" from "${call.from}"`, call.to, gas, call.data, call.from); const contractCallResponse = await this.sdkClient.submitContractCallQuery(call.to, call.data, gas, call.from, EthImpl.ethCall, requestId); @@ -1036,7 +1036,7 @@ export class EthImpl implements Eth { * @param blockNumberOrTag * @param returnLatest */ - private async getHistoricalBlockResponse(blockNumberOrTag?: string | null, returnLatest?: boolean): Promise { + private async getHistoricalBlockResponse(blockNumberOrTag?: string | null, returnLatest?: boolean, requestId?: string | undefined): Promise { let blockResponse: any; // Determine if the latest block should be returned and if not then just return null if (!returnLatest && @@ -1045,16 +1045,16 @@ export class EthImpl implements Eth { } if (blockNumberOrTag == null || blockNumberOrTag === EthImpl.blockLatest || blockNumberOrTag === EthImpl.blockPending) { - const blockPromise = this.mirrorNodeClient.getLatestBlock(); + const blockPromise = this.mirrorNodeClient.getLatestBlock(requestId); const blockAnswer = await blockPromise; blockResponse = blockAnswer.blocks[0]; } else if (blockNumberOrTag == EthImpl.blockEarliest) { - blockResponse = await this.mirrorNodeClient.getBlock(0); + blockResponse = await this.mirrorNodeClient.getBlock(0, requestId); } else if (blockNumberOrTag.length < 32) { // anything less than 32 characters is treated as a number - blockResponse = await this.mirrorNodeClient.getBlock(Number(blockNumberOrTag)); + blockResponse = await this.mirrorNodeClient.getBlock(Number(blockNumberOrTag), requestId); } else { - blockResponse = await this.mirrorNodeClient.getBlock(blockNumberOrTag); + blockResponse = await this.mirrorNodeClient.getBlock(blockNumberOrTag, requestId); } if (_.isNil(blockResponse) || blockResponse.hash === undefined) { // block not found. @@ -1146,28 +1146,59 @@ export class EthImpl implements Eth { throw e; } } else if (fromBlock || toBlock) { - const filters = []; - let order; + let fromBlockTimestamp; + let toBlockTimestamp; + let fromBlockNum = 0; + let toBlockNum; + if (toBlock) { - // @ts-ignore - filters.push(`lte:${parseInt(toBlock)}`); - order = constants.ORDER.DESC; + try { + const blockResponse = await this.getHistoricalBlockResponse(toBlock, true, requestId); + toBlockTimestamp = blockResponse.timestamp.to; + toBlockNum = parseInt(blockResponse.number); + } catch (e) { + // Check if block error is RESOURCE_NOT_FOUND, the most likely scenario this will happen if a block number > height is passed + // In this case eth_getLogs() returns logs up to the latest block. + // @ts-ignore + if (e.statusCode != 404) { + throw e; + } + } } if (fromBlock) { - // @ts-ignore - filters.push(`gte:${parseInt(fromBlock)}`); - order = constants.ORDER.ASC; + try { + const blockResponse = await this.getHistoricalBlockResponse(fromBlock, undefined, requestId); + fromBlockTimestamp = blockResponse.timestamp.from; + fromBlockNum = parseInt(blockResponse.number); + } catch (e) { + // Check if block error is RESOURCE_NOT_FOUND, the most likely scenario this will happen if a block number > height is passed + // In this case eth_getLogs() returns an empty array. + // @ts-ignore + if (e.statusCode != 404) { + throw e; + } + + return []; + } + } + + if(fromBlockTimestamp) { + params.timestamp = [`gte:${fromBlockTimestamp}`]; + } + + if(toBlockTimestamp){ + params.timestamp + ? params.timestamp.push(`lte:${toBlockTimestamp}`) + : params.timestamp = [`lte:${toBlockTimestamp}`]; + } else { + const blockResponse = await this.getHistoricalBlockResponse('latest', true, requestId); + toBlockNum = parseInt(blockResponse.number); } - const blocksResult = await this.mirrorNodeClient.getBlocks(filters, undefined, {order}, requestId); - - const blocks = blocksResult?.blocks; - if (blocks?.length) { - const firstBlock = (order == constants.ORDER.DESC) ? blocks[blocks.length - 1] : blocks[0]; - const lastBlock = (order == constants.ORDER.DESC) ? blocks[0] : blocks[blocks.length - 1]; - params.timestamp = [ - `gte:${firstBlock.timestamp.from}`, - `lte:${lastBlock.timestamp.to}` - ]; + + if(fromBlockNum > toBlockNum) { + return []; + } else if((toBlockNum - fromBlockNum) > constants.ETH_GET_LOGS_BLOCK_RANGE_LIMIT) { + throw predefined.RANGE_TOO_LONG; } } diff --git a/packages/relay/tests/lib/eth.spec.ts b/packages/relay/tests/lib/eth.spec.ts index 7d6ea58aff..fd8b28ad7a 100644 --- a/packages/relay/tests/lib/eth.spec.ts +++ b/packages/relay/tests/lib/eth.spec.ts @@ -1014,7 +1014,6 @@ describe('Eth calls using MirrorNode', async function () { }); describe('eth_getLogs', async function () { - const expectLogData = (res, log, tx, blockLogIndexOffset) => { expect(res.address).to.eq(log.address); expect(res.blockHash).to.eq(EthImpl.toHash32(tx.block_hash)); @@ -1158,15 +1157,23 @@ describe('Eth calls using MirrorNode', async function () { expectLogData2(result[1]); }); - it('fromBlock && toBlock filter', async function () { + it('with valid fromBlock && toBlock filter', async function () { const filteredLogs = { logs: [defaultLogs.logs[0], defaultLogs.logs[1]] }; + const toBlock = { + ...defaultBlock, + number: '0x10', + 'timestamp': { + 'from': `1651560391.060890949`, + 'to': '1651560393.060890949' + }, + }; + mock.onGet(`contracts/${contractId1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults); - mock.onGet(`contracts/results/logs?timestamp=gte:${defaultBlock.timestamp.from}×tamp=lte:${defaultBlock.timestamp.to}`).reply(200, filteredLogs); - mock.onGet('blocks?block.number=lte:16&block.number=gte:5&order=asc').reply(200, { - blocks: [defaultBlock] - }); + mock.onGet(`contracts/results/logs?timestamp=gte:${defaultBlock.timestamp.from}×tamp=lte:${toBlock.timestamp.to}`).reply(200, filteredLogs); + mock.onGet('blocks/5').reply(200, defaultBlock); + mock.onGet('blocks/16').reply(200, toBlock); const result = await ethImpl.getLogs(null, '0x5', '0x10', null, null); expect(result).to.exist; @@ -1174,6 +1181,67 @@ describe('Eth calls using MirrorNode', async function () { expectLogData2(result[1]); }); + it('with non-existing fromBlock filter', async function () { + mock.onGet('blocks/5').reply(200, defaultBlock); + mock.onGet('blocks/16').reply(404, {"_status": { "messages": [{"message": "Not found"}]}}); + const result = await ethImpl.getLogs(null, '0x10', '0x5', null, null); + + expect(result).to.exist; + expect(result).to.be.empty; + }); + + it('when fromBlock > toBlock', async function () { + const fromBlock = { + ...defaultBlock, + number: '0x10', + 'timestamp': { + 'from': `1651560391.060890949`, + 'to': '1651560393.060890949' + }, + }; + + mock.onGet('blocks/16').reply(200, fromBlock); + mock.onGet('blocks/5').reply(200, defaultBlock); + const result = await ethImpl.getLogs(null, '0x10', '0x5', null, null); + + expect(result).to.exist; + expect(result).to.be.empty; + }); + + it('with block tag', async function () { + const filteredLogs = { + logs: [defaultLogs.logs[0]] + }; + + mock.onGet(`contracts/${contractId1}/results/${contractTimestamp1}`).reply(200, defaultDetailedContractResults); + mock.onGet(`contracts/results/logs?timestamp=lte:${defaultBlock.timestamp.to}`).reply(200, filteredLogs); + mock.onGet('blocks?limit=1&order=desc').reply(200, { blocks: [defaultBlock] }); + const result = await ethImpl.getLogs(null, null, 'latest', null, null); + + expect(result).to.exist; + expectLogData1(result[0]); + }); + + it('when block range is too large', async function () { + const fromBlock = { + ...defaultBlock, + number: '0x1' + }; + const toBlock = { + ...defaultBlock, + number: '0x1f6' + }; + mock.onGet('blocks/1').reply(200, fromBlock); + mock.onGet('blocks/502').reply(200, toBlock); + + try { + await ethImpl.getLogs(null, '0x1', '0x1f6', null, null); + } catch (error: any) { + expect(error.message).to.equal('Exceeded maximum block range: 500'); + } + }); + + it('topics filter', async function () { const filteredLogs = { logs: [defaultLogs.logs[0], defaultLogs.logs[1]] @@ -1184,9 +1252,8 @@ describe('Eth calls using MirrorNode', async function () { `?topic0=${defaultLogTopics[0]}&topic1=${defaultLogTopics[1]}` + `&topic2=${defaultLogTopics[2]}&topic3=${defaultLogTopics[3]}` ).reply(200, filteredLogs); - mock.onGet('blocks?block.number=gte:0x5&block.number=lte:0x10').reply(200, { - blocks: [defaultBlock] - }); + mock.onGet('blocks/5').reply(200, defaultBlock); + mock.onGet('blocks/16').reply(200, defaultBlock); const result = await ethImpl.getLogs(null, null, null, null, defaultLogTopics); @@ -1204,7 +1271,7 @@ describe('Eth calls using MirrorNode', async function () { const latestBlock = {...defaultBlock, number: blockNumber3}; const previousFees = JSON.parse(JSON.stringify(defaultNetworkFees)); const latestFees = JSON.parse(JSON.stringify(defaultNetworkFees)); - + previousFees.fees[2].gas += 1; mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]}); @@ -1247,7 +1314,7 @@ describe('Eth calls using MirrorNode', async function () { it('eth_feeHistory verify cached value', async function () { const latestBlock = {...defaultBlock, number: blockNumber3}; const latestFees = defaultNetworkFees; - + mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]}); mock.onGet(`blocks/${latestBlock.number}`).reply(200, latestBlock); mock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(200, latestFees); @@ -1265,7 +1332,7 @@ describe('Eth calls using MirrorNode', async function () { it('eth_feeHistory on mirror 404', async function () { const latestBlock = {...defaultBlock, number: blockNumber3}; - + mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]}); mock.onGet(`blocks/${latestBlock.number}`).reply(200, latestBlock); mock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(404, { @@ -1291,7 +1358,7 @@ describe('Eth calls using MirrorNode', async function () { it('eth_feeHistory on mirror 500', async function () { const latestBlock = {...defaultBlock, number: blockNumber3}; - + mock.onGet('blocks?limit=1&order=desc').reply(200, {blocks: [latestBlock]}); mock.onGet(`blocks/${latestBlock.number}`).reply(200, latestBlock); mock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(404, { @@ -1431,7 +1498,7 @@ describe('Eth calls using MirrorNode', async function () { } ); - var result = await ethImpl.call({ + const result = await ethImpl.call({ "from": contractAddress1, "to": contractAddress2, "gas": maxGasLimitHex @@ -1507,7 +1574,7 @@ describe('Eth calls using MirrorNode', async function () { const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber)); expect(result).to.exist; if (result == null) return; - + // verify slot value expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written); }); @@ -1520,7 +1587,7 @@ describe('Eth calls using MirrorNode', async function () { const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, "latest"); expect(result).to.exist; if (result == null) return; - + // verify slot value expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written); }); @@ -1533,7 +1600,7 @@ describe('Eth calls using MirrorNode', async function () { const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot); expect(result).to.exist; if (result == null) return; - + // verify slot value expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written); }); @@ -1543,7 +1610,7 @@ describe('Eth calls using MirrorNode', async function () { let hasError = false; try { mock.onGet(`blocks/${blockNumber}`).reply(200, null); - const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber)); + const result = await ethImpl.getStorageAt(contractAddress1, defaultDetailedContractResults.state_changes[0].slot, EthImpl.numberTo0x(blockNumber)); } catch (e: any) { hasError = true; expect(e.code).to.equal(-32001); @@ -1565,7 +1632,7 @@ describe('Eth calls using MirrorNode', async function () { hasError = true; expect(e.statusCode).to.equal(404); expect(e.message).to.equal("Request failed with status code 404"); - } + } expect(hasError).to.be.true; }); });