Skip to content

Commit e80299b

Browse files
authored
chore: cherry pick #3368 fix: added polling logic to ensure the retrieval of fully mature records from MN (#3370)
fix: added polling logic to ensure the retrieval of fully mature records from MN (#3368) * fix: added polling logic to getContractResultWithRetry() and getContractResultsLogsWithRetry() * fix: strictly throw errors if immature records found --------- Signed-off-by: Logan Nguyen <[email protected]>
1 parent 593a47f commit e80299b

File tree

7 files changed

+320
-133
lines changed

7 files changed

+320
-133
lines changed

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

Lines changed: 114 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -754,45 +754,75 @@ export class MirrorNodeClient {
754754
}
755755

756756
/**
757-
* In some very rare cases the /contracts/results api is called before all the data is saved in
758-
* the mirror node DB and `transaction_index` or `block_number` is returned as `undefined` or `block_hash` as `0x`.
759-
* A single re-fetch is sufficient to resolve this problem.
757+
* Retrieves contract results with a retry mechanism to handle immature records.
758+
* When querying the /contracts/results api, there are cases where the records are "immature" - meaning
759+
* some fields are not yet properly populated in the mirror node DB at the time of the request.
760+
*
761+
* An immature record can be characterized by:
762+
* - `transaction_index` being null/undefined
763+
* - `block_number` being null/undefined
764+
* - `block_hash` being '0x' (empty hex)
765+
*
766+
* This method implements a retry mechanism to handle immature records by polling until either:
767+
* - The record matures (all fields are properly populated)
768+
* - The maximum retry count is reached
760769
*
761770
* @param {string} methodName - The name of the method used to fetch contract results.
762771
* @param {any[]} args - The arguments to be passed to the specified method for fetching contract results.
763772
* @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.
773+
* @returns {Promise<any>} - A promise resolving to the fetched contract result, either mature or the last fetched result after retries.
765774
*/
766775
public async getContractResultWithRetry(
767776
methodName: string,
768777
args: any[],
769778
requestDetails: RequestDetails,
770779
): 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-
);
780+
const mirrorNodeRetryDelay = this.getMirrorNodeRetryDelay();
781+
const mirrorNodeRequestRetryCount = this.getMirrorNodeRequestRetryCount();
782+
783+
let contractResult = await this[methodName](...args);
784+
785+
for (let i = 0; i < mirrorNodeRequestRetryCount; i++) {
786+
if (contractResult) {
787+
const contractObjects = Array.isArray(contractResult) ? contractResult : [contractResult];
788+
789+
let foundImmatureRecord = false;
790+
791+
for (const contractObject of contractObjects) {
792+
if (
793+
contractObject &&
794+
(contractObject.transaction_index == null ||
795+
contractObject.block_number == null ||
796+
contractObject.block_hash == EthImpl.emptyHex)
797+
) {
798+
// Found immature record, log the info, set flag and exit record traversal
799+
if (this.logger.isLevelEnabled('debug')) {
800+
this.logger.debug(
801+
`${
802+
requestDetails.formattedRequestId
803+
} Contract result contains nullable transaction_index or block_number, or block_hash is an empty hex (0x): contract_result=${JSON.stringify(
804+
contractObject,
805+
)}. Retrying after a delay of ${mirrorNodeRetryDelay} ms `,
806+
);
807+
}
808+
809+
foundImmatureRecord = true;
810+
break;
787811
}
788-
789-
// Backoff before repeating request
790-
await new Promise((r) => setTimeout(r, shortDelay));
791-
return await this[methodName](...args);
792812
}
813+
814+
// if foundImmatureRecord is still false after record traversal, it means no immature record was found. Simply return contractResult to stop the polling process
815+
if (!foundImmatureRecord) return contractResult;
816+
817+
// if immature record found, wait and retry and update contractResult
818+
await new Promise((r) => setTimeout(r, mirrorNodeRetryDelay));
819+
contractResult = await this[methodName](...args);
820+
} else {
821+
break;
793822
}
794823
}
795824

825+
// Return final result after all retry attempts, regardless of record maturity
796826
return contractResult;
797827
}
798828

@@ -895,24 +925,36 @@ export class MirrorNodeClient {
895925
}
896926

