Skip to content

Commit 38a5f77

Browse files
authored
Unit tests for HBAR Rate limit (#584)
Adds unit tests with 100% coverage for new HBAR Rate Limit functionality Signed-off-by: georgi-l95 <[email protected]>
1 parent 466adc6 commit 38a5f77

File tree

7 files changed

+258
-58
lines changed

7 files changed

+258
-58
lines changed

.github/workflows/acceptance.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,31 @@ jobs:
2121
with:
2222
testfilter: erc20
2323

24-
hbarlimiter:
25-
name: HBAR Limiter
24+
ratelimiter:
25+
name: Rate Limiter
2626
uses: ./.github/workflows/acceptance-workflow.yml
2727
needs: [ api, erc20 ]
2828
with:
29-
testfilter: hbarlimiter
29+
testfilter: ratelimiter
3030

3131
tokencreate:
3232
name: Token Create
3333
uses: ./.github/workflows/acceptance-workflow.yml
34-
needs: [ api, erc20, hbarlimiter ]
34+
needs: [ api, erc20, ratelimiter ]
3535
with:
3636
testfilter: tokencreate
3737

3838
tokenmanagement:
3939
name: Token Management
4040
uses: ./.github/workflows/acceptance-workflow.yml
41-
needs: [ api, erc20, hbarlimiter ]
41+
needs: [ api, erc20, ratelimiter ]
4242
with:
4343
testfilter: tokenmanagement
4444

4545
htsprecompilev1:
4646
name: Precompile
4747
uses: ./.github/workflows/acceptance-workflow.yml
48-
needs: [ api, erc20, hbarlimiter ]
48+
needs: [ api, erc20, ratelimiter ]
4949
with:
5050
testfilter: htsprecompilev1
5151

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"acceptancetest": "ts-mocha packages/server/tests/acceptance/index.spec.ts --exit",
2424
"acceptancetest:api": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api' --exit",
2525
"acceptancetest:erc20": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@erc20' --exit",
26-
"acceptancetest:hbarlimiter": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@hbarlimiter' --exit",
26+
"acceptancetest:ratelimiter": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@ratelimiter' --exit",
2727
"acceptancetest:tokencreate": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@tokencreate' --exit",
2828
"acceptancetest:tokenmanagement": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@tokenmanagement' --exit",
2929
"acceptancetest:htsprecompilev1": "ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@htsprecompilev1' --exit",

packages/relay/src/lib/clients/sdkClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ export class SDKClient {
128128
},
129129
});
130130

131-
this.hbarLimiter = new HbarLimit(Date.now());
131+
const duration = parseInt(process.env.HBAR_RATE_LIMIT_DURATION!);
132+
const total = parseInt(process.env.HBAR_RATE_LIMIT_TINYBAR!);
133+
this.hbarLimiter = new HbarLimit(Date.now(), total, duration);
132134
}
133135

