From a8fdd86149c790afa0a77fa2e7b44106b0ae99e0 Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Thu, 5 Dec 2024 14:25:02 +0200 Subject: [PATCH 1/7] chore: add precheck for receivers account in sendRawTransactionCheck Signed-off-by: Nadezhda Popova --- packages/relay/src/lib/precheck.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index 98e247456..196829d13 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -20,7 +20,6 @@ import { ethers, Transaction } from 'ethers'; import { Logger } from 'pino'; - import { prepend0x } from '../formatters'; import { MirrorNodeClient } from './clients'; import constants from './constants'; @@ -85,6 +84,9 @@ export class Precheck { this.value(parsedTx); this.gasPrice(parsedTx, networkGasPriceInWeiBars, requestDetails); this.balance(parsedTx, mirrorAccountInfo, requestDetails); + if (parsedTx.to) { + await this.receiverAccount(parsedTx, requestDetails); + } } /** @@ -376,4 +378,17 @@ export class Precheck { throw predefined.UNSUPPORTED_TRANSACTION_TYPE; } } + + /** + * Checks if the receiver account exists. + * @param {Transaction} tx - The transaction. + * @param {RequestDetails} requestDetails - The request details for logging and tracking. + */ + async receiverAccount(tx: Transaction, requestDetails: RequestDetails) { + const verifyAccount = await this.mirrorNodeClient.getAccount(tx.to!, requestDetails); + + if (verifyAccount !== null && verifyAccount.receiver_sig_required === true) { + throw predefined.INTERNAL_ERROR("Receiver's signature is required."); + } + } } From a8922478a10e93308f6942c5c87f984e61b23116 Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Thu, 5 Dec 2024 16:55:06 +0200 Subject: [PATCH 2/7] fixup! chore: add precheck for receivers account in sendRawTransactionCheck Signed-off-by: Nadezhda Popova --- .../relay/tests/lib/eth/eth_sendRawTransaction.spec.ts | 9 +++++++++ packages/relay/tests/lib/openrpc.spec.ts | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts b/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts index 7f87e7357..dad1335b3 100644 --- a/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts +++ b/packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts @@ -99,6 +99,7 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function () let clock: any; const accountAddress = '0x9eaee9E66efdb91bfDcF516b034e001cc535EB57'; const accountEndpoint = `accounts/${accountAddress}${NO_TRANSACTIONS}`; + const receiverAccountEndpoint = `accounts/${ACCOUNT_ADDRESS_1}${NO_TRANSACTIONS}`; const gasPrice = '0xad78ebc5ac620000'; const transactionIdServicesFormat = '0.0.902@1684375868.230217103'; const transactionId = '0.0.902-1684375868-230217103'; @@ -127,6 +128,13 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function () balance: Hbar.from(100_000_000_000, HbarUnit.Hbar).to(HbarUnit.Tinybar), }, }; + const RECEIVER_ACCOUNT_RES = { + account: ACCOUNT_ADDRESS_1, + balance: { + balance: Hbar.from(100_000_000_000, HbarUnit.Hbar).to(HbarUnit.Tinybar), + }, + receiver_sig_required: false, + }; const useAsyncTxProcessing = ConfigService.get('USE_ASYNC_TX_PROCESSING') as boolean; beforeEach(() => { @@ -135,6 +143,7 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function () sdkClientStub = sinon.createStubInstance(SDKClient); sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub); restMock.onGet(accountEndpoint).reply(200, ACCOUNT_RES); + restMock.onGet(receiverAccountEndpoint).reply(200, RECEIVER_ACCOUNT_RES); restMock.onGet(networkExchangeRateEndpoint).reply(200, mockedExchangeRate); }); diff --git a/packages/relay/tests/lib/openrpc.spec.ts b/packages/relay/tests/lib/openrpc.spec.ts index fd832c48d..3c1a1b3ae 100644 --- a/packages/relay/tests/lib/openrpc.spec.ts +++ b/packages/relay/tests/lib/openrpc.spec.ts @@ -75,7 +75,7 @@ import { overrideEnvsInMochaDescribe, signedTransactionHash, } from '../helpers'; -import { NOT_FOUND_RES } from './eth/eth-config'; +import { CONTRACT_RESULT_MOCK, NOT_FOUND_RES } from './eth/eth-config'; const logger = pino(); const registry = new Registry(); @@ -229,6 +229,7 @@ describe('Open RPC Specification', function () { mock.onGet(`accounts/${defaultContractResults.results[1].from}?transactions=false`).reply(200); mock.onGet(`accounts/${defaultContractResults.results[0].to}?transactions=false`).reply(200); mock.onGet(`accounts/${defaultContractResults.results[1].to}?transactions=false`).reply(200); + mock.onGet(`accounts/${CONTRACT_RESULT_MOCK.from}?transactions=false`).reply(200, CONTRACT_RESULT_MOCK); mock.onGet(`contracts/${defaultContractResults.results[0].from}`).reply(404, NOT_FOUND_RES); mock.onGet(`contracts/${defaultContractResults.results[1].from}`).reply(404, NOT_FOUND_RES); mock.onGet(`contracts/${defaultContractResults.results[0].to}`).reply(200); From 4604f1fca271b0ec3d6670ea61f98dc0ada3bc13 Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Wed, 11 Dec 2024 16:31:37 +0200 Subject: [PATCH 3/7] fixup! fixup! chore: add precheck for receivers account in sendRawTransactionCheck Signed-off-by: Nadezhda Popova --- packages/relay/src/lib/precheck.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index 196829d13..a8acf1273 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -84,9 +84,7 @@ export class Precheck { this.value(parsedTx); this.gasPrice(parsedTx, networkGasPriceInWeiBars, requestDetails); this.balance(parsedTx, mirrorAccountInfo, requestDetails); - if (parsedTx.to) { - await this.receiverAccount(parsedTx, requestDetails); - } + await this.receiverAccount(parsedTx, requestDetails); } /** @@ -380,15 +378,18 @@ export class Precheck { } /** - * Checks if the receiver account exists. + * Checks if the receiver account exists and has receiver_sig_required set to true. * @param {Transaction} tx - The transaction. * @param {RequestDetails} requestDetails - The request details for logging and tracking. */ async receiverAccount(tx: Transaction, requestDetails: RequestDetails) { - const verifyAccount = await this.mirrorNodeClient.getAccount(tx.to!, requestDetails); + if (tx.to) { + const verifyAccount = await this.mirrorNodeClient.getAccount(tx.to!, requestDetails); - if (verifyAccount !== null && verifyAccount.receiver_sig_required === true) { - throw predefined.INTERNAL_ERROR("Receiver's signature is required."); + // When `receiver_sig_required` is set to true, the receiver's account must sign all incoming transactions. + if (verifyAccount !== null && verifyAccount.receiver_sig_required === true) { + throw predefined.RECEIVER_SIGNATURE_REQUIRED; + } } } } From 3b85b1a63218fd1ca6ae3324df442d6f3fa6702d Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Wed, 11 Dec 2024 16:33:14 +0200 Subject: [PATCH 4/7] test: add test for receivers account precheck Signed-off-by: Nadezhda Popova --- packages/relay/src/lib/errors/JsonRpcError.ts | 4 ++ .../tests/acceptance/rpc_batch1.spec.ts | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/relay/src/lib/errors/JsonRpcError.ts b/packages/relay/src/lib/errors/JsonRpcError.ts index bd559a86f..d500a6c84 100644 --- a/packages/relay/src/lib/errors/JsonRpcError.ts +++ b/packages/relay/src/lib/errors/JsonRpcError.ts @@ -238,6 +238,10 @@ export const predefined = { code: -39013, message: 'Invalid block range', }), + RECEIVER_SIGNATURE_REQUIRED: new JsonRpcError({ + code: -32000, + message: "Receiver's signature is required.", + }), FILTER_NOT_FOUND: new JsonRpcError({ code: -32001, message: 'Filter not found', diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 7fd20a5c1..354eb8402 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -26,7 +26,14 @@ import Constants from '@hashgraph/json-rpc-relay/dist/lib/constants'; // Errors and constants from local resources import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError'; import { RequestDetails } from '@hashgraph/json-rpc-relay/dist/lib/types'; -import { FileInfo, FileInfoQuery, Hbar, TransferTransaction } from '@hashgraph/sdk'; +import { + AccountCreateTransaction, + FileInfo, + FileInfoQuery, + Hbar, + PrivateKey, + TransferTransaction, +} from '@hashgraph/sdk'; import { expect } from 'chai'; import { ethers } from 'ethers'; @@ -1586,6 +1593,46 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { const error = predefined.NONCE_TOO_LOW(nonce, nonce + 1); await Assertions.assertPredefinedRpcError(error, sendRawTransaction, true, relay, [signedTx, requestDetails]); }); + + it('should fail "eth_sendRawTransaction" if receiver\'s account has receiver_sig_required enabled', async function () { + const newPrivateKey = PrivateKey.generateED25519(); + const newAccount = await new AccountCreateTransaction() + .setKey(newPrivateKey.publicKey) + .setInitialBalance(100) + .setReceiverSignatureRequired(true) + .freezeWith(servicesNode.client) + .sign(newPrivateKey); + + const transaction = await newAccount.execute(servicesNode.client); + const receipt = await transaction.getReceipt(servicesNode.client); + + if (!receipt.accountId) { + throw new Error('Failed to create new account - accountId is null'); + } + + const toAddress = Utils.idToEvmAddress(receipt.accountId.toString()); + const tx = { + nonce: await accounts[0].wallet.getNonce(), + chainId: CHAIN_ID, + to: toAddress, + from: accounts[0].address, + value: '0x2E90EDD000', + gasLimit: defaultGasLimit, + accessList: [], + maxPriorityFeePerGas: defaultGasPrice, + maxFeePerGas: defaultGasPrice, + }; + + const signedTx = await accounts[0].wallet.signTransaction(tx); + await new Promise((r) => setTimeout(r, 3000)); + + const error = predefined.RECEIVER_SIGNATURE_REQUIRED; + + await Assertions.assertPredefinedRpcError(error, sendRawTransaction, false, relay, [ + signedTx, + requestDetails, + ]); + }); }); it('@release should execute "eth_getTransactionByHash" for existing transaction', async function () { From 62b7885bd3aa476e48f6ecacc4ba9b5b9e151e9f Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Thu, 12 Dec 2024 19:16:47 +0200 Subject: [PATCH 5/7] fixup! test: add test for receivers account precheck Signed-off-by: Nadezhda Popova --- packages/relay/src/lib/precheck.ts | 3 +- .../tests/acceptance/rpc_batch1.spec.ts | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index a8acf1273..d4e7701dc 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -20,6 +20,7 @@ import { ethers, Transaction } from 'ethers'; import { Logger } from 'pino'; + import { prepend0x } from '../formatters'; import { MirrorNodeClient } from './clients'; import constants from './constants'; @@ -384,7 +385,7 @@ export class Precheck { */ async receiverAccount(tx: Transaction, requestDetails: RequestDetails) { if (tx.to) { - const verifyAccount = await this.mirrorNodeClient.getAccount(tx.to!, requestDetails); + const verifyAccount = await this.mirrorNodeClient.getAccount(tx.to, requestDetails); // When `receiver_sig_required` is set to true, the receiver's account must sign all incoming transactions. if (verifyAccount !== null && verifyAccount.receiver_sig_required === true) { diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 354eb8402..0309aa1fc 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -1612,15 +1612,11 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { const toAddress = Utils.idToEvmAddress(receipt.accountId.toString()); const tx = { + ...defaultLegacyTransactionData, + chainId: Number(CHAIN_ID), nonce: await accounts[0].wallet.getNonce(), - chainId: CHAIN_ID, to: toAddress, from: accounts[0].address, - value: '0x2E90EDD000', - gasLimit: defaultGasLimit, - accessList: [], - maxPriorityFeePerGas: defaultGasPrice, - maxFeePerGas: defaultGasPrice, }; const signedTx = await accounts[0].wallet.signTransaction(tx); @@ -1633,6 +1629,39 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { requestDetails, ]); }); + + it('should execute "eth_sendRawTransaction" if receiver\'s account has receiver_sig_required disabled', async function () { + const newPrivateKey = PrivateKey.generateED25519(); + const newAccount = await new AccountCreateTransaction() + .setKey(newPrivateKey.publicKey) + .setInitialBalance(100) + .setReceiverSignatureRequired(false) + .freezeWith(servicesNode.client) + .sign(newPrivateKey); + + const transaction = await newAccount.execute(servicesNode.client); + const receipt = await transaction.getReceipt(servicesNode.client); + + if (!receipt.accountId) { + throw new Error('Failed to create new account - accountId is null'); + } + + const toAddress = Utils.idToEvmAddress(receipt.accountId.toString()); + const tx = { + ...defaultLegacyTransactionData, + chainId: Number(CHAIN_ID), + nonce: await accounts[0].wallet.getNonce(), + to: toAddress, + from: accounts[0].address, + }; + + const signedTx = await accounts[0].wallet.signTransaction(tx); + const transactionHash = await relay.sendRawTransaction(signedTx, requestId); + const info = await mirrorNode.get(`/contracts/results/${transactionHash}`, requestId); + + expect(info).to.exist; + expect(info.result).to.equal('SUCCESS'); + }); }); it('@release should execute "eth_getTransactionByHash" for existing transaction', async function () { From 12cdbe76b484b00dd82aeaea00ffb9ec179a1d8e Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Thu, 12 Dec 2024 20:30:14 +0200 Subject: [PATCH 6/7] fixup! fixup! test: add test for receivers account precheck Signed-off-by: Nadezhda Popova --- packages/relay/tests/lib/precheck.spec.ts | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/relay/tests/lib/precheck.spec.ts b/packages/relay/tests/lib/precheck.spec.ts index c793c3911..5a4a3cf16 100644 --- a/packages/relay/tests/lib/precheck.spec.ts +++ b/packages/relay/tests/lib/precheck.spec.ts @@ -654,4 +654,50 @@ describe('Precheck', async function () { expect(error.code).to.equal(predefined.UNSUPPORTED_TRANSACTION_TYPE.code); }); }); + + describe('receiverAccount', async function () { + let parsedTx: Transaction; + let mirrorAccountTo: any; + const defaultNonce: number = 4; + const toAddress = ethers.Wallet.createRandom().address; + + before(async () => { + const wallet = ethers.Wallet.createRandom(); + const signed = await wallet.signTransaction({ + ...defaultTx, + from: wallet.address, + to: toAddress, + nonce: defaultNonce, + }); + + parsedTx = ethers.Transaction.from(signed); + }); + + it('should fail with signature required error', async function () { + mirrorAccountTo = { + receiver_sig_required: true, + }; + + mock.onGet(`accounts/${parsedTx.to}${transactionsPostFix}`).reply(200, mirrorAccountTo); + + try { + await precheck.receiverAccount(parsedTx, requestDetails); + expectedError(); + } catch (e: any) { + expect(e).to.exist; + expect(e.code).to.eq(-32000); + expect(e).to.eql(predefined.RECEIVER_SIGNATURE_REQUIRED); + } + }); + + it('should accept check if signature required is set to false', async function () { + mirrorAccountTo = { + receiver_sig_required: false, + }; + + mock.onGet(`accounts/${parsedTx.to}${transactionsPostFix}`).reply(200, mirrorAccountTo); + + expect(async () => await precheck.receiverAccount(parsedTx, requestDetails)).not.to.throw; + }); + }); }); From 41102a08080a6c92f08d50d765f97919cbd91292 Mon Sep 17 00:00:00 2001 From: Nadezhda Popova Date: Thu, 12 Dec 2024 21:27:42 +0200 Subject: [PATCH 7/7] fixup! fixup! fixup! test: add test for receivers account precheck Signed-off-by: Nadezhda Popova --- packages/relay/src/lib/errors/JsonRpcError.ts | 4 ++-- packages/relay/src/lib/precheck.ts | 2 +- packages/relay/tests/lib/precheck.spec.ts | 2 +- packages/server/tests/acceptance/rpc_batch1.spec.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/relay/src/lib/errors/JsonRpcError.ts b/packages/relay/src/lib/errors/JsonRpcError.ts index d500a6c84..7e17207b5 100644 --- a/packages/relay/src/lib/errors/JsonRpcError.ts +++ b/packages/relay/src/lib/errors/JsonRpcError.ts @@ -238,9 +238,9 @@ export const predefined = { code: -39013, message: 'Invalid block range', }), - RECEIVER_SIGNATURE_REQUIRED: new JsonRpcError({ + RECEIVER_SIGNATURE_ENABLED: new JsonRpcError({ code: -32000, - message: "Receiver's signature is required.", + message: "Receiver's signature is enabled.", }), FILTER_NOT_FOUND: new JsonRpcError({ code: -32001, diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index d4e7701dc..f0c313b15 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -389,7 +389,7 @@ export class Precheck { // When `receiver_sig_required` is set to true, the receiver's account must sign all incoming transactions. if (verifyAccount !== null && verifyAccount.receiver_sig_required === true) { - throw predefined.RECEIVER_SIGNATURE_REQUIRED; + throw predefined.RECEIVER_SIGNATURE_ENABLED; } } } diff --git a/packages/relay/tests/lib/precheck.spec.ts b/packages/relay/tests/lib/precheck.spec.ts index 5a4a3cf16..46a70bc3a 100644 --- a/packages/relay/tests/lib/precheck.spec.ts +++ b/packages/relay/tests/lib/precheck.spec.ts @@ -686,7 +686,7 @@ describe('Precheck', async function () { } catch (e: any) { expect(e).to.exist; expect(e.code).to.eq(-32000); - expect(e).to.eql(predefined.RECEIVER_SIGNATURE_REQUIRED); + expect(e).to.eql(predefined.RECEIVER_SIGNATURE_ENABLED); } }); diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 0309aa1fc..7ae01cbe5 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -1622,7 +1622,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { const signedTx = await accounts[0].wallet.signTransaction(tx); await new Promise((r) => setTimeout(r, 3000)); - const error = predefined.RECEIVER_SIGNATURE_REQUIRED; + const error = predefined.RECEIVER_SIGNATURE_ENABLED; await Assertions.assertPredefinedRpcError(error, sendRawTransaction, false, relay, [ signedTx,