Skip to content

Commit 28ea22f

Browse files
authored
fix: added safe guard for rewardPercentiles size limit (#3605)
Signed-off-by: Logan Nguyen <[email protected]>
1 parent 63fc16a commit 28ea22f

File tree

5 files changed

+82
-12
lines changed

5 files changed

+82
-12
lines changed

packages/relay/src/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export default {
8484
TYPE_TOKEN: 'token',
8585

8686
DEFAULT_FEE_HISTORY_MAX_RESULTS: 10,
87+
FEE_HISTORY_REWARD_PERCENTILES_MAX_SIZE: 100,
88+
8789
ORDER,
8890

8991
BLOCK_GAS_LIMIT: 30_000_000,

packages/relay/src/lib/eth.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,21 @@ export class EthImpl implements Eth {
326326
const maxResults = ConfigService.get('TEST')
327327
? constants.DEFAULT_FEE_HISTORY_MAX_RESULTS
328328
: Number(ConfigService.get('FEE_HISTORY_MAX_RESULTS'));
329+
const maxRewardPercentilesSize = constants.FEE_HISTORY_REWARD_PERCENTILES_MAX_SIZE;
329330

330331
if (this.logger.isLevelEnabled('trace')) {
331332
this.logger.trace(
332333
`${requestIdPrefix} feeHistory(blockCount=${blockCount}, newestBlock=${newestBlock}, rewardPercentiles=${rewardPercentiles})`,
333334
);
334335
}
335336

337+
if (rewardPercentiles && rewardPercentiles.length > maxRewardPercentilesSize) {
338+
throw predefined.INVALID_PARAMETER(
339+
2,
340+
`Reward percentiles size ${rewardPercentiles.length} is greater than the maximum allowed size ${maxRewardPercentilesSize}`,
341+
);
342+
}
343+
336344
try {
337345
const latestBlockNumber = await this.translateBlockTag(EthImpl.blockLatest, requestDetails);
338346
const newestBlockNumber =

packages/relay/tests/lib/eth/eth_feeHistory.spec.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3+
import { predefined } from '@hashgraph/json-rpc-relay/dist';
34
import { expect, use } from 'chai';
4-
import sinon from 'sinon';
55
import chaiAsPromised from 'chai-as-promised';
6+
import sinon from 'sinon';
67

7-
import constants from '../../../src/lib/constants';
8+
import { numberTo0x } from '../../../src/formatters';
89
import { SDKClient } from '../../../src/lib/clients';
10+
import constants from '../../../src/lib/constants';
11+
import { RequestDetails } from '../../../src/lib/types';
12+
import { overrideEnvsInMochaDescribe } from '../../helpers';
913
import {
1014
BASE_FEE_PER_GAS_HEX,
1115
BLOCK_NUMBER_2,
@@ -16,19 +20,15 @@ import {
1620
GAS_USED_RATIO,
1721
NOT_FOUND_RES,
1822
} from './eth-config';
19-
import { numberTo0x } from '../../../src/formatters';
2023
import { generateEthTestEnv } from './eth-helpers';
21-
import { overrideEnvsInMochaDescribe } from '../../helpers';
22-
import { RequestDetails } from '../../../src/lib/types';
23-
2424
use(chaiAsPromised);
2525

2626
let sdkClientStub: sinon.SinonStubbedInstance<SDKClient>;
2727
let getSdkClientStub: sinon.SinonStub;
2828

2929
describe('@ethFeeHistory using MirrorNode', async function () {
3030
this.timeout(10000);
31-
let { restMock, hapiServiceInstance, ethImpl, cacheService } = generateEthTestEnv();
31+
const { restMock, hapiServiceInstance, ethImpl, cacheService } = generateEthTestEnv();
3232

3333
const requestDetails = new RequestDetails({ requestId: 'eth_feeHistoryTest', ipAddress: '0.0.0.0' });
3434

@@ -65,14 +65,18 @@ describe('@ethFeeHistory using MirrorNode', async function () {
6565
restMock.onGet(BLOCKS_LIMIT_ORDER_URL).reply(200, JSON.stringify({ blocks: [latestBlock] }));
6666
restMock.onGet(`blocks/${previousBlock.number}`).reply(200, JSON.stringify(previousBlock));
6767
restMock.onGet(`blocks/${latestBlock.number}`).reply(200, JSON.stringify(latestBlock));
68-
restMock.onGet(`network/fees?timestamp=lte:${previousBlock.timestamp.to}`).reply(200, JSON.stringify(previousFees));
68+
restMock
69+
.onGet(`network/fees?timestamp=lte:${previousBlock.timestamp.to}`)
70+
.reply(200, JSON.stringify(previousFees));
6971
restMock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(200, JSON.stringify(latestFees));
7072
});
7173

7274
it('eth_feeHistory', async function () {
7375
const updatedFees = previousFees;
7476
previousFees.fees[2].gas += 1;
75-
restMock.onGet(`network/fees?timestamp=lte:${previousBlock.timestamp.to}`).reply(200, JSON.stringify(updatedFees));
77+
restMock
78+
.onGet(`network/fees?timestamp=lte:${previousBlock.timestamp.to}`)
79+
.reply(200, JSON.stringify(updatedFees));
7680
const feeHistory = await ethImpl.feeHistory(2, 'latest', [25, 75], requestDetails);
7781

7882
expect(feeHistory).to.exist;
@@ -131,7 +135,9 @@ describe('@ethFeeHistory using MirrorNode', async function () {
131135
const maxResultsCap = Number(constants.DEFAULT_FEE_HISTORY_MAX_RESULTS);
132136

133137
restMock.onGet(BLOCKS_LIMIT_ORDER_URL).reply(200, JSON.stringify({ blocks: [{ ...DEFAULT_BLOCK, number: 10 }] }));
134-
restMock.onGet(`network/fees?timestamp=lte:${DEFAULT_BLOCK.timestamp.to}`).reply(200, JSON.stringify(DEFAULT_NETWORK_FEES));
138+
restMock
139+
.onGet(`network/fees?timestamp=lte:${DEFAULT_BLOCK.timestamp.to}`)
140+
.reply(200, JSON.stringify(DEFAULT_NETWORK_FEES));
135141
Array.from(Array(11).keys()).map((blockNumber) =>
136142
restMock.onGet(`blocks/${blockNumber}`).reply(200, JSON.stringify({ ...DEFAULT_BLOCK, number: blockNumber })),
137143
);
@@ -181,7 +187,9 @@ describe('@ethFeeHistory using MirrorNode', async function () {
181187
sdkClientStub.getTinyBarGasFee.resolves(fauxGasTinyBars);
182188
restMock.onGet(BLOCKS_LIMIT_ORDER_URL).reply(200, JSON.stringify({ blocks: [latestBlock] }));
183189
restMock.onGet(`blocks/${latestBlock.number}`).reply(200, JSON.stringify(latestBlock));
184-
restMock.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`).reply(404, JSON.stringify(NOT_FOUND_RES));
190+
restMock
191+
.onGet(`network/fees?timestamp=lte:${latestBlock.timestamp.to}`)
192+
.reply(404, JSON.stringify(NOT_FOUND_RES));
185193
restMock.onGet('network/fees').reply(200, JSON.stringify(DEFAULT_NETWORK_FEES));
186194
});
187195

@@ -316,4 +324,29 @@ describe('@ethFeeHistory using MirrorNode', async function () {
316324
expect(feeHistoryUsingCache['baseFeePerGas'].length).to.eq(countBlocks + 1);
317325
});
318326
});
327+
328+
describe('eth_feeHistory with rewardPercentiles', function () {
329+
it('should execute eth_feeHistory with valid rewardPercentiles whose size is less than 100', async function () {
330+
const feeHistory = await ethImpl.feeHistory(1, 'latest', [25, 75], requestDetails);
331+
expect(feeHistory).to.exist;
332+
});
333+
334+
it('should throw INVALID_PARAMETER when rewardPercentiles size is greater than 100', async function () {
335+
const invalidSize = 101;
336+
337+
const jsonRpcError = predefined.INVALID_PARAMETER(
338+
2,
339+
`Reward percentiles size ${invalidSize} is greater than the maximum allowed size ${constants.FEE_HISTORY_REWARD_PERCENTILES_MAX_SIZE}`,
340+
);
341+
342+
await expect(
343+
ethImpl.feeHistory(
344+
1,
345+
'latest',
346+
Array.from({ length: invalidSize }, (_, i) => i),
347+
requestDetails,
348+
),
349+
).to.eventually.rejectedWith(jsonRpcError.message);
350+
});
351+
});
319352
});

packages/server/tests/acceptance/rpc_batch2.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1176,8 +1176,33 @@ describe('@api-batch-2 RPC Server Acceptance Tests', function () {
11761176
expect(res.oldestBlock).to.exist;
11771177
expect(Number(res.oldestBlock)).to.be.gt(0);
11781178
});
1179-
});
11801179

1180+
it('should call eth_feeHistory with valid rewardPercentiles whose size is less than 100', async function () {
1181+
const res = await relay.call(RelayCalls.ETH_ENDPOINTS.ETH_FEE_HISTORY, ['0x1', 'latest', [25, 75]], requestId);
1182+
expect(res.reward).to.exist.to.be.an('Array');
1183+
expect(res.reward.length).to.be.gt(0);
1184+
});
1185+
1186+
it('should fail to call eth_feeHistory with invalid rewardPercentiles whose size is greater than 100', async function () {
1187+
const invalidSize = 101;
1188+
const args = [
1189+
RelayCalls.ETH_ENDPOINTS.ETH_FEE_HISTORY,
1190+
['0x1', 'latest', Array.from({ length: invalidSize }, (_, i) => i)],
1191+
requestId,
1192+
];
1193+
1194+
await Assertions.assertPredefinedRpcError(
1195+
predefined.INVALID_PARAMETER(
1196+
2,
1197+
`Reward percentiles size ${invalidSize} is greater than the maximum allowed size ${constants.FEE_HISTORY_REWARD_PERCENTILES_MAX_SIZE}`,
1198+
),
1199+
relay.call,
1200+
true,
1201+
relay,
1202+
args,
1203+
);
1204+
});
1205+
});
11811206
describe('Formats of addresses in Transaction and Receipt results', () => {
11821207
const getTxData = async (hash) => {
11831208
const txByHash = await relay.call(RelayCalls.ETH_ENDPOINTS.ETH_GET_TRANSACTION_BY_HASH, [hash], requestId);

packages/server/tests/helpers/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ const EMPTY_HEX = '0x';
162162
const TINYBAR_TO_WEIBAR_COEF = 10_000_000_000;
163163

164164
const CALL_EXCEPTION = 'CALL_EXCEPTION';
165+
const FEE_HISTORY_REWARD_PERCENTILES_MAX_SIZE = 100;
165166

166167
export default {
167168
ETH_ENDPOINTS,
@@ -180,4 +181,5 @@ export default {
180181
ACTUAL_GAS_USED,
181182
TINYBAR_TO_WEIBAR_COEF,
182183
METRICS,
184+
FEE_HISTORY_REWARD_PERCENTILES_MAX_SIZE,
183185
};

0 commit comments

Comments
 (0)