diff --git a/package.json b/package.json index 82d108de4f..dea9743ec4 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "acceptancetest:api_batch2": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-batch-2' --exit", "acceptancetest:api_batch3": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-batch-3' --exit", "acceptancetest:erc20": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@erc20' --exit", - "acceptancetest:ratelimiter": "ts-mocha packages/ws-server/tests/acceptance/index.spec.ts -g '@web-socket-ratelimiter' --exit && ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@ratelimiter' --exit", + "acceptancetest:ratelimiter": "ts-mocha packages/ws-server/tests/acceptance/index.spec.ts -g '@web-socket-ratelimiter' --exit && HBAR_RATE_LIMIT_TINYBAR=3000000000 ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@ratelimiter' --exit", "acceptancetest:tokencreate": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@tokencreate' --exit", "acceptancetest:tokenmanagement": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@tokenmanagement' --exit", "acceptancetest:htsprecompilev1": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@htsprecompilev1' --exit", diff --git a/packages/relay/src/lib/clients/sdkClient.ts b/packages/relay/src/lib/clients/sdkClient.ts index 774249f5da..a3ea906717 100644 --- a/packages/relay/src/lib/clients/sdkClient.ts +++ b/packages/relay/src/lib/clients/sdkClient.ts @@ -630,17 +630,6 @@ export class SDKClient { return balance.hbars.to(HbarUnit.Tinybar).multipliedBy(constants.TINYBAR_TO_WEIBAR_COEF); } - private async calculateFileAppendTxTotalTinybarsCost(fileAppendTx: FileAppendTransaction): Promise { - // @ts-ignore - const fileAppendTxs = fileAppendTx._transactionIds.list.map((txId) => - new TransactionRecordQuery().setTransactionId(txId).execute(this.clientMain), - ); - - return (await Promise.all(fileAppendTxs)).reduce((base, record) => { - return base + record.transactionFee.toTinybars().toNumber(); - }, 0); - } - private createFile = async ( callData: Uint8Array, client: Client, @@ -687,22 +676,24 @@ export class SDKClient { .setContents(hexedCallData.substring(this.fileAppendChunkSize, hexedCallData.length)) .setChunkSize(this.fileAppendChunkSize) .setMaxChunks(this.maxChunks); - const fileAppendTxResponse = await fileAppendTx.execute(client); - - // get transaction fee and add expense to limiter - const appendFileRecord = await fileAppendTxResponse.getRecord(this.clientMain); - transactionFee = appendFileRecord.transactionFee; - this.hbarLimiter.addExpense(transactionFee.toTinybars().toNumber(), currentDateNow); - - this.captureMetrics( - SDKClient.transactionMode, - fileAppendTx.constructor.name, - Status.Success, - await this.calculateFileAppendTxTotalTinybarsCost(fileAppendTx), - 0, - callerName, - interactingEntity, - ); + const fileAppendTxResponses = await fileAppendTx.executeAll(client); + + for (let fileAppendTxResponse of fileAppendTxResponses) { + // get transaction fee and add expense to limiter + const appendFileRecord = await fileAppendTxResponse.getRecord(this.clientMain); + const tinybarsCost = appendFileRecord.transactionFee.toTinybars().toNumber(); + + this.captureMetrics( + SDKClient.transactionMode, + fileAppendTx.constructor.name, + Status.Success, + tinybarsCost, + 0, + callerName, + interactingEntity, + ); + this.hbarLimiter.addExpense(tinybarsCost, currentDateNow); + } } // Ensure that the calldata file is not empty diff --git a/packages/server/tests/acceptance/index.spec.ts b/packages/server/tests/acceptance/index.spec.ts index 1c2104338a..85af1a3608 100644 --- a/packages/server/tests/acceptance/index.spec.ts +++ b/packages/server/tests/acceptance/index.spec.ts @@ -31,6 +31,7 @@ import fs from 'fs'; import ServicesClient from '../clients/servicesClient'; import MirrorClient from '../clients/mirrorClient'; import RelayClient from '../clients/relayClient'; +import MetricsClient from '../clients/metricsClient'; // Server related import app from '../../dist/server'; @@ -86,12 +87,22 @@ describe('RPC Server Acceptance Tests', function () { logger.child({ name: `services-test-client` }), ); global.mirrorNode = new MirrorClient(MIRROR_NODE_URL, logger.child({ name: `mirror-node-test-client` })); + global.metrics = new MetricsClient(RELAY_URL, logger.child({ name: `metrics-test-client` })); global.relay = new RelayClient(RELAY_URL, logger.child({ name: `relay-test-client` })); global.relayServer = relayServer; global.socketServer = socketServer; global.logger = logger; global.initialBalance = INITIAL_BALANCE; + global.restartLocalRelay = async function () { + if (global.relayIsLocal) { + stopRelay(); + await new Promise((r) => setTimeout(r, 5000)); // wait for server to shutdown + + runLocalRelay(); + } + }; + before(async () => { // configuration details logger.info('Acceptance Tests Configurations successfully loaded'); @@ -152,15 +163,7 @@ describe('RPC Server Acceptance Tests', function () { const cost = startOperatorBalance.toTinybars().subtract(endOperatorBalance.toTinybars()); logger.info(`Acceptance Tests spent ${Hbar.fromTinybars(cost)}`); - //stop relay - logger.info('Stop relay'); - if (relayServer !== undefined) { - relayServer.close(); - } - - if (process.env.TEST_WS_SERVER === 'true' && socketServer !== undefined) { - socketServer.close(); - } + stopRelay(); }); describe('Acceptance tests', async () => { @@ -181,6 +184,18 @@ describe('RPC Server Acceptance Tests', function () { } } + function stopRelay() { + //stop relay + logger.info('Stop relay'); + if (relayServer !== undefined) { + relayServer.close(); + } + + if (process.env.TEST_WS_SERVER === 'true' && global.socketServer !== undefined) { + global.socketServer.close(); + } + } + function runLocalRelay() { // start local relay, relay instance in local should not be running diff --git a/packages/server/tests/acceptance/rateLimiter.spec.ts b/packages/server/tests/acceptance/rateLimiter.spec.ts index 0245439a32..08ee9a4635 100644 --- a/packages/server/tests/acceptance/rateLimiter.spec.ts +++ b/packages/server/tests/acceptance/rateLimiter.spec.ts @@ -30,7 +30,9 @@ import relayConstants from '../../../../packages/relay/src/lib/constants'; // Local resources import parentContractJson from '../contracts/Parent.json'; +import largeContractJson from '../contracts/EstimatePrecompileContract.json'; import { Utils } from '../helpers/utils'; +import { predefined } from '@hashgraph/json-rpc-relay'; describe('@ratelimiter Rate Limiters Acceptance Tests', function () { this.timeout(480 * 1000); // 480 seconds @@ -38,7 +40,7 @@ describe('@ratelimiter Rate Limiters Acceptance Tests', function () { const accounts: AliasAccount[] = []; // @ts-ignore - const { mirrorNode, relay, logger, initialBalance } = global; + const { mirrorNode, relay, logger, initialBalance, metrics } = global; // cached entities let parentContractAddress: string; @@ -90,72 +92,121 @@ describe('@ratelimiter Rate Limiters Acceptance Tests', function () { }); }); - describe('HBAR Limiter Acceptance Tests', function () { - this.timeout(480 * 1000); // 480 seconds - - this.beforeAll(async () => { - requestId = Utils.generateRequestId(); - const requestIdPrefix = Utils.formatRequestIdMessage(requestId); - - logger.info(`${requestIdPrefix} Creating accounts`); - logger.info(`${requestIdPrefix} HBAR_RATE_LIMIT_TINYBAR: ${process.env.HBAR_RATE_LIMIT_TINYBAR}`); - - const initialAccount: AliasAccount = global.accounts[0]; - - const neededAccounts: number = 2; - accounts.push( - ...(await Utils.createMultipleAliasAccounts( - mirrorNode, - initialAccount, - neededAccounts, - initialBalance, - requestId, - )), - ); - global.accounts.push(...accounts); - - const parentContract = await Utils.deployContract( - parentContractJson.abi, - parentContractJson.bytecode, - accounts[0].wallet, - ); - - parentContractAddress = parentContract.target as string; - global.logger.trace(`${requestIdPrefix} Deploy parent contract on address ${parentContractAddress}`); - }); - - this.beforeEach(async () => { - requestId = Utils.generateRequestId(); - }); + // The following tests exhaust the hbar limit, so they should only be run against a local relay + if (global.relayIsLocal) { + describe('HBAR Limiter Acceptance Tests', function () { + before(async () => { + // Restart the relay to reset the limits + await global.restartLocalRelay(); + }); - describe('HBAR Rate Limit Tests', () => { - const defaultGasPrice = Assertions.defaultGasPrice; - const defaultGasLimit = 3_000_000; - - const defaultLondonTransactionData = { - value: ONE_TINYBAR, - chainId: Number(CHAIN_ID), - maxPriorityFeePerGas: defaultGasPrice, - maxFeePerGas: defaultGasPrice, - gasLimit: defaultGasLimit, - type: 2, - }; + this.timeout(480 * 1000); // 480 seconds + + this.beforeAll(async () => { + requestId = Utils.generateRequestId(); + const requestIdPrefix = Utils.formatRequestIdMessage(requestId); + + logger.info(`${requestIdPrefix} Creating accounts`); + logger.info(`${requestIdPrefix} HBAR_RATE_LIMIT_TINYBAR: ${process.env.HBAR_RATE_LIMIT_TINYBAR}`); + + const initialAccount: AliasAccount = global.accounts[0]; + + const neededAccounts: number = 2; + accounts.push( + ...(await Utils.createMultipleAliasAccounts( + mirrorNode, + initialAccount, + neededAccounts, + initialBalance, + requestId, + )), + ); + global.accounts.push(...accounts); + + const parentContract = await Utils.deployContract( + parentContractJson.abi, + parentContractJson.bytecode, + accounts[0].wallet, + ); + + parentContractAddress = parentContract.target as string; + global.logger.trace(`${requestIdPrefix} Deploy parent contract on address ${parentContractAddress}`); + }); - it('should execute "eth_sendRawTransaction" without triggering HBAR rate limit exceeded ', async function () { - const gasPrice = await relay.gasPrice(requestId); + this.beforeEach(async () => { + requestId = Utils.generateRequestId(); + }); - const transaction = { - ...defaultLondonTransactionData, - to: parentContractAddress, - nonce: await relay.getAccountNonce(accounts[1].address, requestId), - maxPriorityFeePerGas: gasPrice, - maxFeePerGas: gasPrice, + describe('HBAR Rate Limit Tests', () => { + const defaultGasPrice = Assertions.defaultGasPrice; + const defaultGasLimit = 3_000_000; + + const defaultLondonTransactionData = { + value: ONE_TINYBAR, + chainId: Number(CHAIN_ID), + maxPriorityFeePerGas: defaultGasPrice, + maxFeePerGas: defaultGasPrice, + gasLimit: defaultGasLimit, + type: 2, }; - const signedTx = await accounts[1].wallet.signTransaction(transaction); - await expect(relay.call(testConstants.ETH_ENDPOINTS.ETH_SEND_RAW_TRANSACTION, [signedTx], requestId)).to.be - .fulfilled; + it('should execute "eth_sendRawTransaction" without triggering HBAR rate limit exceeded', async function () { + const gasPrice = await relay.gasPrice(requestId); + const remainingHbarsBefore = Number(await metrics.get(testConstants.METRICS.REMAINING_HBAR_LIMIT)); + + const transaction = { + ...defaultLondonTransactionData, + to: parentContractAddress, + nonce: await relay.getAccountNonce(accounts[1].address, requestId), + maxPriorityFeePerGas: gasPrice, + maxFeePerGas: gasPrice, + }; + const signedTx = await accounts[1].wallet.signTransaction(transaction); + + await expect(relay.call(testConstants.ETH_ENDPOINTS.ETH_SEND_RAW_TRANSACTION, [signedTx], requestId)).to.be + .fulfilled; + const remainingHbarsAfter = Number(await metrics.get(testConstants.METRICS.REMAINING_HBAR_LIMIT)); + expect(remainingHbarsAfter).to.be.eq(remainingHbarsBefore); + }); + + it('should deploy a large contract and decrease remaining HBAR in limiter when transaction data is large', async function () { + const remainingHbarsBefore = Number(await metrics.get(testConstants.METRICS.REMAINING_HBAR_LIMIT)); + expect(remainingHbarsBefore).to.be.gt(0); + + const largeContract = await Utils.deployContract( + largeContractJson.abi, + largeContractJson.bytecode, + accounts[0].wallet, + ); + await largeContract.waitForDeployment(); + const remainingHbarsAfter = Number(await metrics.get(testConstants.METRICS.REMAINING_HBAR_LIMIT)); + expect(largeContract.target).to.not.be.null; + expect(remainingHbarsAfter).to.be.lt(remainingHbarsBefore); + }); + + it('multiple deployments of large contracts should eventually exhaust the remaining hbar limit', async function () { + const remainingHbarsBefore = Number(await metrics.get(testConstants.METRICS.REMAINING_HBAR_LIMIT)); + expect(remainingHbarsBefore).to.be.gt(0); + try { + for (let i = 0; i < 50; i++) { + const largeContract = await Utils.deployContract( + largeContractJson.abi, + largeContractJson.bytecode, + accounts[0].wallet, + ); + await largeContract.waitForDeployment(); + expect(largeContract.target).to.not.be.null; + } + + expect(true).to.be.false; + } catch (e: any) { + expect(e.message).to.contain(predefined.HBAR_RATE_LIMIT_EXCEEDED.message); + } + + const remainingHbarsAfter = Number(await metrics.get(testConstants.METRICS.REMAINING_HBAR_LIMIT)); + expect(remainingHbarsAfter).to.be.lte(0); + }); }); }); - }); + } }); diff --git a/packages/server/tests/clients/metricsClient.ts b/packages/server/tests/clients/metricsClient.ts new file mode 100644 index 0000000000..ecd1cd8054 --- /dev/null +++ b/packages/server/tests/clients/metricsClient.ts @@ -0,0 +1,77 @@ +/*- + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import Axios, { AxiosInstance } from 'axios'; +import { Logger } from 'pino'; +import { Utils } from '../helpers/utils'; + +export default class MetricsClient { + private readonly logger: Logger; + private readonly client: AxiosInstance; + private readonly relayUrl: string; + + constructor(relayUrl: string, logger: Logger) { + this.logger = logger; + this.relayUrl = relayUrl; + + const metricsClient = Axios.create({ + baseURL: `${relayUrl}/metrics`, + responseType: 'json' as const, + headers: { + 'Content-Type': 'application/json', + }, + method: 'GET', + timeout: 5 * 1000, + }); + + this.client = metricsClient; + } + + /** + * Retrieves the value of a specified metric. + * + * The response from the /metrics endpoint is a large string with multiple rows of key-value pairs, + * separated by " ", where the key is the metric name. + * Rows may begin with #, which represents a comment and should be ignored. + * This method retrieves the whole response, splits it into rows, finds the first row that starts with the + * provided metric name and returns the corresponding value. + * + * Example extract: + * + * pc_relay_hbar_rate_remaining 11000000000 + * rpc_relay_cache 0 + * rpc_websocket_subscription_times_bucket{le="0.05"} 0 + * rpc_websocket_subscription_times_bucket{le="1"} 0 + * rpc_websocket_subscription_times_bucket{le="10"} 0 + * rpc_websocket_subscription_times_bucket{le="60"} 0 + * + * @param metric + * @param requestId + */ + async get(metric: string, requestId?: string) { + const requestIdPrefix = Utils.formatRequestIdMessage(requestId); + this.logger.debug(`${requestIdPrefix} [GET] Read all metrics from ${this.relayUrl}/metrics`); + const allMetrics = (await this.client.get('')).data; + const allMetricsArray = allMetrics.split('\n'); + const matchPattern = `${metric} `; + const result = allMetricsArray.find((m) => m.startsWith(matchPattern)); + return result.replace(matchPattern, ''); + } +} diff --git a/packages/server/tests/helpers/constants.ts b/packages/server/tests/helpers/constants.ts index ff7b0c3778..7d6fa14b68 100644 --- a/packages/server/tests/helpers/constants.ts +++ b/packages/server/tests/helpers/constants.ts @@ -162,6 +162,10 @@ const ACTUAL_GAS_USED = { ERC_TOKEN_URI_NFT: 27508, }; +const METRICS = { + REMAINING_HBAR_LIMIT: 'rpc_relay_hbar_rate_remaining', +}; + const NON_EXISTING_ADDRESS = '0x5555555555555555555555555555555555555555'; const NON_EXISTING_TX_HASH = '0x5555555555555555555555555555555555555555555555555555555555555555'; const NON_EXISTING_BLOCK_HASH = '0x5555555555555555555555555555555555555555555555555555555555555555'; @@ -189,4 +193,5 @@ export default { AMOUNT, ACTUAL_GAS_USED, TINYBAR_TO_WEIBAR_COEF, + METRICS, }; diff --git a/packages/server/tests/localAcceptance.env b/packages/server/tests/localAcceptance.env index f30b06bf7a..153860b95e 100644 --- a/packages/server/tests/localAcceptance.env +++ b/packages/server/tests/localAcceptance.env @@ -1,6 +1,8 @@ HEDERA_NETWORK={"127.0.0.1:50211":"0.0.3"} -OPERATOR_ID_MAIN=0.0.2 -OPERATOR_KEY_MAIN=302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137 + +#This is a static acount created at startup in the local node +OPERATOR_ID_MAIN=0.0.1022 +OPERATOR_KEY_MAIN=302e020100300506032b657004220420a608e2130a0a3cb34f86e757303c862bee353d9ab77ba4387ec084f881d420d4 CHAIN_ID=0x12a MIRROR_NODE_URL_WEB3=http://127.0.0.1:8545 REDIS_ENABLED=true diff --git a/packages/ws-server/tests/acceptance/sendRawTransaction.spec.ts b/packages/ws-server/tests/acceptance/sendRawTransaction.spec.ts index 0170c6ea3c..2634446bdf 100644 --- a/packages/ws-server/tests/acceptance/sendRawTransaction.spec.ts +++ b/packages/ws-server/tests/acceptance/sendRawTransaction.spec.ts @@ -83,7 +83,7 @@ describe('@web-socket-batch-2 eth_sendRawTransaction', async function () { value: (10 * 10 ** 18).toString(), // 10hbar - the gasPrice to deploy deterministic proxy contract to: constants.DETERMINISTIC_DEPLOYMENT_SIGNER, gasPrice: await global.relay.gasPrice(), - gasLimit: numberTo0x(30000), + gasLimit: numberTo0x(5000000), }; });