diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 8fca29353a..e70603abe8 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -142,6 +142,7 @@ export class EthImpl implements Eth { ? new RedisPendingTransactionStorage(this.redisClient) : new LocalPendingTransactionStorage(); const transactionPoolService = new TransactionPoolService(storage, logger); + transactionPoolService.resetState(); this.transactionService = new TransactionService( cacheService, chain, diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index 9224808178..e97999bc87 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -7,7 +7,7 @@ import { Logger } from 'pino'; import { prepend0x } from '../formatters'; import { MirrorNodeClient } from './clients'; import constants from './constants'; -import { JsonRpcError, predefined } from './errors/JsonRpcError'; +import { predefined } from './errors/JsonRpcError'; import { CommonService, TransactionPoolService } from './services'; import { RequestDetails } from './types'; import { IAccountBalance } from './types/mirrorNode'; @@ -80,9 +80,9 @@ export class Precheck { this.gasLimit(parsedTx); const mirrorAccountInfo = await this.verifyAccount(parsedTx, requestDetails); const signerNonce = - mirrorAccountInfo.ethereum_nonce + ConfigService.get('ENABLE_TX_POOL') + mirrorAccountInfo.ethereum_nonce + (ConfigService.get('ENABLE_TX_POOL') ? await this.transactionPoolService.getPendingCount(parsedTx.from!) - : 0; + : 0); this.nonce(parsedTx, signerNonce); this.chainId(parsedTx); this.value(parsedTx); diff --git a/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts b/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts index 9fd5536d82..07846b5d82 100644 --- a/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts +++ b/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts @@ -257,6 +257,11 @@ export class TransactionService implements ITransactionService { await this.validateRawTransaction(parsedTx, networkGasPriceInWeiBars, requestDetails); + // Save the transaction to the transaction pool before submitting it to the network + if (ConfigService.get('ENABLE_TX_POOL')) { + await this.transactionPoolService.saveTransaction(parsedTx.from!, parsedTx); + } + /** * Note: If the USE_ASYNC_TX_PROCESSING feature flag is enabled, * the transaction hash is calculated and returned immediately after passing all prechecks. @@ -491,11 +496,6 @@ export class TransactionService implements ITransactionService { const originalCallerAddress = parsedTx.from?.toString() || ''; - // Save the transaction to the transaction pool before submitting it to the network - if (ConfigService.get('ENABLE_TX_POOL')) { - await this.transactionPoolService.saveTransaction(originalCallerAddress, parsedTx); - } - this.eventEmitter.emit('eth_execution', { method: constants.ETH_SEND_RAW_TRANSACTION, }); @@ -507,6 +507,11 @@ export class TransactionService implements ITransactionService { requestDetails, ); + // Remove the transaction from the transaction pool after successful submission + if (ConfigService.get('ENABLE_TX_POOL')) { + await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.hash!); + } + sendRawTransactionError = error; // After the try-catch process above, the `submittedTransactionId` is potentially valid in only two scenarios: @@ -551,21 +556,12 @@ export class TransactionService implements ITransactionService { ); } - // Remove the transaction from the transaction pool after successful submission - if (ConfigService.get('ENABLE_TX_POOL')) { - await this.transactionPoolService.removeTransaction(originalCallerAddress, contractResult.hash); - } return contractResult.hash; } catch (e: any) { sendRawTransactionError = e; } } - // Remove the transaction from the transaction pool after unsuccessful submission - if (ConfigService.get('ENABLE_TX_POOL')) { - await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.hash!); - } - // If this point is reached, it means that no valid transaction hash was returned. Therefore, an error must have occurred. return await this.sendRawTransactionErrorHandler( sendRawTransactionError, diff --git a/packages/relay/src/lib/services/transactionPoolService/transactionPoolService.ts b/packages/relay/src/lib/services/transactionPoolService/transactionPoolService.ts index 768c124637..43bb715999 100644 --- a/packages/relay/src/lib/services/transactionPoolService/transactionPoolService.ts +++ b/packages/relay/src/lib/services/transactionPoolService/transactionPoolService.ts @@ -3,7 +3,6 @@ import { Transaction } from 'ethers'; import { Logger } from 'pino'; -import { IExecuteTransactionEventPayload } from '../../types/events'; import { PendingTransactionStorage, TransactionPoolService as ITransactionPoolService, diff --git a/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts b/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts index 1f29278630..58e96b7268 100644 --- a/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts +++ b/packages/relay/tests/lib/repositories/hbarLimiter/ipAddressHbarSpendingPlanRepository.spec.ts @@ -5,7 +5,6 @@ import chaiAsPromised from 'chai-as-promised'; import { randomBytes, uuidV4 } from 'ethers'; import pino from 'pino'; import { Registry } from 'prom-client'; -import { RedisClientType } from 'redis'; import sinon from 'sinon'; import { RedisClientManager } from '../../../../src/lib/clients/redisClientManager'; @@ -30,7 +29,6 @@ describe('IPAddressHbarSpendingPlanRepository', function () { let cacheServiceSpy: sinon.SinonSpiedInstance; let repository: IPAddressHbarSpendingPlanRepository; let redisClientManager: RedisClientManager; - let redisClient: RedisClientType | undefined; if (isSharedCacheEnabled) { useInMemoryRedisServer(logger, 6383); @@ -41,9 +39,6 @@ describe('IPAddressHbarSpendingPlanRepository', function () { if (isSharedCacheEnabled) { redisClientManager = new RedisClientManager(logger, 'redis://127.0.0.1:6383', 1000); await redisClientManager.connect(); - redisClient = redisClientManager.getClient(); - } else { - redisClient = undefined; } cacheService = new CacheService(logger, registry); cacheServiceSpy = sinon.spy(cacheService); diff --git a/packages/relay/tests/lib/services/transactionPoolService/transactionPoolService.spec.ts b/packages/relay/tests/lib/services/transactionPoolService/transactionPoolService.spec.ts index 4caa99ba94..6ef3281c95 100644 --- a/packages/relay/tests/lib/services/transactionPoolService/transactionPoolService.spec.ts +++ b/packages/relay/tests/lib/services/transactionPoolService/transactionPoolService.spec.ts @@ -6,7 +6,6 @@ import { Logger, pino } from 'pino'; import * as sinon from 'sinon'; import { TransactionPoolService } from '../../../../src/lib/services/transactionPoolService/transactionPoolService'; -import { IExecuteTransactionEventPayload } from '../../../../src/lib/types/events'; import { AddToListResult, PendingTransactionStorage } from '../../../../src/lib/types/transactionPool'; describe('TransactionPoolService Test Suite', function () { @@ -18,7 +17,6 @@ describe('TransactionPoolService Test Suite', function () { const testAddress = '0x742d35cc6629c0532c262d2d73f4c8e1a1b7b7b7'; const testTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; - const testTransactionId = '0.0.123@1234567890.123456789'; const testTransaction: Transaction = { hash: testTxHash, data: '0x', @@ -30,21 +28,6 @@ describe('TransactionPoolService Test Suite', function () { nonce: 1, } as Transaction; - const createTestEventPayload = ( - overrides?: Partial, - ): IExecuteTransactionEventPayload => ({ - transactionId: testTransactionId, - transactionHash: testTxHash, - txConstructorName: 'EthereumTransaction', - operatorAccountId: '0.0.2', - requestDetails: { - requestId: 'test-request-id', - ipAddress: '127.0.0.1', - } as any, - originalCallerAddress: testAddress, - ...overrides, - }); - beforeEach(() => { logger = pino({ level: 'silent' }); diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 57c1da8376..3606a404ea 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -22,7 +22,7 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; import { ConfigServiceTestHelper } from '../../../config-service/tests/configServiceTestHelper'; -import { withOverriddenEnvsInMochaTest } from '../../../relay/tests/helpers'; +import { overrideEnvsInMochaDescribe, withOverriddenEnvsInMochaTest } from '../../../relay/tests/helpers'; import basicContract from '../../tests/contracts/Basic.json'; import RelayCalls from '../../tests/helpers/constants'; import MirrorClient from '../clients/mirrorClient'; @@ -32,6 +32,7 @@ import basicContractJson from '../contracts/Basic.json'; import logsContractJson from '../contracts/Logs.json'; // Local resources from contracts directory import parentContractJson from '../contracts/Parent.json'; +import reverterContractJson from '../contracts/Reverter.json'; // Assertions from local resources import Assertions from '../helpers/assertions'; import { Utils } from '../helpers/utils'; @@ -765,6 +766,288 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { type: 1, }; + describe('Transaction Pool feature', async () => { + overrideEnvsInMochaDescribe({ USE_ASYNC_TX_PROCESSING: true }); + describe('ENABLE_TX_POOL = true', async () => { + beforeEach(async () => { + await new Promise(r => setTimeout(r, 2000)); + }); + overrideEnvsInMochaDescribe({ ENABLE_TX_POOL: true }); + it('should have equal nonces (pending and latest) after successfully validated transaction', async () => { + const tx = { + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: await relay.getAccountNonce(accounts[1].address), + }; + const signedTx = await accounts[1].wallet.signTransaction(tx); + const txHash = await relay.sendRawTransaction(signedTx); + await relay.pollForValidTransactionReceipt(txHash); + + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const noncePending = await relay.getAccountNonce(accounts[1].address, 'pending'); + + expect(nonceLatest).to.equal(noncePending); + }); + + it('should have equal nonces (pending and latest) after CN reverted transaction', async () => { + const tx = { + ...defaultLondonTransactionData, + to: null, + data: '0x' + '00'.repeat(5121), + nonce: await relay.getAccountNonce(accounts[1].address), + gasLimit: 41484, + }; + const signedTx = await accounts[1].wallet.signTransaction(tx); + const txHash = await relay.sendRawTransaction(signedTx); + await relay.pollForValidTransactionReceipt(txHash); + const mnResult = await mirrorNode.get(`/contracts/results/${txHash}`); + + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const noncePending = await relay.getAccountNonce(accounts[1].address, 'pending'); + + expect(mnResult.result).to.equal('INSUFFICIENT_GAS'); + expect(nonceLatest).to.equal(noncePending); + }); + + it('should have equal nonces (pending and latest) after multiple CN reverted transactions', async () => { + const accountNonce = await relay.getAccountNonce(accounts[1].address); + const tx1 = { + ...defaultLondonTransactionData, + to: null, + data: '0x' + '00'.repeat(5121), + nonce: accountNonce, + gasLimit: 41484, + }; + const tx2 = { + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: accountNonce, + gasLimit: 21000, + }; + const tx3 = { + ...defaultLondonTransactionData, + to: null, + data: '0x' + '00'.repeat(5121), + nonce: accountNonce + 1, + gasLimit: 41484, + }; + const signedTx1 = await accounts[1].wallet.signTransaction(tx1); + const signedTx2 = await accounts[1].wallet.signTransaction(tx2); + const signedTx3 = await accounts[1].wallet.signTransaction(tx3); + + const txHash1 = await relay.sendRawTransaction(signedTx1); + await new Promise((r) => setTimeout(r, 100)); + const txHash2 = await relay.sendRawTransaction(signedTx2); + await new Promise((r) => setTimeout(r, 100)); + const txHash3 = await relay.sendRawTransaction(signedTx3); + await Promise.all([ + relay.pollForValidTransactionReceipt(txHash1), + relay.pollForValidTransactionReceipt(txHash2), + relay.pollForValidTransactionReceipt(txHash3), + ]); + + const [mnResult1, mnResult2, mnResult3] = await Promise.all([ + mirrorNode.get(`/contracts/results/${txHash1}`), + mirrorNode.get(`/contracts/results/${txHash2}`), + mirrorNode.get(`/contracts/results/${txHash3}`), + ]); + + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const noncePending = await relay.getAccountNonce(accounts[1].address, 'pending'); + + expect(mnResult1.result).to.equal('INSUFFICIENT_GAS'); + expect(mnResult2.result).to.equal('SUCCESS'); + expect(mnResult3.result).to.equal('INSUFFICIENT_GAS'); + expect(nonceLatest).to.equal(noncePending); + }); + + it('should have equal nonces (pending and latest) for contract reverted transaction', async () => { + const reverterContract = await Utils.deployContract( + reverterContractJson.abi, + reverterContractJson.bytecode, + accounts[0].wallet, + ); + + const tx = { + ...defaultLondonTransactionData, + to: reverterContract.target, + data: '0xd0efd7ef', + nonce: await relay.getAccountNonce(accounts[1].address), + value: ONE_TINYBAR, + }; + const signedTx = await accounts[1].wallet.signTransaction(tx); + const txHash = await relay.sendRawTransaction(signedTx); + await relay.pollForValidTransactionReceipt(txHash); + const mnResult = await mirrorNode.get(`/contracts/results/${txHash}`); + + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const noncePending = await relay.getAccountNonce(accounts[1].address, 'pending'); + + expect(mnResult.result).to.equal('CONTRACT_REVERT_EXECUTED'); + expect(nonceLatest).to.equal(noncePending); + }); + + it('should have difference between pending and latest nonce when a single transaction has been sent', async () => { + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const signedTx1 = await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: nonceLatest, + gasLimit: 21000, + }); + const txHash1 = await relay.sendRawTransaction(signedTx1); + + const noncePending = await relay.getAccountNonce(accounts[1].address, 'pending'); + const signedTx2 = await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: noncePending, + gasLimit: 21000, + }); + const txHash2 = await relay.sendRawTransaction(signedTx2); + + const [receipt1, receipt2] = await Promise.all([ + relay.pollForValidTransactionReceipt(txHash1), + relay.pollForValidTransactionReceipt(txHash2), + ]); + + expect(receipt1.status).to.equal('0x1'); + expect(receipt2.status).to.equal('0x1'); + expect(nonceLatest).to.be.lessThan(noncePending); + }); + + it('should have difference between pending and latest nonce when multiple transactions have been sent simultaneously', async () => { + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const signedTx1 = await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: nonceLatest, + gasLimit: 21000, + }); + const txHash1 = await relay.sendRawTransaction(signedTx1); + + const noncePendingTx2 = await relay.getAccountNonce(accounts[1].address, 'pending'); + const signedTx2 = await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: noncePendingTx2, + gasLimit: 21000, + }); + const txHash2 = await relay.sendRawTransaction(signedTx2); + + const noncePendingTx3 = await relay.getAccountNonce(accounts[1].address, 'pending'); + const signedTx3 = await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: noncePendingTx3, + gasLimit: 21000, + }); + const txHash3 = await relay.sendRawTransaction(signedTx3); + + const [receipt1, receipt2, receipt3] = await Promise.all([ + relay.pollForValidTransactionReceipt(txHash1), + relay.pollForValidTransactionReceipt(txHash2), + relay.pollForValidTransactionReceipt(txHash3), + ]); + + expect(receipt1.status).to.equal('0x1'); + expect(receipt2.status).to.equal('0x1'); + expect(receipt3.status).to.equal('0x1'); + expect(nonceLatest).to.be.lessThan(noncePendingTx2); + expect(noncePendingTx2).to.be.lessThan(noncePendingTx3); + }); + }); + + describe('ENABLE_TX_POOL = false', async () => { + overrideEnvsInMochaDescribe({ ENABLE_TX_POOL: false }); + it('should return latest nonce after transaction has been sent ', async () => { + const nonce = await relay.getAccountNonce(accounts[1].address); + const tx = { + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce, + }; + const signedTx = await accounts[1].wallet.signTransaction(tx); + const txHash = await relay.sendRawTransaction(signedTx); + await relay.pollForValidTransactionReceipt(txHash); + + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + + expect(nonce).to.not.equal(nonceLatest); + expect(nonce).to.be.lessThan(nonceLatest); + }); + + it('should return equal nonces (pending and latest) when transaction has been sent', async () => { + const nonce = await relay.getAccountNonce(accounts[1].address); + const tx = { + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce, + }; + const signedTx = await accounts[1].wallet.signTransaction(tx); + await relay.sendRawTransaction(signedTx); + + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const noncePending = await relay.getAccountNonce(accounts[1].address, Constants.BLOCK_PENDING); + + expect(nonceLatest).to.equal(noncePending); + }); + + it('should fail with WRONG_NONCE when multiple transactions have been sent simultaneously', async () => { + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + + const txs = []; + for (let i = 0; i < 10; i++) { + txs.push( + relay.sendRawTransaction( + await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: nonceLatest + i, + }), + ), + ); + } + const txHashes = await Promise.all(txs); + + // wait for at least one block time + await new Promise((r) => setTimeout(r, 2100)); + + // currently, there is no way to fetch WRONG_NONCE transactions via MN or on `eth_getTransactionReceipt` by evm hash + // eth_sendRawTransaction returns always an evm hash, so as end-users we don't have the transaction id + + // the WRONG_NONCE transactions are filtered out from MN /api/v1/contract/results/ + // and /api/v1/transactions/ doesn't exist (only /api/v1/transactions/ + + // the only thing we can rely on right now is the "not found" status that is returned on /api/v1/contracts/results/ by evm tx hash + const receipts = await Promise.allSettled( + txHashes.map((hash) => mirrorNode.get(`/contracts/results/${hash}`)), + ); + const rejected = receipts.filter((receipt) => receipt.status === 'rejected'); + expect(rejected).to.not.be.empty; + rejected.forEach((reject) => expect(reject.reason.response.status).to.equal(404)); + }); + }); + + it('should fail with WRONG_NONCE when a transaction with very high nonce has been sent', async () => { + const nonceLatest = await relay.getAccountNonce(accounts[1].address); + const txHash = await relay.sendRawTransaction( + await accounts[1].wallet.signTransaction({ + ...defaultLondonTransactionData, + to: accounts[2].address, + nonce: nonceLatest + 100, + }), + ); + + // wait for at least one block time + await new Promise((r) => setTimeout(r, 2100)); + + await expect(mirrorNode.get(`/contracts/results/${txHash}`)).to.eventually.be.rejected.and.satisfy( + (err: any) => err.response.status === 404, + ); + }); + }); + it('@release should execute "eth_getTransactionByBlockHashAndIndex"', async function () { const response = await relay.call(RelayCalls.ETH_ENDPOINTS.ETH_GET_TRANSACTION_BY_BLOCK_HASH_AND_INDEX, [ mirrorContractDetails.block_hash.substring(0, 66), diff --git a/packages/server/tests/clients/relayClient.ts b/packages/server/tests/clients/relayClient.ts index 5024a5a09f..462a0931b2 100644 --- a/packages/server/tests/clients/relayClient.ts +++ b/packages/server/tests/clients/relayClient.ts @@ -94,10 +94,11 @@ export default class RelayClient { /** * @param evmAddress + * @param blockTag * Returns: The nonce of the account with the provided `evmAddress` */ - async getAccountNonce(evmAddress: string): Promise { - const nonce = await this.provider.send('eth_getTransactionCount', [evmAddress, 'latest']); + async getAccountNonce(evmAddress: string, blockTag: string = 'latest'): Promise { + const nonce = await this.provider.send('eth_getTransactionCount', [evmAddress, blockTag]); return Number(nonce); } diff --git a/packages/server/tests/helpers/utils.ts b/packages/server/tests/helpers/utils.ts index c086cd8034..22c1deae96 100644 --- a/packages/server/tests/helpers/utils.ts +++ b/packages/server/tests/helpers/utils.ts @@ -269,10 +269,10 @@ export class Utils { const address = wallet.address; // create hollow account - await signer.sendTransaction({ + await (await signer.sendTransaction({ to: wallet.address, value: accountBalance, - }); + })).wait(); const mirrorNodeAccount = (await mirrorNode.get(`/accounts/${address}`)).account; const accountId = AccountId.fromString(mirrorNodeAccount);