Skip to content

Commit

Permalink
Merge branch 'main' into 2260-Implement-a-Memory-Leak-detection-Test
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Yanev <victor.yanev@limechain.tech>

# Conflicts:
#	package-lock.json
#	package.json
#	packages/server/tsconfig.json
  • Loading branch information
victor-yanev committed Jul 12, 2024
1 parent 4580e6c commit 806c8a1
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 155 deletions.
136 changes: 49 additions & 87 deletions packages/server/src/koaJsonRpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*
*/

import { methodConfiguration } from './lib/methodConfiguration';
import { IMethodRateLimitConfiguration, methodConfiguration } from './lib/methodConfiguration';
import jsonResp from './lib/RpcResponse';
import RateLimit from '../rateLimit';
import parse from 'co-body';
Expand All @@ -27,104 +27,84 @@ import path from 'path';
import { Logger } from 'pino';

import {
ParseError,
InvalidRequest,
InternalError,
InvalidRequest,
IPRateLimitExceeded,
MethodNotFound,
Unauthorized,
JsonRpcError as JsonRpcErrorServer,
MethodNotFound,
ParseError,
} from './lib/RpcError';
import Koa from 'koa';
import { Histogram, Registry } from 'prom-client';
import { JsonRpcError, predefined } from '@hashgraph/json-rpc-relay';
import { RpcErrorCodeToStatusMap, HttpStatusCodeAndMessage } from './lib/HttpStatusCodeAndMessage';
import { RpcErrorCodeToStatusMap } from './lib/HttpStatusCodeAndMessage';
import {
getBatchRequestsEnabled,
getBatchRequestsMaxSize,
getDefaultRateLimit,
getLimitDuration,
getRequestIdIsOptional,
hasOwnProperty,
} from './lib/utils';
import { IJsonRpcRequest } from './lib/IJsonRpcRequest';

const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
dotenv.config({ path: path.resolve(__dirname, '../../../../../.env') });

import constants from '@hashgraph/json-rpc-relay/dist/lib/constants';

const INTERNAL_ERROR = 'INTERNAL ERROR';
const INVALID_PARAMS_ERROR = 'INVALID PARAMS ERROR';
const INVALID_REQUEST = 'INVALID REQUEST';
const IP_RATE_LIMIT_EXCEEDED = 'IP RATE LIMIT EXCEEDED';
const JSON_RPC_ERROR = 'JSON RPC ERROR';
const CONTRACT_REVERT = 'CONTRACT REVERT';
const METHOD_NOT_FOUND = 'METHOD NOT FOUND';
const REQUEST_ID_HEADER_NAME = 'X-Request-Id';
const responseSuccessStatusCode = '200';
const METRIC_HISTOGRAM_NAME = 'rpc_relay_method_result';
const BATCH_REQUEST_METHOD_NAME = 'batch_request';

