diff --git a/.env.example b/.env.example index 384335f735..45f2c61935 100755 --- a/.env.example +++ b/.env.example @@ -39,3 +39,5 @@ WS_MAX_CONNECTION_TTL= WS_CONNECTION_LIMIT_PER_IP= WS_SUBSCRIPTION_LIMIT= WS_MULTIPLE_ADDRESSES_ENABLED= +ETH_BLOCK_NUMBER_CACHE_TTL_MS= +ETH_GET_BALANCE_CACHE_TTL_MS= diff --git a/docs/configuration.md b/docs/configuration.md index 3f786e5341..a9f41f342e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,6 +49,7 @@ Unless you need to set a non-default value, it is recommended to only populate o | `DEFAULT_RATE_LIMIT` | "200" | default fallback rate limit, if no other is configured. | | `ETH_CALL_CACHE_TTL` | "200" | Maximum time in ms to cache an eth_call response. | | `ETH_BLOCK_NUMBER_CACHE_TTL_MS` | "1000" | Time in ms to cache response from mirror node | +| `ETH_GET_BALANCE_CACHE_TTL_MS` | "1000" | Time in ms to cache balance returned | | `ETH_CALL_DEFAULT_TO_CONSENSUS_NODE ` | "false" | Flag to set if eth_call logic should first query the mirror node. | | `ETH_GET_LOGS_BLOCK_RANGE_LIMIT` | "1000" | The maximum block number range to consider during an eth_getLogs call. | | `FEE_HISTORY_MAX_RESULTS` | "10" | The maximum number of results to returns as part of `eth_feeHistory`. | diff --git a/k6/.envexample b/k6/.envexample index aecd1b816b..ec7e397dc7 100644 --- a/k6/.envexample +++ b/k6/.envexample @@ -10,4 +10,3 @@ DEFAULT_GRACEFUL_STOP= FILTER_TEST= DEBUG_MODE= SIGNED_TXS= -ETH_BLOCK_NUMBER_CACHE_TTL_MS= \ No newline at end of file diff --git a/packages/relay/src/lib/constants.ts b/packages/relay/src/lib/constants.ts index 89c3177828..e321c5cf17 100644 --- a/packages/relay/src/lib/constants.ts +++ b/packages/relay/src/lib/constants.ts @@ -23,6 +23,7 @@ enum CACHE_KEY { FEE_HISTORY = 'fee_history', GET_CONTRACT_RESULT = 'getContractResult', ETH_BLOCK_NUMBER = 'eth_block_number', + ETH_GET_BALANCE = 'eth_get_balance', } enum CACHE_TTL { diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 1f59e1446c..2052d2caf4 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -67,6 +67,7 @@ export class EthImpl implements Eth { static invalidEVMInstruction = '0xfe'; static ethCallCacheTtl = process.env.ETH_CALL_CACHE_TTL || 200; static ethBlockNumberCacheTtlMs = process.env.ETH_BLOCK_NUMBER_CACHE_TTL_MS || 1000; + static ethGetBalanceCacheTtlMs = process.env.ETH_GET_BALANCE_CACHE_TTL_MS || 1000; // endpoint metric callerNames static ethCall = 'eth_call'; @@ -309,6 +310,7 @@ export class EthImpl implements Eth { const blockNumberCached = this.cache.get(cacheKey); if(blockNumberCached) { + this.logger.trace(`${requestIdPrefix} returning cached value ${cacheKey}:${JSON.stringify(blockNumberCached)}`); return blockNumberCached; } @@ -318,6 +320,7 @@ export class EthImpl implements Eth { const currentBlock = EthImpl.numberTo0x(blocks[0].number); // save the latest block number in cache this.cache.set(cacheKey, currentBlock, { ttl: EthImpl.ethBlockNumberCacheTtlMs }); + this.logger.trace(`${requestIdPrefix} caching ${cacheKey}:${JSON.stringify(currentBlock)} for ${EthImpl.ethBlockNumberCacheTtlMs} ms`); return currentBlock; } @@ -577,6 +580,15 @@ export class EthImpl implements Eth { } } + // check cache first + // create a key for the cache + const cacheKey = `${constants.CACHE_KEY.ETH_GET_BALANCE}-${account}-${blockNumberOrTag}`; + const cachedBalance = this.cache.get(cacheKey); + if (cachedBalance) { + this.logger.trace(`${requestIdPrefix} returning cached value ${cacheKey}:${JSON.stringify(cachedBalance)}`); + return cachedBalance; + } + let blockNumber = null; let balanceFound = false; let weibars: BigInt = BigInt(0); @@ -649,6 +661,10 @@ export class EthImpl implements Eth { return EthImpl.zeroHex; } + // save in cache the current balance for the account and blockNumberOrTag + this.cache.set(cacheKey, EthImpl.numberTo0x(weibars), {ttl: EthImpl.ethGetBalanceCacheTtlMs}); + this.logger.trace(`${requestIdPrefix} caching ${cacheKey}:${JSON.stringify(cachedBalance)} for ${EthImpl.ethGetBalanceCacheTtlMs} ms`); + return EthImpl.numberTo0x(weibars); } catch (error: any) { throw this.genericErrorHandler(error, `${requestIdPrefix} Error raised during getBalance for account ${account}`); diff --git a/packages/relay/tests/lib/eth.spec.ts b/packages/relay/tests/lib/eth.spec.ts index a30f2f11f6..89ae8a2514 100644 --- a/packages/relay/tests/lib/eth.spec.ts +++ b/packages/relay/tests/lib/eth.spec.ts @@ -1318,6 +1318,44 @@ describe('Eth calls using MirrorNode', async function () { expect(resBalance).to.equal(defHexBalance); }); + it('should return balance for latest block from cache', async () => { + restMock.onGet(`blocks?limit=1&order=desc`).reply(200, { + blocks: [{ + number: 10000 + }] + }); + restMock.onGet(`accounts/${contractAddress1}`).reply(200, { + account: contractAddress1, + balance: { + balance: defBalance + } + }); + + const resBalance = await ethImpl.getBalance(contractAddress1, null); + expect(resBalance).to.equal(defHexBalance); + + // next call should use cache + restMock.onGet(`accounts/${contractAddress1}`).reply(404, {}); + + const resBalanceCached = await ethImpl.getBalance(contractAddress1, null); + expect(resBalanceCached).to.equal(resBalance); + + // Third call should return new number using mirror node + const newBalance = 55555; + const newBalanceHex = EthImpl.numberTo0x(BigInt(newBalance) * TINYBAR_TO_WEIBAR_COEF_BIGINT); + restMock.onGet(`accounts/${contractAddress1}`).reply(200, { + account: contractAddress1, + balance: { + balance: newBalance + } + }); + // expire cache, instead of waiting for ttl we clear it to simulate expiry faster. + cache.clear(); + + const resBalanceNew = await ethImpl.getBalance(contractAddress1, null); + expect(newBalanceHex).to.equal(resBalanceNew); + }); + it('should return balance from mirror node with block number passed as param the same as latest', async () => { const blockNumber = "0x2710"; restMock.onGet(`blocks?limit=1&order=desc`).reply(200, { @@ -1394,7 +1432,7 @@ describe('Eth calls using MirrorNode', async function () { const resCached = await ethImpl.getBalance(contractAddress1, null); expect(resNoCache).to.equal(defHexBalance); - expect(resCached).to.equal(EthImpl.zeroHex); + expect(resCached).to.equal(defHexBalance); }); describe('with blockNumberOrTag filter', async function() {