Skip to content

Commit ccec0b6

Browse files
authored
chore: cherry pick #3349 - fix: enhanced reliability of eth RPC methods with null checks and retry mechanisms to release/0.63 (#3363)
fix: enhanced reliability of eth RPC methods with null checks and retry mechanisms (#3349) * fix: fixed flaky precheck test * fix: handle log null entities more gracefully in getBlock() * fix: added null check to root hash builder * fix: modified getContractResultWithRetry() to accept more general getContract MN methods * fix: reused getContractResultWithRetry for getContractResults * fix: added getContractResultsLogsWithRetry() * fix: reverted licenses Revert "fix: reverted licenses" This reverts commit d3c860a. Reapply "fix: reverted licenses" This reverts commit 50a9acd5031490f3f805042a70bfdae7c589146d. * fix: checked empty blockHash for getHistoricalBlockResponse() * fix: throw error if log.block_number or log.index is null or log.block_hash is empty --------- Signed-off-by: Logan Nguyen <[email protected]>
1 parent 4f91480 commit ccec0b6

File tree

10 files changed

+316
-87
lines changed

10 files changed

+316
-87
lines changed

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

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* -
1+
/*-
22
*
33
* Hedera JSON RPC Relay
44
*
@@ -620,7 +620,10 @@ export class MirrorNodeClient {
620620
requestDetails,
621621
);
622622

623-
await this.cacheService.set(cachedLabel, block, MirrorNodeClient.GET_BLOCK_ENDPOINT, requestDetails);
623+
if (block) {
624+
await this.cacheService.set(cachedLabel, block, MirrorNodeClient.GET_BLOCK_ENDPOINT, requestDetails);
625+
}
626+
624627
return block;
625628
}
626629

@@ -754,21 +757,42 @@ export class MirrorNodeClient {
754757
* In some very rare cases the /contracts/results api is called before all the data is saved in
755758
* the mirror node DB and `transaction_index` or `block_number` is returned as `undefined` or `block_hash` as `0x`.
756759
* A single re-fetch is sufficient to resolve this problem.
757-
* @param {string} transactionIdOrHash - The transaction ID or hash
758-
* @param {RequestDetails} requestDetails - The request details for logging and tracking.
760+
*
761+
* @param {string} methodName - The name of the method used to fetch contract results.
762+
* @param {any[]} args - The arguments to be passed to the specified method for fetching contract results.
763+
* @param {RequestDetails} requestDetails - Details used for logging and tracking the request.
764+
* @returns {Promise<any>} - A promise resolving to the fetched contract result, either on the first attempt or after a retry.
759765
*/
760-
public async getContractResultWithRetry(transactionIdOrHash: string, requestDetails: RequestDetails) {
761-
const contractResult = await this.getContractResult(transactionIdOrHash, requestDetails);
762-
if (
763-
contractResult &&
764-
!(
765-
contractResult.transaction_index &&
766-
contractResult.block_number &&
767-
contractResult.block_hash != EthImpl.emptyHex
768-
)
769-
) {
770-
return this.getContractResult(transactionIdOrHash, requestDetails);
766+
public async getContractResultWithRetry(
767+
methodName: string,
768+
args: any[],
769+
requestDetails: RequestDetails,
770+
): Promise<any> {
771+
const shortDelay = 500;
772+
const contractResult = await this[methodName](...args);
773+
774+
if (contractResult) {
775+
const contractObjects = Array.isArray(contractResult) ? contractResult : [contractResult];
776+
for (const contractObject of contractObjects) {
777+
if (
778+
contractObject &&
779+
(contractObject.transaction_index == null ||
780+
contractObject.block_number == null ||
781+
contractObject.block_hash == EthImpl.emptyHex)
782+
) {
783+
if (this.logger.isLevelEnabled('debug')) {
784+
this.logger.debug(
785+
`${requestDetails.formattedRequestId} Contract result contains undefined transaction_index, block_number, or block_hash is an empty hex (0x): transaction_hash:${contractObject.hash}, transaction_index:${contractObject.transaction_index}, block_number=${contractObject.block_number}, block_hash=${contractObject.block_hash}. Retrying after a delay of ${shortDelay} ms `,
786+
);
787+
}
788+
789+
// Backoff before repeating request
790+
await new Promise((r) => setTimeout(r, shortDelay));
791+
return await this[methodName](...args);
792+
}
793+
}
771794
}
795+
772796
return contractResult;
773797
}
774798

@@ -870,14 +894,25 @@ export class MirrorNodeClient {
870894
return this.getQueryParams(queryParamObject);
871895
}
872896

873-
public async getContractResultsLogs(
897+
/**
898+
* In some very rare cases the /contracts/results/logs api is called before all the data is saved in
899+
* the mirror node DB and `transaction_index`, `block_number`, `index` is returned as `undefined`, or block_hash is an empty hex (0x).
900+
* A single re-fetch is sufficient to resolve this problem.
901+
*
902+
* @param {RequestDetails} requestDetails - Details used for logging and tracking the request.
903+
* @param {IContractLogsResultsParams} [contractLogsResultsParams] - Parameters for querying contract logs results.
904+
* @param {ILimitOrderParams} [limitOrderParams] - Parameters for limit and order when fetching the logs.
905+
* @returns {Promise<any[]>} - A promise resolving to the paginated contract logs results.
906+
*/
907+
public async getContractResultsLogsWithRetry(
874908
requestDetails: RequestDetails,
875909
contractLogsResultsParams?: IContractLogsResultsParams,
876910
limitOrderParams?: ILimitOrderParams,
877-
) {
911+
): Promise<any[]> {
912+
const shortDelay = 500;
878913
const queryParams = this.prepareLogsParams(contractLogsResultsParams, limitOrderParams);
879914

880-
return this.getPaginatedResults(
915+
const logResults = await this.getPaginatedResults(
881916
`${MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT}${queryParams}`,
882917
MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT,
883918
MirrorNodeClient.CONTRACT_RESULT_LOGS_PROPERTY,
@@ -886,6 +921,38 @@ export class MirrorNodeClient {
886921
1,
887922
MirrorNodeClient.mirrorNodeContractResultsLogsPageMax,
888923
);
924+
925+
if (logResults) {
926+
for (const log of logResults) {
927+
if (
928+
log &&
929+
(log.transaction_index == null ||
930+
log.block_number == null ||
931+
log.index == null ||
932+
log.block_hash === EthImpl.emptyHex)
933+
) {
934+
if (this.logger.isLevelEnabled('debug')) {
935+
this.logger.debug(
936+
`${requestDetails.formattedRequestId} Contract result log contains undefined transaction_index, block_number, index, or block_hash is an empty hex (0x): transaction_hash:${log.transaction_hash}, transaction_index:${log.transaction_index}, block_number=${log.block_number}, log_index=${log.index}, block_hash=${log.block_hash}. Retrying after a delay of ${shortDelay} ms.`,
937+
);
938+
}
939+
940+
// Backoff before repeating request
941+
await new Promise((r) => setTimeout(r, shortDelay));
942+
return await this.getPaginatedResults(
943+
`${MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT}${queryParams}`,
944+
MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT,
945+
MirrorNodeClient.CONTRACT_RESULT_LOGS_PROPERTY,
946+
requestDetails,
947+
[],
948+
1,
949+
MirrorNodeClient.mirrorNodeContractResultsLogsPageMax,
950+
);
951+
}
952+
}
953+
}
954+
955+
return logResults;
889956
}
890957

891958
public async getContractResultsLogsByAddress(

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,11 @@ export class SDKClient {
724724
);
725725
return transactionResponse;
726726
} catch (e: any) {
727+
this.logger.warn(
728+
e,
729+
`${requestDetails.formattedRequestId} Transaction failed while executing transaction via the SDK: transactionId=${transaction.transactionId}, callerName=${callerName}, txConstructorName=${txConstructorName}`,
730+
);
731+
727732
if (e instanceof JsonRpcError) {
728733
throw e;
729734
}

packages/relay/src/lib/eth.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* -
1+
/*-
22
*
33
* Hedera JSON RPC Relay
44
*
@@ -1922,13 +1922,17 @@ export class EthImpl implements Eth {
19221922
transactionIndex: string,
19231923
requestDetails: RequestDetails,
19241924
): Promise<Transaction | null> {
1925-
const contractResults = await this.mirrorNodeClient.getContractResults(
1925+
const contractResults = await this.mirrorNodeClient.getContractResultWithRetry(
1926+
this.mirrorNodeClient.getContractResults.name,
1927+
[
1928+
requestDetails,
1929+
{
1930+
[blockParam.title]: blockParam.value,
1931+
transactionIndex: Number(transactionIndex),
1932+
},
1933+
undefined,
1934+
],
19261935
requestDetails,
1927-
{
1928-
[blockParam.title]: blockParam.value,
1929-
transactionIndex: Number(transactionIndex),
1930-
},
1931-
undefined,
19321936
);
19331937

19341938
if (!contractResults[0]) return null;
@@ -2201,7 +2205,11 @@ export class EthImpl implements Eth {
22012205
this.logger.trace(`${requestIdPrefix} getTransactionByHash(hash=${hash})`, hash);
22022206
}
22032207

2204-
const contractResult = await this.mirrorNodeClient.getContractResultWithRetry(hash, requestDetails);
2208+
const contractResult = await this.mirrorNodeClient.getContractResultWithRetry(
2209+
this.mirrorNodeClient.getContractResult.name,
2210+
[hash, requestDetails],
2211+
requestDetails,
2212+
);
22052213
if (contractResult === null || contractResult.hash === undefined) {
22062214
// handle synthetic transactions
22072215
const syntheticLogs = await this.common.getLogsWithParams(
@@ -2265,7 +2273,12 @@ export class EthImpl implements Eth {
22652273
return cachedResponse;
22662274
}
22672275

2268-
const receiptResponse = await this.mirrorNodeClient.getContractResultWithRetry(hash, requestDetails);
2276+
const receiptResponse = await this.mirrorNodeClient.getContractResultWithRetry(
2277+
this.mirrorNodeClient.getContractResult.name,
2278+
[hash, requestDetails],
2279+
requestDetails,
2280+
);
2281+
22692282
if (receiptResponse === null || receiptResponse.hash === undefined) {
22702283
// handle synthetic transactions
22712284
const syntheticLogs = await this.common.getLogsWithParams(
@@ -2531,10 +2544,11 @@ export class EthImpl implements Eth {
25312544
if (blockResponse == null) return null;
25322545
const timestampRange = blockResponse.timestamp;
25332546
const timestampRangeParams = [`gte:${timestampRange.from}`, `lte:${timestampRange.to}`];
2534-
const contractResults = await this.mirrorNodeClient.getContractResults(
2547+
2548+
const contractResults = await this.mirrorNodeClient.getContractResultWithRetry(
2549+
this.mirrorNodeClient.getContractResults.name,
2550+
[requestDetails, { timestamp: timestampRangeParams }, undefined],
25352551
requestDetails,
2536-
{ timestamp: timestampRangeParams },
2537-
undefined,
25382552
);
25392553
const gasUsed = blockResponse.gas_used;
25402554
const params = { timestamp: timestampRangeParams };

packages/relay/src/lib/services/debugService/index.ts

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

21+
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
2122
import type { Logger } from 'pino';
22-
import type { MirrorNodeClient } from '../../clients';
23-
import type { IDebugService } from './IDebugService';
24-
import type { CommonService } from '../ethService';
23+
2524
import { decodeErrorMessage, mapKeysAndValues, numberTo0x, strip0x } from '../../../formatters';
25+
import type { MirrorNodeClient } from '../../clients';
26+
import { IOpcode } from '../../clients/models/IOpcode';
27+
import { IOpcodesResponse } from '../../clients/models/IOpcodesResponse';
2628
import constants, { CallType, TracerType } from '../../constants';
2729
import { predefined } from '../../errors/JsonRpcError';
2830
import { EthImpl } from '../../eth';
29-
import { IOpcodesResponse } from '../../clients/models/IOpcodesResponse';
30-
import { IOpcode } from '../../clients/models/IOpcode';
3131
import { ICallTracerConfig, IOpcodeLoggerConfig, ITracerConfig, RequestDetails } from '../../types';
32-
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
32+
import type { CommonService } from '../ethService';
33+
import type { IDebugService } from './IDebugService';
3334

3435
/**
3536
* Represents a DebugService for tracing and debugging transactions and debugging
@@ -300,7 +301,11 @@ export class DebugService implements IDebugService {
300301
try {
301302
const [actionsResponse, transactionsResponse] = await Promise.all([
302303
this.mirrorNodeClient.getContractsResultsActions(transactionHash, requestDetails),
303-
this.mirrorNodeClient.getContractResultWithRetry(transactionHash, requestDetails),
304+
this.mirrorNodeClient.getContractResultWithRetry(
305+
this.mirrorNodeClient.getContractResult.name,
306+
[transactionHash, requestDetails],
307+
requestDetails,
308+
),
304309
]);
305310
if (!actionsResponse || !transactionsResponse) {
306311
throw predefined.RESOURCE_NOT_FOUND(`Failed to retrieve contract results for transaction ${transactionHash}`);

packages/relay/src/lib/services/ethService/ethCommonService/index.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,21 @@
1818
*
1919
*/
2020

21-
import constants from '../../../constants';
22-
import { JsonRpcError, predefined } from '../../../errors/JsonRpcError';
23-
import { ICommonService } from './ICommonService';
21+
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
22+
import * as _ from 'lodash';
2423
import { Logger } from 'pino';
25-
import { MirrorNodeClient } from '../../../clients';
24+
2625
import { nullableNumberTo0x, numberTo0x, parseNumericEnvVar, toHash32 } from '../../../../formatters';
27-
import { SDKClientError } from '../../../errors/SDKClientError';
26+
import { MirrorNodeClient } from '../../../clients';
27+
import constants from '../../../constants';
28+
import { JsonRpcError, predefined } from '../../../errors/JsonRpcError';
2829
import { MirrorNodeClientError } from '../../../errors/MirrorNodeClientError';
30+
import { SDKClientError } from '../../../errors/SDKClientError';
31+
import { EthImpl } from '../../../eth';
2932
import { Log } from '../../../model';
30-
import * as _ from 'lodash';
31-
import { CacheService } from '../../cacheService/cacheService';
32-
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
3333
import { RequestDetails } from '../../../types';
34+
import { CacheService } from '../../cacheService/cacheService';
35+
import { ICommonService } from './ICommonService';
3436

3537
/**
3638
* Create a new Common Service implementation.
@@ -175,6 +177,20 @@ export class CommonService implements ICommonService {
175177
returnLatest?: boolean,
176178
): Promise<any> {
177179
if (!returnLatest && this.blockTagIsLatestOrPending(blockNumberOrTagOrHash)) {
180+
if (this.logger.isLevelEnabled('debug')) {
181+
this.logger.debug(
182+
`${requestDetails.formattedRequestId} Detected a contradiction between blockNumberOrTagOrHash and returnLatest. The request does not target the latest block, yet blockNumberOrTagOrHash representing latest or pending: returnLatest=${returnLatest}, blockNumberOrTagOrHash=${blockNumberOrTagOrHash}`,
183+
);
184+
}
185+
return null;
186+
}
187+
188+
if (blockNumberOrTagOrHash === EthImpl.emptyHex) {
189+
if (this.logger.isLevelEnabled('debug')) {
190+
this.logger.debug(
191+
`${requestDetails.formattedRequestId} Invalid input detected in getHistoricalBlockResponse(): blockNumberOrTagOrHash=${blockNumberOrTagOrHash}.`,
192+
);
193+
}
178194
return null;
179195
}
180196

@@ -321,7 +337,7 @@ export class CommonService implements ICommonService {
321337
if (address) {
322338
logResults = await this.getLogsByAddress(address, params, requestDetails);
323339
} else {
324-
logResults = await this.mirrorNodeClient.getContractResultsLogs(requestDetails, params);
340+
logResults = await this.mirrorNodeClient.getContractResultsLogsWithRetry(requestDetails, params);
325341
}
326342

327343
if (!logResults) {
@@ -330,13 +346,28 @@ export class CommonService implements ICommonService {
330346

331347
const logs: Log[] = [];
332348
for (const log of logResults) {
349+
if (log.block_number == null || log.index == null || log.block_hash === EthImpl.emptyHex) {
350+
if (this.logger.isLevelEnabled('debug')) {
351+
this.logger.debug(
352+
`${
353+
requestDetails.formattedRequestId
354+
} Log entry is missing required fields: block_number, index, or block_hash is an empty hex (0x). log=${JSON.stringify(
355+
log,
356+
)}`,
357+
);
358+
}
359+
throw predefined.INTERNAL_ERROR(
360+
`The log entry from the remote Mirror Node server is missing required fields. `,
361+
);
362+
}
363+
333364
logs.push(
334365
new Log({
335366
address: log.address,
336367
blockHash: toHash32(log.block_hash),
337368
blockNumber: numberTo0x(log.block_number),
338369
data: log.data,
339-
logIndex: nullableNumberTo0x(log.index),
370+
logIndex: numberTo0x(log.index),
340371
removed: false,
341372
topics: log.topics,
342373
transactionHash: toHash32(log.transaction_hash),

packages/relay/src/receiptsRootUtils.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
import { RLP } from '@ethereumjs/rlp';
2222
import { Trie } from '@ethereumjs/trie';
2323
import { bytesToInt, concatBytes, hexToBytes, intToBytes, intToHex } from '@ethereumjs/util';
24-
import { EthImpl } from './lib/eth';
24+
2525
import { prepend0x } from './formatters';
26+
import { EthImpl } from './lib/eth';
2627
import { Log } from './lib/model';
2728
import { LogsBloomUtils } from './logsBloomUtils';
2829

@@ -93,16 +94,29 @@ export class ReceiptsRootUtils {
9394
public static buildReceiptRootHashes(txHashes: string[], contractResults: any[], logs: Log[]): IReceiptRootHash[] {
9495
const receipts: IReceiptRootHash[] = [];
9596

96-
for (let i in txHashes) {
97+
for (const i in txHashes) {
9798
const txHash: string = txHashes[i];
9899
const logsPerTx: Log[] = logs.filter((log) => log.transactionHash == txHash);
99100
const crPerTx: any[] = contractResults.filter((cr) => cr.hash == txHash);
101+
102+
// Determine the transaction index for the current transaction hash:
103+
// - Prefer the `transaction_index` from the contract results (`crPerTx`) if available.
104+
// - Fallback to the `transactionIndex` from logs (`logsPerTx`) if no valid `transaction_index` is found in `crPerTx`.
105+
// - If neither source provides a valid value, `transactionIndex` remains `null`.
106+
let transactionIndex: any = null;
107+
if (crPerTx.length && crPerTx[0].transaction_index != null) {
108+
transactionIndex = intToHex(crPerTx[0].transaction_index);
109+
} else if (logsPerTx.length) {
110+
transactionIndex = logsPerTx[0].transactionIndex;
111+
}
112+
100113
receipts.push({
101-
transactionIndex: crPerTx.length ? intToHex(crPerTx[0].transaction_index) : logsPerTx[0].transactionIndex,
114+
transactionIndex,
102115
type: crPerTx.length && crPerTx[0].type ? intToHex(crPerTx[0].type) : null,
103116
root: crPerTx.length ? crPerTx[0].root : EthImpl.zeroHex32Byte,
104117
status: crPerTx.length ? crPerTx[0].status : EthImpl.oneHex,
105-
cumulativeGasUsed: crPerTx.length ? intToHex(crPerTx[0].block_gas_used) : EthImpl.zeroHex,
118+
cumulativeGasUsed:
119+
crPerTx.length && crPerTx[0].block_gas_used ? intToHex(crPerTx[0].block_gas_used) : EthImpl.zeroHex,
106120
logsBloom: crPerTx.length
107121
? crPerTx[0].bloom
108122
: LogsBloomUtils.buildLogsBloom(logs[0].address, logsPerTx[0].topics),

0 commit comments

Comments
 (0)