export default class KoaJsonRpc {
private registry: any;
private registryTotal: any;
private token: any;
private methodConfig: any;
private duration: number;
private limit: string;
private rateLimit: RateLimit;
private metricsRegistry: any;
private koaApp: Koa<Koa.DefaultState, Koa.DefaultContext>;
private readonly registry: { [key: string]: (params?: any) => Promise<any> };
private readonly registryTotal: { [key: string]: number };
private readonly methodConfig: IMethodRateLimitConfiguration;
private readonly duration: number = getLimitDuration();
private readonly defaultRateLimit: number = getDefaultRateLimit();
private readonly limit: string;
private readonly rateLimit: RateLimit;
private readonly metricsRegistry: Registry;
private readonly koaApp: Koa<Koa.DefaultState, Koa.DefaultContext>;
private readonly logger: Logger;
private readonly requestIdIsOptional: boolean = getRequestIdIsOptional(); // default to false
private readonly batchRequestsMaxSize: number = getBatchRequestsMaxSize(); // default to 100
private readonly methodResponseHistogram: Histogram;

private requestId: string;
private logger: Logger;
private startTimestamp!: number;
private readonly requestIdIsOptional = process.env.REQUEST_ID_IS_OPTIONAL == 'true'; // default to false
private readonly batchRequestsMaxSize: number = process.env.BATCH_REQUESTS_MAX_SIZE
? parseInt(process.env.BATCH_REQUESTS_MAX_SIZE)
: 100; // default to 100
private methodResponseHistogram: Histogram | undefined;

constructor(logger: Logger, register: Registry, opts?) {

constructor(logger: Logger, register: Registry, opts?: { limit: string | null }) {
this.koaApp = new Koa();
this.requestId = '';
this.limit = '1mb';
this.duration = process.env.LIMIT_DURATION
? parseInt(process.env.LIMIT_DURATION)
: constants.DEFAULT_RATE_LIMIT.DURATION;
this.registry = Object.create(null);
this.registryTotal = Object.create(null);
this.methodConfig = methodConfiguration;
if (opts) {
this.limit = opts.limit || this.limit;
}
this.limit = opts?.limit ?? '1mb';
this.logger = logger;
this.rateLimit = new RateLimit(logger.child({ name: 'ip-rate-limit' }), register, this.duration);
this.metricsRegistry = register;
this.initMetrics();
}

private initMetrics() {
// clear and create metric in registry
const metricHistogramName = 'rpc_relay_method_result';
this.metricsRegistry.removeSingleMetric(metricHistogramName);
this.metricsRegistry.removeSingleMetric(METRIC_HISTOGRAM_NAME);
this.methodResponseHistogram = new Histogram({
name: metricHistogramName,
name: METRIC_HISTOGRAM_NAME,
help: 'JSON RPC method statusCode latency histogram',
labelNames: ['method', 'statusCode', 'isPartOfBatch'],
registers: [this.metricsRegistry],
buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 20000, 30000, 40000, 50000, 60000], // ms (milliseconds)
});
}

// we do it as a method so we can mock it in tests, since by default is false, but we need to test it as true
private getBatchRequestsEnabled(): boolean {
return process.env.BATCH_REQUESTS_ENABLED == 'true'; // default to false
}

useRpc(name, func) {
useRpc(name: string, func: (params?: any) => Promise<any>): void {
this.registry[name] = func;
this.registryTotal[name] = this.methodConfig[name].total;
this.registryTotal[name] = this.methodConfig[name]?.total;

if (!this.registryTotal[name]) {
this.registryTotal[name] = process.env.DEFAULT_RATE_LIMIT || 200;
this.registryTotal[name] = this.defaultRateLimit;
}
}

rpcApp() {
return async (ctx, next) => {
this.startTimestamp = ctx.state.start;

rpcApp(): (ctx: Koa.Context, _next: Koa.Next) => Promise<void> {
return async (ctx: Koa.Context, _next: Koa.Next) => {
this.requestId = ctx.state.reqId;
ctx.set(REQUEST_ID_HEADER_NAME, this.requestId);

Expand All @@ -135,20 +115,11 @@ export default class KoaJsonRpc {
return;
}

if (this.token) {
const headerToken = ctx.get('authorization').split(' ').pop();
if (headerToken !== this.token) {
ctx.body = jsonResp(null, new Unauthorized(), undefined);
return;
}
}

let body: any;
try {
body = await parse.json(ctx, { limit: this.limit });
} catch (err) {
const errBody = jsonResp(null, new ParseError(), undefined);
ctx.body = errBody;
ctx.body = jsonResp(null, new ParseError(), undefined);
return;
}
//check if body is array or object
Expand All @@ -160,7 +131,7 @@ export default class KoaJsonRpc {
};
}

private async handleSingleRequest(ctx, body: any): Promise<void> {
private async handleSingleRequest(ctx: Koa.Context, body: any): Promise<void> {
ctx.state.methodName = body.method;
const response = await this.getRequestResult(body, ctx.ip);
ctx.body = response;
Expand All @@ -175,9 +146,9 @@ export default class KoaJsonRpc {
}
}

private async handleMultipleRequest(ctx, body: any): Promise<void> {
private async handleMultipleRequest(ctx: Koa.Context, body: any[]): Promise<void> {
// verify that batch requests are enabled
if (!this.getBatchRequestsEnabled()) {
if (!getBatchRequestsEnabled()) {
ctx.body = jsonResp(null, predefined.BATCH_REQUESTS_DISABLED, undefined);
ctx.status = 400;
ctx.state.status = `${ctx.status} (${INVALID_REQUEST})`;
Expand All @@ -196,26 +167,17 @@ export default class KoaJsonRpc {
return;
}

// verify rate limit for batch request
const batchRequestTotalLimit = this.methodConfig[BATCH_REQUEST_METHOD_NAME].total;
// check rate limit for method and ip
if (this.rateLimit.shouldRateLimit(ctx.ip, BATCH_REQUEST_METHOD_NAME, batchRequestTotalLimit, this.requestId)) {
return jsonResp(null, new IPRateLimitExceeded(BATCH_REQUEST_METHOD_NAME), undefined);
}

const response: any[] = [];
ctx.state.methodName = BATCH_REQUEST_METHOD_NAME;

// we do the requests in parallel to save time, but we need to keep track of the order of the responses (since the id might be optional)
const promises = body.map((item: any) => {
const promises: Promise<any>[] = body.map(async (item: any) => {
const startTime = Date.now();
const result = this.getRequestResult(item, ctx.ip).then((res) => {
return this.getRequestResult(item, ctx.ip).then((res) => {
const ms = Date.now() - startTime;
this.methodResponseHistogram?.labels(item.method, `${res.error ? res.error.code : 200}`, 'true').observe(ms);
return res;
});

return result;
});
const results = await Promise.all(promises);
response.push(...results);
Expand All @@ -226,7 +188,7 @@ export default class KoaJsonRpc {
ctx.state.status = responseSuccessStatusCode;
}

async getRequestResult(request: any, ip: any): Promise<any> {
async getRequestResult(request: any, ip: string): Promise<any> {
try {
const methodName = request.method;

Expand Down Expand Up @@ -259,12 +221,12 @@ export default class KoaJsonRpc {
}
}

validateJsonRpcRequest(body): boolean {
validateJsonRpcRequest(body: IJsonRpcRequest): boolean {
// validate it has the correct jsonrpc version, method, and id
if (
body.jsonrpc !== '2.0' ||
!hasOwnProperty(body, 'method') ||
this.hasInvalidReqestId(body) ||
this.hasInvalidRequestId(body) ||
!hasOwnProperty(body, 'id')
) {
this.logger.warn(
Expand Down Expand Up @@ -295,7 +257,7 @@ export default class KoaJsonRpc {
return this.requestId;
}

hasInvalidReqestId(body): boolean {
hasInvalidRequestId(body: IJsonRpcRequest): boolean {
const hasId = hasOwnProperty(body, 'id');
if (this.requestIdIsOptional && !hasId) {
// If the request is invalid, we still want to return a valid JSON-RPC response, default id to 0
Expand Down
26 changes: 26 additions & 0 deletions packages/server/src/koaJsonRpc/lib/IJsonRpcRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*-
*
* Hedera JSON RPC Relay - Hardhat Example
*
* Copyright (C) 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.
*
*/

export interface IJsonRpcRequest {
id: string;
jsonrpc: string;
method: string;
params?: any[];
}
16 changes: 12 additions & 4 deletions packages/server/src/koaJsonRpc/lib/methodConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,20 @@ dotenv.config({ path: path.resolve(__dirname, '../../../../../.env') });

import CONSTANTS from '../../../../relay/dist/lib/constants';

const tier1rateLimit = process.env.TIER_1_RATE_LIMIT || CONSTANTS.DEFAULT_RATE_LIMIT.TIER_1;
const tier2rateLimit = process.env.TIER_2_RATE_LIMIT || CONSTANTS.DEFAULT_RATE_LIMIT.TIER_2;
const tier3rateLimit = process.env.TIER_3_RATE_LIMIT || CONSTANTS.DEFAULT_RATE_LIMIT.TIER_3;
const tier1rateLimit = parseInt(process.env.TIER_1_RATE_LIMIT ?? CONSTANTS.DEFAULT_RATE_LIMIT.TIER_1.toString());
const tier2rateLimit = parseInt(process.env.TIER_2_RATE_LIMIT ?? CONSTANTS.DEFAULT_RATE_LIMIT.TIER_2.toString());
const tier3rateLimit = parseInt(process.env.TIER_3_RATE_LIMIT ?? CONSTANTS.DEFAULT_RATE_LIMIT.TIER_3.toString());

export interface IMethodRateLimit {
total: number;
}

export interface IMethodRateLimitConfiguration {
[method: string]: IMethodRateLimit;
}

// total requests per rate limit duration (default ex. 200 request per 60000ms)
export const methodConfiguration = {
export const methodConfiguration: IMethodRateLimitConfiguration = {
web3_clientVersion: {
total: tier3rateLimit,
},
Expand Down
25 changes: 25 additions & 0 deletions packages/server/src/koaJsonRpc/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,33 @@
*/

import type { Server } from 'http';
import constants from '@hashgraph/json-rpc-relay/dist/lib/constants';

export function hasOwnProperty(obj: any, prop: PropertyKey): boolean {
return Object.prototype.hasOwnProperty.call(obj, prop);
}

export function setServerTimeout(server: Server): void {
const requestTimeoutMs = parseInt(process.env.SERVER_REQUEST_TIMEOUT_MS ?? '60000');
server.setTimeout(requestTimeoutMs);
}

export function getBatchRequestsMaxSize(): number {
return parseInt(process.env.BATCH_REQUESTS_MAX_SIZE ?? '100');
}

export function getLimitDuration(): number {
return parseInt(process.env.LIMIT_DURATION ?? constants.DEFAULT_RATE_LIMIT.DURATION.toString());
}

export function getDefaultRateLimit(): number {
return parseInt(process.env.DEFAULT_RATE_LIMIT ?? '200');
}

export function getRequestIdIsOptional(): boolean {
return process.env.REQUEST_ID_IS_OPTIONAL == 'true';
}

export function getBatchRequestsEnabled(): boolean {
return process.env.BATCH_REQUESTS_ENABLED == 'true';
}
2 changes: 1 addition & 1 deletion packages/server/src/rateLimit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class RateLimit {
private logger: Logger;
private ipRateLimitCounter: Counter;

constructor(logger: Logger, register: Registry, duration) {
constructor(logger: Logger, register: Registry, duration: number) {
this.logger = logger;
this.duration = duration;
this.database = Object.create(null);
Expand Down
2 changes: 1 addition & 1 deletion packages/server/tests/acceptance/rpc_batch1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () {

Assertions.jsonRpcError(
// @ts-ignore
rejected?.reason?.response?.bodyJson?.error,
rejected?.reason?.info?.error,
predefined.NONCE_TOO_LOW(nonce, nonce + 1),
);
}),
Expand Down
10 changes: 4 additions & 6 deletions packages/server/tests/acceptance/rpc_batch2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { expect } from 'chai';
import { ethers } from 'ethers';
import { AliasAccount } from '../types/AliasAccount';
import { Utils } from '../helpers/utils';
import { predefined } from '@hashgraph/json-rpc-relay';
import { EthImpl } from '@hashgraph/json-rpc-relay/dist/lib/eth';
import { numberTo0x } from '@hashgraph/json-rpc-relay/dist/formatters';
import { ContractId, Hbar, HbarUnit } from '@hashgraph/sdk';

// Assertions from local resources
Expand All @@ -35,15 +38,10 @@ import storageContractJson from '../contracts/Storage.json';
import TokenCreateJson from '../contracts/TokenCreateContract.json';
import ERC20MockJson from '../contracts/ERC20Mock.json';

// Errors from local resources
import { predefined } from '../../../relay/src/lib/errors/JsonRpcError';

// Helper functions/constants from local resources
import { EthImpl } from '../../../../packages/relay/src/lib/eth';
import RelayCalls from '../../tests/helpers/constants';
import Helper from '../../tests/helpers/constants';
import Address from '../../tests/helpers/constants';
import { numberTo0x } from '../../../../packages/relay/src/formatters';
import constants from '../../tests/helpers/constants';

describe('@api-batch-2 RPC Server Acceptance Tests', function () {
Expand Down Expand Up @@ -195,7 +193,7 @@ describe('@api-batch-2 RPC Server Acceptance Tests', function () {
],
requestId,
);
const gasPriceDeviation = parseFloat((EthImpl.gasTxBaseCost * 0.2).toString());
const gasPriceDeviation = parseFloat((Number(EthImpl.gasTxBaseCost) * 0.2).toString());
expect(res).to.contain('0x');
expect(parseInt(res)).to.be.lessThan(Number(EthImpl.gasTxBaseCost) * (1 + gasPriceDeviation));
expect(parseInt(res)).to.be.greaterThan(Number(EthImpl.gasTxBaseCost) * (1 - gasPriceDeviation));
Expand Down
Loading

0 comments on commit 806c8a1

Please sign in to comment.