134136
async getAccountBalance(account: string, callerName: string, requestId?: string): Promise<AccountBalance> {

packages/relay/src/lib/hbarlimiter/index.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,34 @@
1717
* limitations under the License.
1818
*
1919
*/
20-
import dotenv from 'dotenv';
21-
import findConfig from 'find-config';
2220

2321
export default class HbarLimit {
22+
private enabled: boolean = false;
2423
private remainingBudget: number;
25-
private total: number;
26-
private duration: number;
24+
private duration: number = 0;
25+
private total: number = 0;
2726
private reset: number;
2827

29-
constructor(currentDateNow: number) {
30-
dotenv.config({ path: findConfig('.env') || '' });
31-
32-
this.total = parseInt(process.env.HBAR_RATE_LIMIT_TINYBAR!);
28+
constructor(currentDateNow: number, total: number, duration: number) {
29+
this.enabled = false;
30+
31+
if (total && duration) {
32+
this.enabled = true;
33+
this.total = total;
34+
this.duration = duration;
35+
}
3336
this.remainingBudget = this.total;
34-
this.duration = parseInt(process.env.HBAR_RATE_LIMIT_DURATION!);
3537
this.reset = currentDateNow + this.duration;
3638
}
3739

3840
/**
3941
* Decides whether we should limit expenses, based on remaining budget.
4042
*/
4143
shouldLimit(currentDateNow: number): boolean {
44+
if (!this.enabled) {
45+
return false;
46+
}
47+
4248
if (this.shouldResetLimiter(currentDateNow)){
4349
this.resetLimiter(currentDateNow);
4450
}
@@ -49,12 +55,37 @@ export default class HbarLimit {
4955
* Add expense to the remaining budget.
5056
*/
5157
addExpense(cost: number, currentDateNow: number) {
58+
if (!this.enabled) {
59+
return;
60+
}
61+
5262
if (this.shouldResetLimiter(currentDateNow)){
5363
this.resetLimiter(currentDateNow);
5464
}
5565
this.remainingBudget -= cost;
5666
}
5767

68+
/**
69+
* Returns whether rate limiter is enabled or not.
70+
*/
71+
isEnabled(){
72+
return this.enabled;
73+
}
74+
75+
/**
76+
* Returns remaining budget.
77+
*/
78+
getRemainingBudget(){
79+
return this.remainingBudget;
80+
}
81+
82+
/**
83+
* Returns timestamp for the next rate limit reset.
84+
*/
85+
getResetTime(){
86+
return this.reset;
87+
}
88+
5889
/**
5990
* Decides whether it should reset budget and timer.
6091
*/
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*-
2+
*
3+
* Hedera JSON RPC Relay
4+
*
5+
* Copyright (C) 2022 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+
import { expect } from 'chai';
22+
import HbarLimit from '../../src/lib/hbarlimiter';
23+
24+
describe('HBAR Rate Limiter', async function () {
25+
this.timeout(20000);
26+
let rateLimiter: HbarLimit;
27+
let currentDateNow: number;
28+
const invalidDuration: number = 0;
29+
const invalidTotal: number = 0;
30+
const validDuration: number = 60000;
31+
const validTotal: number = 100000000;
32+
33+
this.beforeEach(() => {
34+
currentDateNow = Date.now();
35+
});
36+
37+
it('should be disabled, if we pass invalid total', async function () {
38+
rateLimiter = new HbarLimit(currentDateNow, invalidTotal, validDuration);
39+
40+
const isEnabled = rateLimiter.isEnabled();
41+
const limiterResetTime = rateLimiter.getResetTime();
42+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
43+
const shouldRateLimit = rateLimiter.shouldLimit(currentDateNow);
44+
rateLimiter.addExpense(validTotal, currentDateNow);
45+
46+
expect(isEnabled).to.equal(false);
47+
expect(shouldRateLimit).to.equal(false);
48+
expect(limiterResetTime).to.equal(currentDateNow);
49+
expect(limiterRemainingBudget).to.equal(0);
50+
});
51+
52+
it('should be disabled, if we pass invalid duration', async function () {
53+
rateLimiter = new HbarLimit(currentDateNow, validTotal, invalidDuration);
54+
55+
const isEnabled = rateLimiter.isEnabled();
56+
const limiterResetTime = rateLimiter.getResetTime();
57+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
58+
const shouldRateLimit = rateLimiter.shouldLimit(currentDateNow);
59+
rateLimiter.addExpense(validTotal, currentDateNow);
60+
61+
expect(isEnabled).to.equal(false);
62+
expect(shouldRateLimit).to.equal(false);
63+
expect(limiterResetTime).to.equal(currentDateNow);
64+
expect(limiterRemainingBudget).to.equal(0);
65+
});
66+
67+
it('should be disabled, if we pass both invalid duration and total', async function () {
68+
rateLimiter = new HbarLimit(currentDateNow, invalidTotal, invalidDuration);
69+
70+
const isEnabled = rateLimiter.isEnabled();
71+
const limiterResetTime = rateLimiter.getResetTime();
72+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
73+
const shouldRateLimit = rateLimiter.shouldLimit(currentDateNow);
74+
rateLimiter.addExpense(validTotal, currentDateNow);
75+
76+
expect(isEnabled).to.equal(false);
77+
expect(shouldRateLimit).to.equal(false);
78+
expect(limiterResetTime).to.equal(currentDateNow);
79+
expect(limiterRemainingBudget).to.equal(0);
80+
});
81+
82+
it('should be enabled, if we pass valid duration and total', async function () {
83+
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
84+
85+
const isEnabled = rateLimiter.isEnabled();
86+
const limiterResetTime = rateLimiter.getResetTime();
87+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
88+
const shouldRateLimit = rateLimiter.shouldLimit(currentDateNow);
89+
90+
expect(isEnabled).to.equal(true);
91+
expect(shouldRateLimit).to.equal(false);
92+
expect(limiterResetTime).to.equal(currentDateNow + validDuration);
93+
expect(limiterRemainingBudget).to.equal(validTotal);
94+
});
95+
96+
it('should not rate limit', async function () {
97+
const cost = 10000000;
98+
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
99+
rateLimiter.addExpense(cost, currentDateNow);
100+
101+
const isEnabled = rateLimiter.isEnabled();
102+
const limiterResetTime = rateLimiter.getResetTime();
103+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
104+
const shouldRateLimit = rateLimiter.shouldLimit(currentDateNow);
105+
106+
expect(isEnabled).to.equal(true);
107+
expect(shouldRateLimit).to.equal(false);
108+
expect(limiterResetTime).to.equal(currentDateNow + validDuration);
109+
expect(limiterRemainingBudget).to.equal(validTotal - cost);
110+
});
111+
112+
it('should rate limit', async function () {
113+
const cost = 1000000000;
114+
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
115+
rateLimiter.addExpense(cost, currentDateNow);
116+
117+
const isEnabled = rateLimiter.isEnabled();
118+
const limiterResetTime = rateLimiter.getResetTime();
119+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
120+
const shouldRateLimit = rateLimiter.shouldLimit(currentDateNow);
121+
122+
expect(isEnabled).to.equal(true);
123+
expect(shouldRateLimit).to.equal(true);
124+
expect(limiterResetTime).to.equal(currentDateNow + validDuration);
125+
expect(limiterRemainingBudget).to.equal(validTotal - cost);
126+
});
127+
128+
it('should reset budget, while checking if we should rate limit', async function () {
129+
const cost = 1000000000;
130+
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
131+
rateLimiter.addExpense(cost, currentDateNow);
132+
133+
const isEnabled = rateLimiter.isEnabled();
134+
const futureDate = currentDateNow + validDuration * 2;
135+
const shouldRateLimit = rateLimiter.shouldLimit(futureDate);
136+
const limiterResetTime = rateLimiter.getResetTime();
137+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
138+
139+
expect(isEnabled).to.equal(true);
140+
expect(shouldRateLimit).to.equal(false);
141+
expect(limiterResetTime).to.equal(futureDate + validDuration);
142+
expect(limiterRemainingBudget).to.equal(validTotal);
143+
});
144+
145+
it('should reset budget, while adding expense', async function () {
146+
const cost = 1000000000;
147+
rateLimiter = new HbarLimit(currentDateNow, validTotal, validDuration);
148+
149+
rateLimiter.addExpense(cost, currentDateNow);
150+
const shouldRateLimitBefore = rateLimiter.shouldLimit(currentDateNow);
151+
152+
const futureDate = currentDateNow + validDuration * 2;
153+
rateLimiter.addExpense(100, futureDate);
154+
const shouldRateLimitAfter = rateLimiter.shouldLimit(futureDate);
155+
156+
const isEnabled = rateLimiter.isEnabled();
157+
const limiterResetTime = rateLimiter.getResetTime();
158+
const limiterRemainingBudget = rateLimiter.getRemainingBudget();
159+
160+
expect(isEnabled).to.equal(true);
161+
expect(shouldRateLimitBefore).to.equal(true);
162+
expect(shouldRateLimitAfter).to.equal(false);
163+
expect(limiterResetTime).to.equal(futureDate + validDuration);
164+
expect(limiterRemainingBudget).to.equal(validTotal - 100);
165+
});
166+
});

packages/server/tests/acceptance/hbarLimiter.spec.ts renamed to packages/server/tests/acceptance/rateLimiter.spec.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import { ContractFunctionParameters } from '@hashgraph/sdk';
2727

2828
// local resources
2929
import parentContractJson from '../contracts/Parent.json';
30-
import { predefined } from '../../../relay/src/lib/errors/JsonRpcError';
30+
import { predefined } from '@hashgraph/json-rpc-relay/src/lib/errors/JsonRpcError';
3131

32-
describe('@hbarlimiter HBAR Limiter Acceptance Tests', function () {
32+
describe('@ratelimiter Rate Limiters Acceptance Tests', function () {
3333
this.timeout(240 * 1000); // 240 seconds
3434

3535
const accounts: AliasAccount[] = [];
@@ -45,6 +45,45 @@ describe('@hbarlimiter HBAR Limiter Acceptance Tests', function () {
4545
const CHAIN_ID = process.env.CHAIN_ID || 0;
4646
const ONE_TINYBAR = ethers.utils.parseUnits('1', 10);
4747

48+
describe('RPC Rate Limiter Acceptance Tests', () => {
49+
it('should throw rate limit exceeded error', async function() {
50+
let rateLimited = false;
51+
try{
52+
//Currently chaindId is TIER 2 request per LIMIT_DURATION from env. We are trying to get an error for rate limit by exceeding this threshold
53+
for (let index = 0; index < parseInt(process.env.TIER_2_RATE_LIMIT!) * 2; index++) {
54+
await relay.call('eth_chainId', [null]);
55+
// If we don't wait between calls, the relay can't register so many request at one time. So instead of 200 requests for example, it registers only 5.
56+
await new Promise(r => setTimeout(r, 1));
57+
}
58+
}catch(error) {
59+
rateLimited = true;
60+
Assertions.jsonRpcError(error, predefined.IP_RATE_LIMIT_EXCEEDED);
61+
}
62+
63+
expect(rateLimited).to.be.true;
64+
65+
// wait until rate limit is reset
66+
await new Promise(r => setTimeout(r, parseInt(process.env.LIMIT_DURATION!)));
67+
});
68+
69+
it('should not throw rate limit exceeded error', async function () {
70+
for (let index = 0; index < parseInt(process.env.TIER_2_RATE_LIMIT!); index++) {
71+
await relay.call('eth_chainId', [null]);
72+
// If we don't wait between calls, the relay can't register so many request at one time. So instead of 200 requests for example, it registers only 5.
73+
await new Promise(r => setTimeout(r, 1));
74+
}
75+
76+
// wait until rate limit is reset
77+
await new Promise(r => setTimeout(r, parseInt(process.env.LIMIT_DURATION!)));
78+
79+
for (let index = 0; index < parseInt(process.env.TIER_2_RATE_LIMIT!); index++) {
80+
await relay.call('eth_chainId', [null]);
81+
// If we don't wait between calls, the relay can't register so many request at one time. So instead of 200 requests for example, it registers only 5.
82+
await new Promise(r => setTimeout(r, 1));
83+
}
84+
});
85+
});
86+
4887
describe('HBAR Limiter Acceptance Tests', function () {
4988
this.timeout(240 * 1000); // 240 seconds
5089

@@ -79,6 +118,7 @@ describe('@hbarlimiter HBAR Limiter Acceptance Tests', function () {
79118
};
80119

81120
it('should fail to execute "eth_sendRawTransaction" due to HBAR rate limit exceeded ', async function () {
121+
await new Promise(r => setTimeout(r, parseInt(process.env.HBAR_RATE_LIMIT_DURATION!)));
82122
let rateLimit = false;
83123
try {
84124
for (let index = 0; index < parseInt(process.env.TIER_1_RATE_LIMIT!); index++) {

0 commit comments

Comments
 (0)