Skip to content

Commit 0d2d0f3

Browse files
Cherry Pick Switch eth_call to Mirror-node (#1019)
Switch eth_call to Mirror Node (#1005) - Refactor of eth_call with added condition for using only consensus node for calls or mirror node and fallback to consensus node. - Helm charts set for different env. - Fixes erc20 tests as some of the tests actions need more time propagated to the mirror node. --------- Signed-off-by: Ivo Yankov <[email protected]> Signed-off-by: georgi-l95 <[email protected]> Co-authored-by: Ivo Yankov <[email protected]>
1 parent 6bd8ba6 commit 0d2d0f3

File tree

22 files changed

+1664
-72
lines changed

22 files changed

+1664
-72
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ GAS_PRICE_TINY_BAR_BUFFER =
2424
MIRROR_NODE_LIMIT_PARAM =
2525
CLIENT_TRANSPORT_SECURITY=
2626
INPUT_SIZE_LIMIT=
27-
ETH_CALL_CONSENSUS=
27+
ETH_CALL_DEFAULT_TO_CONSENSUS_NODE=
2828
CONSENSUS_MAX_EXECUTION_TIME=
2929
ETH_CALL_CACHE_TTL=
3030
SDK_REQUEST_TIMEOUT=

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
4848
| `CONSENSUS_MAX_EXECUTION_TIME` | "15000" | Maximum time in ms the SDK will wait when submitting a transaction/query before throwing a TIMEOUT error. |
4949
| `DEFAULT_RATE_LIMIT` | "200" | default fallback rate limit, if no other is configured. |
5050
| `ETH_CALL_CACHE_TTL` | "200" | Maximum time in ms to cache an eth_call response. |
51-
| `ETH_CALL_CONSENSUS` | "false" | Flag to set if eth_call logic should first query the mirror node. |
51+
| `ETH_CALL_DEFAULT_TO_CONSENSUS_NODE ` | "false" | Flag to set if eth_call logic should first query the mirror node. |
5252
| `ETH_GET_LOGS_BLOCK_RANGE_LIMIT` | "1000" | The maximum block number range to consider during an eth_getLogs call. |
5353
| `FEE_HISTORY_MAX_RESULTS` | "10" | The maximum number of results to returns as part of `eth_feeHistory`. |
5454
| `GAS_PRICE_TINY_BAR_BUFFER` | "10000000000" | The additional buffer range to allow during a relay precheck of gas price. This supports slight fluctuations in network gasprice calculations. |

helm-chart/templates/configmap.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ data:
3535
ETH_CALL_CACHE_TTL: {{ .Values.config.ETH_CALL_CACHE_TTL | quote }}
3636
CONSENSUS_MAX_EXECUTION_TIME: {{ .Values.config.CONSENSUS_MAX_EXECUTION_TIME | quote }}
3737
SUBSCRIPTIONS_ENABLED: {{ .Values.config.SUBSCRIPTIONS_ENABLED | quote }}
38+
ETH_CALL_DEFAULT_TO_CONSENSUS_NODE: {{ .Values.config.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE | quote }}

helm-chart/value-test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ config:
124124
CONSENSUS_MAX_EXECUTION_TIME: 15000
125125
SDK_REQUEST_TIMEOUT: 10000
126126
SUBSCRIPTIONS_ENABLED: false
127+
ETH_CALL_DEFAULT_TO_CONSENSUS_NODE: false
127128

128129
# Enable rolling_restarts if SDK calls fail this is usually due to stale connections that get cycled on restart
129130
rolling_restart:

helm-chart/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ config:
125125
CONSENSUS_MAX_EXECUTION_TIME: 15000
126126
SDK_REQUEST_TIMEOUT: 10000
127127
SUBSCRIPTIONS_ENABLED: false
128+
ETH_CALL_DEFAULT_TO_CONSENSUS_NODE: true
128129

129130
# Enable rolling_restarts if SDK calls fail this is usually due to stale connections that get cycled on restart
130131
rolling_restart:

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import constants from './../constants';
2525
import { Histogram, Registry } from 'prom-client';
2626
import { formatRequestIdMessage } from '../../formatters';
2727
import axiosRetry from 'axios-retry';
28+
import { predefined } from "../errors/JsonRpcError";
2829
const LRU = require('lru-cache');
2930

3031
type REQUEST_METHODS = 'GET' | 'POST';
@@ -148,7 +149,7 @@ export class MirrorNodeClient {
148149
this.web3Url = '';
149150

150151
this.restClient = restClient;
151-
this.web3Client = !!web3Client ? web3Client : restClient;
152+
this.web3Client = web3Client ? web3Client : restClient;
152153
} else {
153154
this.restUrl = this.buildUrl(restUrl);
154155
this.web3Url = this.buildUrl(web3Url);
@@ -238,7 +239,15 @@ export class MirrorNodeClient {
238239
}
239240

240241
this.logger.error(new Error(error.message), `${requestIdPrefix} [${method}] ${path} ${effectiveStatusCode} status`);
241-
throw new MirrorNodeClientError(error.message, effectiveStatusCode);
242+
243+
const mirrorError = new MirrorNodeClientError(error, effectiveStatusCode);
244+
245+
// we only need contract revert errors here as it's not the same as not supported
246+
if (mirrorError.isContractReverted() && !mirrorError.isNotSupported() && !mirrorError.isNotSupportedSystemContractOperaton()) {
247+
throw predefined.CONTRACT_REVERT(mirrorError.errorMessage);
248+
}
249+
250+
throw mirrorError;
242251
}
243252

244253
async getPaginatedResults(url: string, pathLabel: string, resultProperty: string, allowedErrorStatuses?: number[], requestId?: string, results = [], page = 1) {
@@ -528,7 +537,7 @@ export class MirrorNodeClient {
528537
}
529538

530539
public async postContractCall(callData: string, requestId?: string) {
531-
return this.post(MirrorNodeClient.CONTRACT_CALL_ENDPOINT, callData, MirrorNodeClient.CONTRACT_CALL_ENDPOINT, [400], requestId);
540+
return this.post(MirrorNodeClient.CONTRACT_CALL_ENDPOINT, callData, MirrorNodeClient.CONTRACT_CALL_ENDPOINT, [], requestId);
532541
}
533542

534543
getQueryParams(params: object) {

packages/relay/src/lib/errors/MirrorNodeClientError.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,54 @@
2121

2222
export class MirrorNodeClientError extends Error {
2323
public statusCode: number;
24+
public errorMessage?: string;
2425

2526
static retryErrorCodes: Array<number> = [400, 404, 408, 425, 500]
2627

2728
static ErrorCodes = {
2829
ECONNABORTED: 504,
29-
CONTRACT_REVERT_EXECUTED : 400
30+
CONTRACT_REVERT_EXECUTED : 400,
31+
NOT_SUPPORTED: 501
3032
};
3133

3234
static statusCodes = {
3335
NOT_FOUND: 404
3436
};
3537

36-
constructor(message: string, statusCode: number) {
37-
super(message);
38-
this.statusCode = statusCode;
38+
constructor(error: any, statusCode: number) {
39+
// web3 module sends errors in this format, this is why we need a check to distinguish
40+
if (error.response?.data?._status?.messages?.length) {
41+
const msg = error.response.data._status.messages[0];
42+
const {message, data} = msg;
43+
super(message);
3944

40-
Object.setPrototypeOf(this, MirrorNodeClientError.prototype);
45+
this.errorMessage = data;
46+
}
47+
else {
48+
super(error.message);
49+
}
50+
51+
this.statusCode = statusCode;
52+
Object.setPrototypeOf(this, MirrorNodeClientError.prototype);
4153
}
4254

4355
public isTimeout(): boolean {
4456
return this.statusCode === MirrorNodeClientError.ErrorCodes.ECONNABORTED;
4557
}
4658

59+
public isContractReverted(): boolean {
60+
return this.statusCode === MirrorNodeClientError.ErrorCodes.CONTRACT_REVERT_EXECUTED;
61+
}
62+
4763
public isNotFound(): boolean {
4864
return this.statusCode === MirrorNodeClientError.statusCodes.NOT_FOUND;
4965
}
66+
67+
public isNotSupported(): boolean {
68+
return this.statusCode === MirrorNodeClientError.ErrorCodes.NOT_SUPPORTED;
69+
}
70+
71+
public isNotSupportedSystemContractOperaton(): boolean {
72+
return this.message === 'Precompile not supported';
73+
}
5074
}

packages/relay/src/lib/eth.ts

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -974,48 +974,31 @@ export class EthImpl implements Eth {
974974
const requestIdPrefix = formatRequestIdMessage(requestId);
975975
this.logger.trace(`${requestIdPrefix} call(hash=${JSON.stringify(call)}, blockParam=${blockParam})`, call, blockParam);
976976

977-
// The "to" address must always be 42 chars.
978-
if (!call.to || call.to.length != 42) {
979-
throw predefined.INVALID_CONTRACT_ADDRESS(call.to);
980-
}
977+
await this.performCallChecks(call, requestId);
981978

982-
// If "From" is distinct from blank, we check is a valid account
983-
if(call.from) {
984-
const fromEntityType = await this.mirrorNodeClient.resolveEntityType(call.from, requestId, [constants.TYPE_ACCOUNT]);
985-
if (fromEntityType?.type !== constants.TYPE_ACCOUNT) {
986-
throw predefined.NON_EXISTING_ACCOUNT(call.from);
987-
}
988-
}
989-
// Check "To" is a valid Contract or HTS Address
990-
const toEntityType = await this.mirrorNodeClient.resolveEntityType(call.to, requestId, [constants.TYPE_TOKEN, constants.TYPE_CONTRACT]);
991-
if(!(toEntityType?.type === constants.TYPE_CONTRACT || toEntityType?.type === constants.TYPE_TOKEN)) {
992-
throw predefined.NON_EXISTING_CONTRACT(call.to);
979+
// Get a reasonable value for "gas" if it is not specified.
980+
let gas = Number(call.gas) || 400_000;
981+
982+
let value: string | null = null;
983+
if (typeof call.value === 'string') {
984+
value = (new BN(call.value)).toString();
993985
}
994986

987+
// Gas limit for `eth_call` is 50_000_000, but the current Hedera network limit is 15_000_000
988+
// With values over the gas limit, the call will fail with BUSY error so we cap it at 15_000_000
989+
if (gas > constants.BLOCK_GAS_LIMIT) {
990+
this.logger.trace(`${requestIdPrefix} eth_call gas amount (${gas}) exceeds network limit, capping gas to ${constants.BLOCK_GAS_LIMIT}`);
991+
gas = constants.BLOCK_GAS_LIMIT;
992+
}
993+
995994
try {
996-
// Get a reasonable value for "gas" if it is not specified.
997-
let gas = Number(call.gas) || 400_000;
998-
999-
let value: string | null = null;
1000-
if (typeof call.value === 'string') {
1001-
value = (new BN(call.value)).toString();
1002-
}
1003-
1004-
// Gas limit for `eth_call` is 50_000_000, but the current Hedera network limit is 15_000_000
1005-
// With values over the gas limit, the call will fail with BUSY error so we cap it at 15_000_000
1006-
if (gas > constants.BLOCK_GAS_LIMIT) {
1007-
this.logger.trace(`${requestIdPrefix} eth_call gas amount (${gas}) exceeds network limit, capping gas to ${constants.BLOCK_GAS_LIMIT}`);
1008-
gas = constants.BLOCK_GAS_LIMIT;
1009-
}
1010-
1011-
// Execute the call and get the response
1012-
this.logger.debug(`${requestIdPrefix} Making eth_call on contract ${call.to} with gas ${gas} and call data "${call.data}" from "${call.from}"`, call.to, gas, call.data, call.from);
1013-
1014-
// ETH_CALL_CONSENSUS = false enables the use of Mirror node
1015-
if (process.env.ETH_CALL_CONSENSUS == 'false') {
995+
// ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = false enables the use of Mirror node
996+
if (process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE == 'false') {
1016997
//temporary workaround until precompiles are implemented in Mirror node evm module
1017998
const isHts = await this.mirrorNodeClient.resolveEntityType(call.to, requestId, [constants.TYPE_TOKEN]);
1018999
if (!(isHts?.type === constants.TYPE_TOKEN)) {
1000+
// Execute the call and get the response
1001+
this.logger.debug(`${requestIdPrefix} Making eth_call on contract ${call.to} with gas ${gas} and call data "${call.data}" from "${call.from}" using mirror-node.`, call.to, gas, call.data, call.from);
10191002
const callData = {
10201003
...call,
10211004
gas,
@@ -1029,7 +1012,36 @@ export class EthImpl implements Eth {
10291012
return EthImpl.emptyHex;
10301013
}
10311014
}
1015+
1016+
return await this.callConsensusNode(call, gas, requestId);
1017+
} catch (e: any) {
1018+
// Temporary workaround until mirror node web3 module implements the support of precompiles
1019+
// If mirror node throws NOT_SUPPORTED or precompile is not supported, rerun eth_call and force it to go through the Consensus network
1020+
if (e instanceof MirrorNodeClientError && (e.isNotSupported() || e.isNotSupportedSystemContractOperaton())) {
1021+
return await this.callConsensusNode(call, gas, requestId);
1022+
}
1023+
1024+
this.logger.error(e, `${requestIdPrefix} Failed to successfully submit contractCallQuery`);
1025+
if (e instanceof JsonRpcError) {
1026+
return e;
1027+
}
1028+
return predefined.INTERNAL_ERROR();
1029+
}
1030+
}
10321031

1032+
/**
1033+
* Execute a contract call query to the consensus node
1034+
*
1035+
* @param call
1036+
* @param gas
1037+
* @param requestId
1038+
*/
1039+
async callConsensusNode(call: any, gas: number, requestId?: string): Promise<string | JsonRpcError> {
1040+
const requestIdPrefix = formatRequestIdMessage(requestId);
1041+
// Execute the call and get the response
1042+
this.logger.debug(`${requestIdPrefix} Making eth_call on contract ${call.to} with gas ${gas} and call data "${call.data}" from "${call.from}" using consensus-node.`, call.to, gas, call.data, call.from);
1043+
1044+
try {
10331045
let data = call.data;
10341046
if (data) {
10351047
data = crypto.createHash('sha1').update(call.data).digest('hex'); // NOSONAR
@@ -1048,7 +1060,6 @@ export class EthImpl implements Eth {
10481060

10491061
this.cache.set(cacheKey, formattedCallReponse, { ttl: EthImpl.ethCallCacheTtl });
10501062
return formattedCallReponse;
1051-
10521063
} catch (e: any) {
10531064
this.logger.error(e, `${requestIdPrefix} Failed to successfully submit contractCallQuery`);
10541065
if (e instanceof JsonRpcError) {
@@ -1058,6 +1069,32 @@ export class EthImpl implements Eth {
10581069
}
10591070
}
10601071

1072+
/**
1073+
* Perform neccecery checks for the passed call object
1074+
*
1075+
* @param call
1076+
* @param requestId
1077+
*/
1078+
async performCallChecks(call: any, requestId?: string) {
1079+
// The "to" address must always be 42 chars.
1080+
if (!call.to || call.to.length != 42) {
1081+
throw predefined.INVALID_CONTRACT_ADDRESS(call.to);
1082+
}
1083+
1084+
// If "From" is distinct from blank, we check is a valid account
1085+
if(call.from) {
1086+
const fromEntityType = await this.mirrorNodeClient.resolveEntityType(call.from, requestId, [constants.TYPE_ACCOUNT]);
1087+
if (fromEntityType?.type !== constants.TYPE_ACCOUNT) {
1088+
throw predefined.NON_EXISTING_ACCOUNT(call.from);
1089+
}
1090+
}
1091+
// Check "To" is a valid Contract or HTS Address
1092+
const toEntityType = await this.mirrorNodeClient.resolveEntityType(call.to, requestId, [constants.TYPE_TOKEN, constants.TYPE_CONTRACT]);
1093+
if(!(toEntityType?.type === constants.TYPE_CONTRACT || toEntityType?.type === constants.TYPE_TOKEN)) {
1094+
throw predefined.NON_EXISTING_CONTRACT(call.to);
1095+
}
1096+
}
1097+
10611098
/**
10621099
* Gets a transaction by the provided hash
10631100
*

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,12 +2371,12 @@ describe('Eth calls using MirrorNode', async function () {
23712371
let initialEthCallConesneusFF;
23722372

23732373
before(() => {
2374-
initialEthCallConesneusFF = process.env.ETH_CALL_CONSENSUS;
2375-
process.env.ETH_CALL_CONSENSUS = 'true';
2374+
initialEthCallConesneusFF = process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE;
2375+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = 'true';
23762376
});
23772377

23782378
after(() => {
2379-
process.env.ETH_CALL_CONSENSUS = initialEthCallConesneusFF;
2379+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = initialEthCallConesneusFF;
23802380
});
23812381

23822382
it('eth_call with no gas', async function () {
@@ -2567,12 +2567,12 @@ describe('Eth calls using MirrorNode', async function () {
25672567
let initialEthCallConesneusFF;
25682568

25692569
before(() => {
2570-
initialEthCallConesneusFF = process.env.ETH_CALL_CONSENSUS;
2571-
process.env.ETH_CALL_CONSENSUS = 'false';
2570+
initialEthCallConesneusFF = process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE;
2571+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = 'false';
25722572
});
25732573

25742574
after(() => {
2575-
process.env.ETH_CALL_CONSENSUS = initialEthCallConesneusFF;
2575+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = initialEthCallConesneusFF;
25762576
});
25772577

25782578
//temporary workaround until precompiles are implemented in Mirror node evm module
@@ -2631,6 +2631,38 @@ describe('Eth calls using MirrorNode', async function () {
26312631
expect(result).to.equal("0x00");
26322632
});
26332633

2634+
it('eth_call with all fields, but mirror node throws NOT_SUPPORTED', async function () {
2635+
const callData = {
2636+
...defaultCallData,
2637+
"from": accountAddress1,
2638+
"to": contractAddress2,
2639+
"data": contractCallData,
2640+
"gas": maxGasLimit
2641+
};
2642+
2643+
web3Mock.onPost('contracts/call', {...callData, estimate: false}).reply(501, {
2644+
'_status': {
2645+
'messages': [
2646+
{
2647+
'message': 'Precompile not supported'
2648+
}
2649+
]
2650+
}
2651+
});
2652+
2653+
sdkClientStub.submitContractCallQuery.returns({
2654+
asBytes: function () {
2655+
return Uint8Array.of(0);
2656+
}
2657+
}
2658+
);
2659+
2660+
const result = await ethImpl.call(callData, 'latest');
2661+
2662+
sinon.assert.calledWith(sdkClientStub.submitContractCallQuery, contractAddress2, contractCallData, maxGasLimit, accountAddress1, 'eth_call');
2663+
expect(result).to.equal("0x00");
2664+
});
2665+
26342666
it('caps gas at 15_000_000', async function () {
26352667
const callData = {
26362668
...defaultCallData,

packages/relay/tests/lib/openrpc.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ describe("Open RPC Specification", function () {
179179
});
180180

181181
it('should execute "eth_call" against mirror node', async function () {
182-
let initialEthCallConesneusFF = process.env.ETH_CALL_CONSENSUS;
183-
process.env.ETH_CALL_CONSENSUS = 'false';
182+
let initialEthCallConesneusFF = process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE;
183+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = 'false';
184184
mock.onGet(`contracts/${defaultCallData.from}`).reply(404);
185185
mock.onGet(`accounts/${defaultCallData.from}`).reply(200, {
186186
account: "0.0.1723",
@@ -190,12 +190,12 @@ describe("Open RPC Specification", function () {
190190

191191
const response = await ethImpl.call({...defaultCallData, gas: `0x${defaultCallData.gas.toString(16)}`}, 'latest');
192192
validateResponseSchema(methodsResponseSchema.eth_call, response);
193-
process.env.ETH_CALL_CONSENSUS = initialEthCallConesneusFF;
193+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = initialEthCallConesneusFF;
194194
});
195195

196196
it('should execute "eth_call" against consensus node', async function () {
197-
let initialEthCallConesneusFF = process.env.ETH_CALL_CONSENSUS;
198-
process.env.ETH_CALL_CONSENSUS = 'true';
197+
let initialEthCallConesneusFF = process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE;
198+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = 'true';
199199
mock.onGet(`contracts/${defaultCallData.from}`).reply(404);
200200
mock.onGet(`accounts/${defaultCallData.from}`).reply(200, {
201201
account: "0.0.1723",
@@ -205,7 +205,7 @@ describe("Open RPC Specification", function () {
205205

206206
sdkClientStub.submitContractCallQuery.returns({ asBytes: () => Buffer.from('12') });
207207
const response = await ethImpl.call(defaultTransaction, 'latest');
208-
process.env.ETH_CALL_CONSENSUS = initialEthCallConesneusFF;
208+
process.env.ETH_CALL_DEFAULT_TO_CONSENSUS_NODE = initialEthCallConesneusFF;
209209
});
210210

211211
it('should execute "eth_chainId"', function () {

0 commit comments

Comments
 (0)