Skip to content

Commit eb3742a

Browse files
authored
chore: Increase batch request limit (#2601)
* chore: Increase batch request limit Signed-off-by: Victor Yanev <[email protected]> * fix: handle undefined method limit configuration Signed-off-by: Victor Yanev <[email protected]> * fix: handle undefined method limit configuration Signed-off-by: Victor Yanev <[email protected]> * chore: add types to KoaJsonRpc class Signed-off-by: Victor Yanev <[email protected]> * chore: revert changes to RpcResponse.ts Signed-off-by: Victor Yanev <[email protected]> * chore: remove batchMaxCount option for ethers.JsonRpcProvider in relayClient.ts Signed-off-by: Victor Yanev <[email protected]> * chore: remove batchMaxCount option for ethers.JsonRpcProvider in servicesClient.ts Signed-off-by: Victor Yanev <[email protected]> * chore: fix nonce pre-check test in rpc_batch1.spec.ts Signed-off-by: Victor Yanev <[email protected]> * chore: clean-up KoaJsonRpc Signed-off-by: Victor Yanev <[email protected]> * chore: remove unused constants Signed-off-by: Victor Yanev <[email protected]> * chore: fix warnings Signed-off-by: Victor Yanev <[email protected]> * chore: extract helper methods in utils.ts Signed-off-by: Victor Yanev <[email protected]> * chore: add return types to useRpc() and rpcApp() Signed-off-by: Victor Yanev <[email protected]> * chore: formatting Signed-off-by: Victor Yanev <[email protected]> * chore: fix code smells Signed-off-by: Victor Yanev <[email protected]> * chore: add interface IJsonRpcRequest Signed-off-by: Victor Yanev <[email protected]> * chore: remove batch request rate limit in webSocketServer.ts Signed-off-by: Victor Yanev <[email protected]> * test: fix rateLimiter.spec.ts Signed-off-by: Victor Yanev <[email protected]> * test: remove unused variable Signed-off-by: Victor Yanev <[email protected]> * chore: use BATCH_REQUEST_METHOD_NAME for ctx.state.methodName Signed-off-by: Victor Yanev <[email protected]> --------- Signed-off-by: Victor Yanev <[email protected]>
1 parent 1aea661 commit eb3742a

File tree

16 files changed

+201
-156
lines changed

16 files changed

+201
-156
lines changed

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "root",
33
"devDependencies": {
44
"@types/chai-as-promised": "^7.1.5",
5+
"@types/co-body": "6.1.0",
56
"@typescript-eslint/eslint-plugin": "^6.5.0",
67
"@typescript-eslint/parser": "^6.5.0",
78
"axios-mock-adapter": "^1.20.0",

packages/server/src/koaJsonRpc/index.ts

Lines changed: 49 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
*/
2020

21-
import { methodConfiguration } from './lib/methodConfiguration';
21+
import { IMethodRateLimitConfiguration, methodConfiguration } from './lib/methodConfiguration';
2222
import jsonResp from './lib/RpcResponse';
2323
import RateLimit from '../rateLimit';
2424
import parse from 'co-body';
@@ -27,104 +27,84 @@ import path from 'path';
2727
import { Logger } from 'pino';
2828

2929
import {
30-
ParseError,
31-
InvalidRequest,
3230
InternalError,
31+
InvalidRequest,
3332
IPRateLimitExceeded,
34-
MethodNotFound,
35-
Unauthorized,
3633
JsonRpcError as JsonRpcErrorServer,
34+
MethodNotFound,
35+
ParseError,
3736
} from './lib/RpcError';
3837
import Koa from 'koa';
3938
import { Histogram, Registry } from 'prom-client';
4039
import { JsonRpcError, predefined } from '@hashgraph/json-rpc-relay';
41-
import { RpcErrorCodeToStatusMap, HttpStatusCodeAndMessage } from './lib/HttpStatusCodeAndMessage';
40+
import { RpcErrorCodeToStatusMap } from './lib/HttpStatusCodeAndMessage';
41+
import {
42+
getBatchRequestsEnabled,
43+
getBatchRequestsMaxSize,
44+
getDefaultRateLimit,
45+
getLimitDuration,
46+
getRequestIdIsOptional,
47+
hasOwnProperty,
48+
} from './lib/utils';
49+
import { IJsonRpcRequest } from './lib/IJsonRpcRequest';
4250

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

46-
import constants from '@hashgraph/json-rpc-relay/dist/lib/constants';
47-
48-
const INTERNAL_ERROR = 'INTERNAL ERROR';
49-
const INVALID_PARAMS_ERROR = 'INVALID PARAMS ERROR';
5053
const INVALID_REQUEST = 'INVALID REQUEST';
51-
const IP_RATE_LIMIT_EXCEEDED = 'IP RATE LIMIT EXCEEDED';
52-
const JSON_RPC_ERROR = 'JSON RPC ERROR';
53-
const CONTRACT_REVERT = 'CONTRACT REVERT';
54-
const METHOD_NOT_FOUND = 'METHOD NOT FOUND';
5554
const REQUEST_ID_HEADER_NAME = 'X-Request-Id';
5655
const responseSuccessStatusCode = '200';
56+
const METRIC_HISTOGRAM_NAME = 'rpc_relay_method_result';
5757
const BATCH_REQUEST_METHOD_NAME = 'batch_request';
5858

5959
export default class KoaJsonRpc {
60-
private registry: any;
61-
private registryTotal: any;
62-
private token: any;
63-
private methodConfig: any;
64-
private duration: number;
65-
private limit: string;
66-
private rateLimit: RateLimit;
67-
private metricsRegistry: any;
68-
private koaApp: Koa<Koa.DefaultState, Koa.DefaultContext>;
60+
private readonly registry: { [key: string]: (params?: any) => Promise<any> };
61+
private readonly registryTotal: { [key: string]: number };
62+
private readonly methodConfig: IMethodRateLimitConfiguration;
63+
private readonly duration: number = getLimitDuration();
64+
private readonly defaultRateLimit: number = getDefaultRateLimit();
65+
private readonly limit: string;
66+
private readonly rateLimit: RateLimit;
67+
private readonly metricsRegistry: Registry;
68+
private readonly koaApp: Koa<Koa.DefaultState, Koa.DefaultContext>;
69+
private readonly logger: Logger;
70+
private readonly requestIdIsOptional: boolean = getRequestIdIsOptional(); // default to false
71+
private readonly batchRequestsMaxSize: number = getBatchRequestsMaxSize(); // default to 100
72+
private readonly methodResponseHistogram: Histogram;
73+
6974
private requestId: string;
70-
private logger: Logger;
71-
private startTimestamp!: number;
72-
private readonly requestIdIsOptional = process.env.REQUEST_ID_IS_OPTIONAL == 'true'; // default to false
73-
private readonly batchRequestsMaxSize: number = process.env.BATCH_REQUESTS_MAX_SIZE
74-
? parseInt(process.env.BATCH_REQUESTS_MAX_SIZE)
75-
: 100; // default to 100
76-
private methodResponseHistogram: Histogram | undefined;
77-
78-
constructor(logger: Logger, register: Registry, opts?) {
75+
76+
constructor(logger: Logger, register: Registry, opts?: { limit: string | null }) {
7977
this.koaApp = new Koa();
8078
this.requestId = '';
81-
this.limit = '1mb';
82-
this.duration = process.env.LIMIT_DURATION
83-
? parseInt(process.env.LIMIT_DURATION)
84-
: constants.DEFAULT_RATE_LIMIT.DURATION;
8579
this.registry = Object.create(null);
8680
this.registryTotal = Object.create(null);
8781
this.methodConfig = methodConfiguration;
88-
if (opts) {
89-
this.limit = opts.limit || this.limit;
90-
}
82+
this.limit = opts?.limit ?? '1mb';
9183
this.logger = logger;
9284
this.rateLimit = new RateLimit(logger.child({ name: 'ip-rate-limit' }), register, this.duration);
9385
this.metricsRegistry = register;
94-
this.initMetrics();
95-
}
96-
97-
private initMetrics() {
9886
// clear and create metric in registry
99-
const metricHistogramName = 'rpc_relay_method_result';
100-
this.metricsRegistry.removeSingleMetric(metricHistogramName);
87+
this.metricsRegistry.removeSingleMetric(METRIC_HISTOGRAM_NAME);
10188
this.methodResponseHistogram = new Histogram({
102-
name: metricHistogramName,
89+
name: METRIC_HISTOGRAM_NAME,
10390
help: 'JSON RPC method statusCode latency histogram',
10491
labelNames: ['method', 'statusCode', 'isPartOfBatch'],
10592
registers: [this.metricsRegistry],
10693
buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 20000, 30000, 40000, 50000, 60000], // ms (milliseconds)
10794
});
10895
}
10996

110-
// 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
111-
private getBatchRequestsEnabled(): boolean {
112-
return process.env.BATCH_REQUESTS_ENABLED == 'true'; // default to false
113-
}
114-
115-
useRpc(name, func) {
97+
useRpc(name: string, func: (params?: any) => Promise<any>): void {
11698
this.registry[name] = func;
117-
this.registryTotal[name] = this.methodConfig[name].total;
99+
this.registryTotal[name] = this.methodConfig[name]?.total;
118100

119101
if (!this.registryTotal[name]) {
120-
this.registryTotal[name] = process.env.DEFAULT_RATE_LIMIT || 200;
102+
this.registryTotal[name] = this.defaultRateLimit;
121103
}
122104
}
123105

124-
rpcApp() {
125-
return async (ctx, next) => {
126-
this.startTimestamp = ctx.state.start;
127-
106+
rpcApp(): (ctx: Koa.Context, _next: Koa.Next) => Promise<void> {
107+
return async (ctx: Koa.Context, _next: Koa.Next) => {
128108
this.requestId = ctx.state.reqId;
129109
ctx.set(REQUEST_ID_HEADER_NAME, this.requestId);
130110

@@ -135,20 +115,11 @@ export default class KoaJsonRpc {
135115
return;
136116
}
137117

138-
if (this.token) {
139-
const headerToken = ctx.get('authorization').split(' ').pop();
140-
if (headerToken !== this.token) {
141-
ctx.body = jsonResp(null, new Unauthorized(), undefined);
142-
return;
143-
}
144-
}
145-
146118
let body: any;
147119
try {
148120
body = await parse.json(ctx, { limit: this.limit });
149121
} catch (err) {
150-
const errBody = jsonResp(null, new ParseError(), undefined);
151-
ctx.body = errBody;
122+
ctx.body = jsonResp(null, new ParseError(), undefined);
152123
return;
153124
}
154125
//check if body is array or object
@@ -160,7 +131,7 @@ export default class KoaJsonRpc {
160131
};
161132
}
162133

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

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

199-
// verify rate limit for batch request
200-
const batchRequestTotalLimit = this.methodConfig[BATCH_REQUEST_METHOD_NAME].total;
201-
// check rate limit for method and ip
202-
if (this.rateLimit.shouldRateLimit(ctx.ip, BATCH_REQUEST_METHOD_NAME, batchRequestTotalLimit, this.requestId)) {
203-
return jsonResp(null, new IPRateLimitExceeded(BATCH_REQUEST_METHOD_NAME), undefined);
204-
}
205-
206170
const response: any[] = [];
207171
ctx.state.methodName = BATCH_REQUEST_METHOD_NAME;
208172

209173
// 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)
210-
const promises = body.map((item: any) => {
174+
const promises: Promise<any>[] = body.map(async (item: any) => {
211175
const startTime = Date.now();
212-
const result = this.getRequestResult(item, ctx.ip).then((res) => {
176+
return this.getRequestResult(item, ctx.ip).then((res) => {
213177
const ms = Date.now() - startTime;
214178
this.methodResponseHistogram?.labels(item.method, `${res.error ? res.error.code : 200}`, 'true').observe(ms);
215179
return res;
216180
});
217-
218-
return result;
219181
});
220182
const results = await Promise.all(promises);
221183
response.push(...results);
@@ -226,7 +188,7 @@ export default class KoaJsonRpc {
226188
ctx.state.status = responseSuccessStatusCode;
227189
}
228190

229-
async getRequestResult(request: any, ip: any): Promise<any> {
191+
async getRequestResult(request: any, ip: string): Promise<any> {
230192
try {
231193
const methodName = request.method;
232194

@@ -259,12 +221,12 @@ export default class KoaJsonRpc {
259221
}
260222
}
261223

262-
validateJsonRpcRequest(body): boolean {
224+
validateJsonRpcRequest(body: IJsonRpcRequest): boolean {
263225
// validate it has the correct jsonrpc version, method, and id
264226
if (
265227
body.jsonrpc !== '2.0' ||
266228
!hasOwnProperty(body, 'method') ||
267-
this.hasInvalidReqestId(body) ||
229+
this.hasInvalidRequestId(body) ||
268230
!hasOwnProperty(body, 'id')
269231
) {
270232
this.logger.warn(
@@ -295,7 +257,7 @@ export default class KoaJsonRpc {
295257
return this.requestId;
296258
}
297259

298-
hasInvalidReqestId(body): boolean {
260+
hasInvalidRequestId(body: IJsonRpcRequest): boolean {
299261
const hasId = hasOwnProperty(body, 'id');
300262
if (this.requestIdIsOptional && !hasId) {
301263
// If the request is invalid, we still want to return a valid JSON-RPC response, default id to 0
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*-
2+
*
3+
* Hedera JSON RPC Relay - Hardhat Example
4+
*
5+
* Copyright (C) 2024 Hedera Hashgraph, LLC
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*
19+
*/
20+
21+
export interface IJsonRpcRequest {
22+
id: string;
23+
jsonrpc: string;
24+
method: string;
25+
params?: any[];
26+
}

packages/server/src/koaJsonRpc/lib/methodConfiguration.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,20 @@ dotenv.config({ path: path.resolve(__dirname, '../../../../../.env') });
2424

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

27-
const tier1rateLimit = process.env.TIER_1_RATE_LIMIT || CONSTANTS.DEFAULT_RATE_LIMIT.TIER_1;
28-
const tier2rateLimit = process.env.TIER_2_RATE_LIMIT || CONSTANTS.DEFAULT_RATE_LIMIT.TIER_2;
29-
const tier3rateLimit = process.env.TIER_3_RATE_LIMIT || CONSTANTS.DEFAULT_RATE_LIMIT.TIER_3;
27+
const tier1rateLimit = parseInt(process.env.TIER_1_RATE_LIMIT ?? CONSTANTS.DEFAULT_RATE_LIMIT.TIER_1.toString());
28+
const tier2rateLimit = parseInt(process.env.TIER_2_RATE_LIMIT ?? CONSTANTS.DEFAULT_RATE_LIMIT.TIER_2.toString());
29+
const tier3rateLimit = parseInt(process.env.TIER_3_RATE_LIMIT ?? CONSTANTS.DEFAULT_RATE_LIMIT.TIER_3.toString());
30+
31+
export interface IMethodRateLimit {
32+
total: number;
33+
}
34+
35+
export interface IMethodRateLimitConfiguration {
36+
[method: string]: IMethodRateLimit;
37+
}
3038

3139
// total requests per rate limit duration (default ex. 200 request per 60000ms)
32-
export const methodConfiguration = {
40+
export const methodConfiguration: IMethodRateLimitConfiguration = {
3341
web3_clientVersion: {
3442
total: tier3rateLimit,
3543
},

0 commit comments

Comments
 (0)