897927
/**
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.
928+
* Retrieves contract results log with a retry mechanism to handle immature records.
929+
* When querying the /contracts/results/logs api, there are cases where the records are "immature" - meaning
930+
* some fields are not yet properly populated in the mirror node DB at the time of the request.
931+
*
932+
* An immature record can be characterized by:
933+
* - `transaction_index` being null/undefined
934+
* - `log index` being null/undefined
935+
* - `block_number` being null/undefined
936+
* - `block_hash` being '0x' (empty hex)
937+
*
938+
* This method implements a retry mechanism to handle immature records by polling until either:
939+
* - The record matures (all fields are properly populated)
940+
* - The maximum retry count is reached
901941
*
902942
* @param {RequestDetails} requestDetails - Details used for logging and tracking the request.
903943
* @param {IContractLogsResultsParams} [contractLogsResultsParams] - Parameters for querying contract logs results.
904944
* @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.
945+
* @returns {Promise<any[]>} - A promise resolving to the paginated contract logs results, either mature or the last fetched result after retries.
906946
*/
907947
public async getContractResultsLogsWithRetry(
908948
requestDetails: RequestDetails,
909949
contractLogsResultsParams?: IContractLogsResultsParams,
910950
limitOrderParams?: ILimitOrderParams,
911951
): Promise<any[]> {
912-
const shortDelay = 500;
952+
const mirrorNodeRetryDelay = this.getMirrorNodeRetryDelay();
953+
const mirrorNodeRequestRetryCount = this.getMirrorNodeRequestRetryCount();
954+
913955
const queryParams = this.prepareLogsParams(contractLogsResultsParams, limitOrderParams);
914956

915-
const logResults = await this.getPaginatedResults(
957+
let logResults = await this.getPaginatedResults(
916958
`${MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT}${queryParams}`,
917959
MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT,
918960
MirrorNodeClient.CONTRACT_RESULT_LOGS_PROPERTY,
@@ -922,33 +964,50 @@ export class MirrorNodeClient {
922964
MirrorNodeClient.mirrorNodeContractResultsLogsPageMax,
923965
);
924966

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-
);
967+
for (let i = 0; i < mirrorNodeRequestRetryCount; i++) {
968+
if (logResults) {
969+
let foundImmatureRecord = false;
970+
971+
for (const log of logResults) {
972+
if (
973+
log &&
974+
(log.transaction_index == null ||
975+
log.block_number == null ||
976+
log.index == null ||
977+
log.block_hash === EthImpl.emptyHex)
978+
) {
979+
// Found immature record, log the info, set flag and exit record traversal
980+
if (this.logger.isLevelEnabled('debug')) {
981+
this.logger.debug(
982+
`${
983+
requestDetails.formattedRequestId
984+
} Contract result log contains undefined transaction_index, block_number, index, or block_hash is an empty hex (0x): log=${JSON.stringify(
985+
log,
986+
)}. Retrying after a delay of ${mirrorNodeRetryDelay} ms.`,
987+
);
988+
}
989+
990+
foundImmatureRecord = true;
991+
break;
938992
}
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-
);
951993
}
994+
995+
// if foundImmatureRecord is still false after record traversal, it means no immature record was found. Simply return logResults to stop the polling process
996+
if (!foundImmatureRecord) return logResults;
997+
998+
// if immature record found, wait and retry and update logResults
999+
await new Promise((r) => setTimeout(r, mirrorNodeRetryDelay));
1000+
logResults = await this.getPaginatedResults(
1001+
`${MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT}${queryParams}`,
1002+
MirrorNodeClient.GET_CONTRACT_RESULT_LOGS_ENDPOINT,
1003+
MirrorNodeClient.CONTRACT_RESULT_LOGS_PROPERTY,
1004+
requestDetails,
1005+
[],
1006+
1,
1007+
MirrorNodeClient.mirrorNodeContractResultsLogsPageMax,
1008+
);
1009+
} else {
1010+
break;
9521011
}
9531012
}
9541013

packages/relay/src/lib/eth.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,6 +1937,8 @@ export class EthImpl implements Eth {
19371937

19381938
if (!contractResults[0]) return null;
19391939

1940+
this.handleImmatureContractResultRecord(contractResults[0], requestDetails);
1941+
19401942
const resolvedToAddress = await this.resolveEvmAddress(contractResults[0].to, requestDetails);
19411943
const resolvedFromAddress = await this.resolveEvmAddress(contractResults[0].from, requestDetails, [
19421944
constants.TYPE_ACCOUNT,
@@ -2231,11 +2233,7 @@ export class EthImpl implements Eth {
22312233
return this.createTransactionFromLog(syntheticLogs[0]);
22322234
}
22332235

2234-
if (!contractResult.block_number || (!contractResult.transaction_index && contractResult.transaction_index !== 0)) {
2235-
this.logger.warn(
2236-
`${requestIdPrefix} getTransactionByHash(hash=${hash}) mirror-node returned status 200 with missing properties in contract_results - block_number==${contractResult.block_number} and transaction_index==${contractResult.transaction_index}`,
2237-
);
2238-
}
2236+
this.handleImmatureContractResultRecord(contractResult, requestDetails);
22392237

22402238
const fromAddress = await this.resolveEvmAddress(contractResult.from, requestDetails, [constants.TYPE_ACCOUNT]);
22412239
const toAddress = await this.resolveEvmAddress(contractResult.to, requestDetails);
@@ -2329,6 +2327,8 @@ export class EthImpl implements Eth {
23292327
);
23302328
return receipt;
23312329
} else {
2330+
this.handleImmatureContractResultRecord(receiptResponse, requestDetails);
2331+
23322332
const effectiveGas = await this.getCurrentGasPriceForBlock(receiptResponse.block_hash, requestDetails);
23332333
// support stricter go-eth client which requires the transaction hash property on logs
23342334
const logs = receiptResponse.logs.map((log) => {
@@ -2341,7 +2341,7 @@ export class EthImpl implements Eth {
23412341
removed: false,
23422342
topics: log.topics,
23432343
transactionHash: toHash32(receiptResponse.hash),
2344-
transactionIndex: nullableNumberTo0x(receiptResponse.transaction_index),
2344+
transactionIndex: numberTo0x(receiptResponse.transaction_index),
23452345
});
23462346
});
23472347

@@ -2357,7 +2357,7 @@ export class EthImpl implements Eth {
23572357
logs: logs,
23582358
logsBloom: receiptResponse.bloom === EthImpl.emptyHex ? EthImpl.emptyBloom : receiptResponse.bloom,
23592359
transactionHash: toHash32(receiptResponse.hash),
2360-
transactionIndex: nullableNumberTo0x(receiptResponse.transaction_index),
2360+
transactionIndex: numberTo0x(receiptResponse.transaction_index),
23612361
effectiveGasPrice: effectiveGas,
23622362
root: receiptResponse.root || constants.DEFAULT_ROOT_HASH,
23632363
status: receiptResponse.status,
@@ -2570,6 +2570,8 @@ export class EthImpl implements Eth {
25702570
// prepare transactionArray
25712571
let transactionArray: any[] = [];
25722572
for (const contractResult of contractResults) {
2573+
this.handleImmatureContractResultRecord(contractResult, requestDetails);
2574+
25732575
// there are several hedera-specific validations that occur right before entering the evm
25742576
// if a transaction has reverted there, we should not include that tx in the block response
25752577
if (Utils.isRevertedDueToHederaSpecificValidation(contractResult)) {
@@ -2839,4 +2841,32 @@ export class EthImpl implements Eth {
28392841
const exchangeRateInCents = currentNetworkExchangeRate.cent_equivalent / currentNetworkExchangeRate.hbar_equivalent;
28402842
return exchangeRateInCents;
28412843
}
2844+
2845+
/**
2846+
* Checks if a contract result record is immature by validating required fields.
2847+
* An immature record can be characterized by:
2848+
* - `transaction_index` being null/undefined
2849+
* - `block_number` being null/undefined
2850+
* - `block_hash` being '0x' (empty hex)
2851+
*
2852+
* @param {any} record - The contract result record to validate
2853+
* @param {RequestDetails} requestDetails - Details used for logging and tracking the request
2854+
* @throws {Error} If the record is missing required fields
2855+
*/
2856+
private handleImmatureContractResultRecord(record: any, requestDetails: RequestDetails) {
2857+
if (record.transaction_index == null || record.block_number == null || record.block_hash === EthImpl.emptyHex) {
2858+
if (this.logger.isLevelEnabled('debug')) {
2859+
this.logger.debug(
2860+
`${
2861+
requestDetails.formattedRequestId
2862+
} Contract result is missing required fields: block_number, transaction_index, or block_hash is an empty hex (0x). contractResult=${JSON.stringify(
2863+
record,
2864+
)}`,
2865+
);
2866+
}
2867+
throw predefined.INTERNAL_ERROR(
2868+
`The contract result response from the remote Mirror Node server is missing required fields. `,
2869+
);
2870+
}
2871+
}
28422872
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,12 @@ export class CommonService implements ICommonService {
346346

347347
const logs: Log[] = [];
348348
for (const log of logResults) {
349-
if (log.block_number == null || log.index == null || log.block_hash === EthImpl.emptyHex) {
349+
if (
350+
log.transaction_index == null ||
351+
log.block_number == null ||
352+
log.index == null ||
353+
log.block_hash === EthImpl.emptyHex
354+
) {
350355
if (this.logger.isLevelEnabled('debug')) {
351356
this.logger.debug(
352357
`${
@@ -371,7 +376,7 @@ export class CommonService implements ICommonService {
371376
removed: false,
372377
topics: log.topics,
373378
transactionHash: toHash32(log.transaction_hash),
374-
transactionIndex: nullableNumberTo0x(log.transaction_index),
379+
transactionIndex: numberTo0x(log.transaction_index),
375380
}),
376381
);
377382
}

0 commit comments

Comments
 (0)