From 292b26b0f9fcecbabf554c7b8456e1e47ed0d2e4 Mon Sep 17 00:00:00 2001 From: konstantinabl Date: Mon, 26 Feb 2024 10:08:08 +0200 Subject: [PATCH] fix: Enables getStorageAt to accept blockHashes (#2123) * Enables getStorageAt to accept block hashes Signed-off-by: Konstantina Blazhukova * Adds tests for the new functionality Signed-off-by: Konstantina Blazhukova * Adds to openrpc Signed-off-by: Konstantina Blazhukova --------- Signed-off-by: Konstantina Blazhukova --- docs/openrpc.json | 19 +++++++- packages/relay/src/lib/eth.ts | 10 ++--- .../ethService/ethCommonService/index.ts | 18 ++++---- .../tests/lib/eth/eth_getStorageAt.spec.ts | 21 +++++++++ packages/server/src/validator/methods.ts | 2 +- .../tests/acceptance/rpc_batch2.spec.ts | 45 +++++++++++++++++++ .../server/tests/integration/server.spec.ts | 24 ++++++++-- 7 files changed, 120 insertions(+), 19 deletions(-) diff --git a/docs/openrpc.json b/docs/openrpc.json index d06fbfa0b3..05e6e264e2 100644 --- a/docs/openrpc.json +++ b/docs/openrpc.json @@ -439,7 +439,7 @@ "name": "Block", "required": false, "schema": { - "$ref": "#/components/schemas/BlockNumberOrTag" + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" } } ], @@ -1444,6 +1444,23 @@ } ] }, + "BlockNumberOrTagOrHash": { + "title": "Block number or tag", + "oneOf": [ + { + "title": "Block number", + "$ref": "#/components/schemas/uint" + }, + { + "title": "Block tag", + "$ref": "#/components/schemas/BlockTag" + }, + { + "title": "Block hash", + "$ref": "#/components/schemas/hash32" + } + ] + }, "TransactionWithSender": { "title": "Transaction object with sender", "type": "object", diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 576404f7ae..e138f457c8 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -793,20 +793,20 @@ export class EthImpl implements Eth { async getStorageAt( address: string, slot: string, - blockNumberOrTag?: string | null, + blockNumberOrTagOrHash?: string | null, requestIdPrefix?: string, ): Promise { this.logger.trace( - `${requestIdPrefix} getStorageAt(address=${address}, slot=${slot}, blockNumberOrTag=${blockNumberOrTag})`, + `${requestIdPrefix} getStorageAt(address=${address}, slot=${slot}, blockNumberOrOrHashTag=${blockNumberOrTagOrHash})`, ); let result = EthImpl.zeroHex32Byte; // if contract or slot not found then return 32 byte 0 - const blockResponse = await this.common.getHistoricalBlockResponse(blockNumberOrTag, false, requestIdPrefix); + const blockResponse = await this.common.getHistoricalBlockResponse(blockNumberOrTagOrHash, false, requestIdPrefix); // To save a request to the mirror node for `latest` and `pending` blocks, we directly return null from `getHistoricalBlockResponse` // But if a block number or `earliest` tag is passed and the mirror node returns `null`, we should throw an error. - if (!this.common.blockTagIsLatestOrPending(blockNumberOrTag) && blockResponse == null) { - throw predefined.RESOURCE_NOT_FOUND(`block '${blockNumberOrTag}'.`); + if (!this.common.blockTagIsLatestOrPending(blockNumberOrTagOrHash) && blockResponse == null) { + throw predefined.RESOURCE_NOT_FOUND(`block '${blockNumberOrTagOrHash}'.`); } const blockEndTimestamp = blockResponse?.timestamp?.to; diff --git a/packages/relay/src/lib/services/ethService/ethCommonService/index.ts b/packages/relay/src/lib/services/ethService/ethCommonService/index.ts index 8aee5a89db..b2f784c03e 100644 --- a/packages/relay/src/lib/services/ethService/ethCommonService/index.ts +++ b/packages/relay/src/lib/services/ethService/ethCommonService/index.ts @@ -149,16 +149,16 @@ export class CommonService implements ICommonService { * @param returnLatest */ public async getHistoricalBlockResponse( - blockNumberOrTag?: string | null, + blockNumberOrTagOrHash?: string | null, returnLatest?: boolean, requestIdPrefix?: string | undefined, ): Promise { - if (!returnLatest && this.blockTagIsLatestOrPending(blockNumberOrTag)) { + if (!returnLatest && this.blockTagIsLatestOrPending(blockNumberOrTagOrHash)) { return null; } - const blockNumber = Number(blockNumberOrTag); - if (blockNumberOrTag != null && blockNumberOrTag.length < 32 && !isNaN(blockNumber)) { + const blockNumber = Number(blockNumberOrTagOrHash); + if (blockNumberOrTagOrHash != null && blockNumberOrTagOrHash.length < 32 && !isNaN(blockNumber)) { const latestBlockResponse = await this.mirrorNodeClient.getLatestBlock(requestIdPrefix); const latestBlock = latestBlockResponse.blocks[0]; if (blockNumber > latestBlock.number + this.maxBlockRange) { @@ -166,20 +166,20 @@ export class CommonService implements ICommonService { } } - if (blockNumberOrTag == null || this.blockTagIsLatestOrPending(blockNumberOrTag)) { + if (blockNumberOrTagOrHash == null || this.blockTagIsLatestOrPending(blockNumberOrTagOrHash)) { const latestBlockResponse = await this.mirrorNodeClient.getLatestBlock(requestIdPrefix); return latestBlockResponse.blocks[0]; } - if (blockNumberOrTag == CommonService.blockEarliest) { + if (blockNumberOrTagOrHash == CommonService.blockEarliest) { return await this.mirrorNodeClient.getBlock(0, requestIdPrefix); } - if (blockNumberOrTag.length < 32) { - return await this.mirrorNodeClient.getBlock(Number(blockNumberOrTag), requestIdPrefix); + if (blockNumberOrTagOrHash.length < 32) { + return await this.mirrorNodeClient.getBlock(Number(blockNumberOrTagOrHash), requestIdPrefix); } - return await this.mirrorNodeClient.getBlock(blockNumberOrTag, requestIdPrefix); + return await this.mirrorNodeClient.getBlock(blockNumberOrTagOrHash, requestIdPrefix); } /** diff --git a/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts b/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts index 7f3fcd9ff4..22998dcfa5 100644 --- a/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getStorageAt.spec.ts @@ -37,6 +37,7 @@ import { DETAILD_CONTRACT_RESULT_NOT_FOUND, MOST_RECENT_BLOCK, OLDER_BLOCK, + BLOCK_HASH, } from './eth-config'; import { predefined } from '../../../src/lib/errors/JsonRpcError'; import RelayAssertions from '../../assertions'; @@ -72,6 +73,7 @@ describe('@ethGetStorageAt eth_getStorageAt spec', async function () { currentMaxBlockRange = Number(process.env.ETH_GET_TRANSACTION_COUNT_MAX_BLOCK_RANGE); process.env.ETH_GET_TRANSACTION_COUNT_MAX_BLOCK_RANGE = '1'; restMock.onGet(`blocks/${BLOCK_NUMBER}`).reply(200, DEFAULT_BLOCK); + restMock.onGet(`blocks/${BLOCK_HASH}`).reply(200, DEFAULT_BLOCK); restMock.onGet(BLOCKS_LIMIT_ORDER_URL).reply(200, MOST_RECENT_BLOCK); }); @@ -131,6 +133,25 @@ describe('@ethGetStorageAt eth_getStorageAt spec', async function () { expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written); }); + it('eth_getStorageAt with match with block hash', async function () { + // mirror node request mocks + restMock + .onGet( + `contracts/${CONTRACT_ADDRESS_1}/state?timestamp=${DEFAULT_BLOCK.timestamp.to}&slot=0x0000000000000000000000000000000000000000000000000000000000000101&limit=100&order=desc`, + ) + .reply(200, DEFAULT_CURRENT_CONTRACT_STATE); + + const result = await ethImpl.getStorageAt( + CONTRACT_ADDRESS_1, + defaultDetailedContractResults.state_changes[0].slot, + BLOCK_HASH, + ); + confirmResult(result); + + // verify slot value + expect(result).equal(defaultDetailedContractResults.state_changes[0].value_written); + }); + it('eth_getStorageAt with match with latest block', async function () { // mirror node request mocks restMock diff --git a/packages/server/src/validator/methods.ts b/packages/server/src/validator/methods.ts index 38e712b0df..89639dd839 100644 --- a/packages/server/src/validator/methods.ts +++ b/packages/server/src/validator/methods.ts @@ -127,7 +127,7 @@ export const METHODS = { type: 'hex', }, 2: { - type: 'blockNumber', + type: 'blockNumber|blockHash', }, }, eth_getTransactionByBlockHashAndIndex: { diff --git a/packages/server/tests/acceptance/rpc_batch2.spec.ts b/packages/server/tests/acceptance/rpc_batch2.spec.ts index a2aaae89c2..41c9119246 100644 --- a/packages/server/tests/acceptance/rpc_batch2.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch2.spec.ts @@ -920,6 +920,51 @@ describe('@api-batch-2 RPC Server Acceptance Tests', function () { ); expect(storageVal).to.eq(EXPECTED_STORAGE_VAL); }); + + it('should execute "eth_getStorageAt" request to get current state changes with passing specific block hash', async function () { + const EXPECTED_STORAGE_VAL = '0x0000000000000000000000000000000000000000000000000000000000000008'; + + const gasPrice = await relay.gasPrice(); + const transaction = { + value: 0, + gasLimit: 50000, + chainId: Number(CHAIN_ID), + to: evmAddress, + nonce: await relay.getAccountNonce(accounts[1].address), + gasPrice: gasPrice, + data: STORAGE_CONTRACT_UPDATE, + maxPriorityFeePerGas: gasPrice, + maxFeePerGas: gasPrice, + type: 2, + }; + + const signedTx = await accounts[1].wallet.signTransaction(transaction); + const transactionHash = await relay.sendRawTransaction(signedTx, requestId); + + const blockHash = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_TRANSACTION_RECEIPT, + [transactionHash], + requestId, + ).blockHash; + + const transaction1 = { + ...transaction, + nonce: await relay.getAccountNonce(accounts[1].address), + data: NEXT_STORAGE_CONTRACT_UPDATE, + }; + + const signedTx1 = await accounts[1].wallet.signTransaction(transaction1); + await relay.sendRawTransaction(signedTx1, requestId); + await new Promise((r) => setTimeout(r, 2000)); + + //Get previous state change with specific block number + const storageVal = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_STORAGE_AT, + [evmAddress, '0x0000000000000000000000000000000000000000000000000000000000000000', blockHash], + requestId, + ); + expect(storageVal).to.eq(EXPECTED_STORAGE_VAL); + }); }); // Only run the following tests against a local node since they only work with the genesis account diff --git a/packages/server/tests/integration/server.spec.ts b/packages/server/tests/integration/server.spec.ts index 98d994148d..3f5f3767cb 100644 --- a/packages/server/tests/integration/server.spec.ts +++ b/packages/server/tests/integration/server.spec.ts @@ -1830,7 +1830,7 @@ describe('RPC Server', async function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 2: ${Validator.BLOCK_NUMBER_ERROR}, value: 123`, + `Invalid parameter 2: ${Validator.INVALID_BLOCK_HASH_TAG_NUMBER} 123`, ); } }); @@ -1849,7 +1849,26 @@ describe('RPC Server', async function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 2: ${Validator.BLOCK_NUMBER_ERROR}, value: newest`, + `Invalid parameter 2: ${Validator.INVALID_BLOCK_HASH_TAG_NUMBER} newest`, + ); + } + }); + + it('validates parameter 2 is valid block tag', async function () { + try { + await this.testClient.post('/', { + id: '2', + jsonrpc: '2.0', + method: RelayCalls.ETH_ENDPOINTS.ETH_GET_STORAGE_AT, + params: ['0x0000000000000000000000000000000000000001', '0x1', 'newest'], + }); + + Assertions.expectedError(); + } catch (error) { + BaseTest.invalidParamError( + error.response, + Validator.ERROR_CODE, + `Invalid parameter 2: ${Validator.INVALID_BLOCK_HASH_TAG_NUMBER} newest`, ); } }); @@ -2045,7 +2064,6 @@ describe('RPC Server', async function () { Assertions.expectedError(); } catch (error) { - console.log(error.response.data); BaseTest.invalidParamError( error.response, Validator.ERROR_CODE,