Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface IRegularTransactionReceiptParams {
logs: Log[];
receiptResponse: any;
to: string | null;
blockGasUsedBeforeTransaction: number;
}

/**
Expand Down Expand Up @@ -88,7 +89,7 @@ class TransactionReceiptFactory {
* @returns {ITransactionReceipt} Transaction receipt for the regular transaction
*/
public static createRegularReceipt(params: IRegularTransactionReceiptParams): ITransactionReceipt {
const { receiptResponse, effectiveGas, from, logs } = params;
const { receiptResponse, effectiveGas, from, logs, blockGasUsedBeforeTransaction } = params;
let { to } = params;

// Determine contract address if it exists
Expand All @@ -104,7 +105,7 @@ class TransactionReceiptFactory {
blockNumber: numberTo0x(receiptResponse.block_number),
from: from,
to: to,
cumulativeGasUsed: numberTo0x(receiptResponse.block_gas_used),
cumulativeGasUsed: numberTo0x(blockGasUsedBeforeTransaction + receiptResponse.gas_used),
gasUsed: nanOrNumberTo0x(receiptResponse.gas_used),
contractAddress: contractAddress,
logs: logs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,27 +132,50 @@ function populateSyntheticTransactions(
}

function buildReceiptRootHashes(txHashes: string[], contractResults: any[], logs: Log[]): IReceiptRootHash[] {
const receipts: IReceiptRootHash[] = [];

for (const i in txHashes) {
const txHash: string = txHashes[i];
const logsPerTx: Log[] = logs.filter((log) => log.transactionHash == txHash);
const crPerTx: any[] = contractResults.filter((cr) => cr.hash == txHash);

let transactionIndex: any = null;
const items: {
transactionIndex: number;
logsPerTx: Log[];
crPerTx: any[];
}[] = [];

for (const txHash of txHashes) {
const logsPerTx: Log[] = logs.filter((log) => log.transactionHash === txHash);
const crPerTx: any[] = contractResults.filter((cr) => cr.hash === txHash);
Comment on lines +142 to +143
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Don't we really know what is the type of crPerTx?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be able to type it. Maybe we can create another issue to fix it everywhere in the codebase?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it’s not that important, it just crossed my mind while I was checking the PR. We can address it in a separate issue, we already have this problem in the code.


// Derive numeric transaction index (for ordering)
let txIndexNum: number = 0;
if (crPerTx.length && crPerTx[0].transaction_index != null) {
transactionIndex = intToHex(crPerTx[0].transaction_index);
txIndexNum = crPerTx[0].transaction_index;
} else if (logsPerTx.length) {
transactionIndex = logsPerTx[0].transactionIndex;
txIndexNum = parseInt(logsPerTx[0].transactionIndex, 16);
}

items.push({
transactionIndex: txIndexNum,
logsPerTx,
crPerTx,
});
}

// Sort by transaction index = block order
items.sort((a, b) => a.transactionIndex - b.transactionIndex);

const receipts: IReceiptRootHash[] = [];
let cumulativeGas = 0;

for (const item of items) {
const { transactionIndex, logsPerTx, crPerTx } = item;

const gasUsed = crPerTx[0]?.gas_used ?? 0;
cumulativeGas += gasUsed;
const transactionIndexHex = intToHex(transactionIndex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still might have -1 here...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use 0 as a fallback value. Do you see any risks in doing that?


receipts.push({
transactionIndex,
transactionIndex: transactionIndexHex,
type: crPerTx.length && crPerTx[0].type ? intToHex(crPerTx[0].type) : null,
root: crPerTx.length ? crPerTx[0].root : constants.ZERO_HEX_32_BYTE,
status: crPerTx.length ? crPerTx[0].status : constants.ONE_HEX,
cumulativeGasUsed:
crPerTx.length && crPerTx[0].block_gas_used ? intToHex(crPerTx[0].block_gas_used) : constants.ZERO_HEX,
cumulativeGasUsed: intToHex(cumulativeGas),
logsBloom: crPerTx.length
? crPerTx[0].bloom
: LogsBloomUtils.buildLogsBloom(logs[0].address, logsPerTx[0].topics),
Expand Down Expand Up @@ -372,7 +395,15 @@ export async function getBlockReceipts(
logsByHash.set(log.transactionHash, existingLogs);
}

const receiptPromises = contractResults.map(async (contractResult) => {
// Ensure contract results are processed in transaction_index (block) order
const sortedContractResults = [...contractResults].sort((a, b) => {
const aIdx = a.transaction_index ?? Number.MAX_SAFE_INTEGER;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should we sort by eventually? In previous method we used:

crPerTx[0].transaction_index

but when it was missing we checked:

logsPerTx[0].transactionIndex

now we are taking only cr, right?

Since we are fetching them all with a single request woulnd't it be posisble to fget them fetched from mirrorndoe right away, instead of sorting them on our own?

Aren;'t they, by any chance already sorted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case, we should first create all receipts, both regular and synthetic, and then do another loop to calculate cumulativeGasUsed for each.

Synthetic receipts have 0 gasUsed, but it doesn't imply they should have 0 as cumulativeGasUsed. Please let me know what you think about this approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I get it now, synthetics dont ever influnce this value since they always use 0 gas.In this case I don't think we should take them into consideration here.

BUT we need to set this cumulativeGasPrice for the synthetic receipts generated here:
https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4936/changes/BASE..586dec6a6ba4dd355f07abae9d2425755e928503#diff-b8e1972336c5bbc108e68c01afa6bf2999c059bf3ea9ee67642d8a906f57a45eR444

as well. Which we aren't doing now, right?

const bIdx = b.transaction_index ?? Number.MAX_SAFE_INTEGER;
return aIdx - bIdx;
});

let blockGasUsedBeforeTransaction = 0;
const receiptPromises = sortedContractResults.map(async (contractResult) => {
if (Utils.isRejectedDueToHederaSpecificValidation(contractResult)) {
logger.debug(
`Transaction with hash %s is skipped due to hedera-specific validation failure (%s)`,
Expand All @@ -394,12 +425,16 @@ export async function getBlockReceipts(
logs: contractResult.logs,
receiptResponse: contractResult,
to,
blockGasUsedBeforeTransaction,
};
return TransactionReceiptFactory.createRegularReceipt(transactionReceiptParams) as ITransactionReceipt;

const receipt = TransactionReceiptFactory.createRegularReceipt(transactionReceiptParams) as ITransactionReceipt;
blockGasUsedBeforeTransaction += contractResult.gas_used;
return receipt;
});

const resolvedReceipts = await Promise.all(receiptPromises);
receipts.push(...resolvedReceipts.filter(Boolean));
receipts.push(...resolvedReceipts.filter((r): r is ITransactionReceipt => r !== null));

const regularTxHashes = new Set(contractResults.map((result) => result.hash));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import {
} from '../../../factories/transactionReceiptFactory';
import { Log, Transaction } from '../../../model';
import { Precheck } from '../../../precheck';
import { ITransactionReceipt, LockAcquisitionResult, RequestDetails, TypedEvents } from '../../../types';
import {
IContractResultsParams,
ITransactionReceipt,
LockAcquisitionResult,
RequestDetails,
TypedEvents,
} from '../../../types';
import HAPIService from '../../hapiService/hapiService';
import { ICommonService, LockService, TransactionPoolService } from '../../index';
import { ITransactionService } from './ITransactionService';
Expand Down Expand Up @@ -487,12 +493,39 @@ export class TransactionService implements ITransactionService {
this.common.resolveEvmAddress(receiptResponse.to, requestDetails),
]);

let blockGasUsedBeforeTransaction = 0;
if (receiptResponse.transaction_index > 0) {
const params: IContractResultsParams = {
blockNumber: receiptResponse.block_number,
};

const blockContractResults = await this.mirrorNodeClient.getContractResults(requestDetails, params);
Comment on lines +498 to +502
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I really don’t like the idea of fetching heavy block data for every transaction just to retrieve this single value :/ . But I don't know what to do about that, I really think this should be just calcualted in the mirrornode.

q:
Is there any correlation between the transaction index and the timestamp? Since this is the Hashgraph overall, consensus timestamps should matter for ordering, right? Maybe it applies to the on-block orders and we can fetch only the transactions with a timestamp lower than or equal to the one we’re querying for? (or would there be a problem with internal transactions for examlple?)

Also, we’re ignoring the fallback tx index from logs and only using the one from contract results. Wont that create differences for a single transaction when comparing it across these different endpoints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.hedera.com/api-reference/contracts/list-contract-results-from-all-contracts-on-the-network#parameter-transaction-index - there is an option to even query by transaction index x >= 0. A thing to discuss is, if we want to ask mirror node team to calculate this value on their side, or we do it ourselves and optimize what we can

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I didn’t anticipate all of these implications when we were preparing the initial issue. What I do know is that fetching the block can be a very heavy operation (and I think it happens far less frequently than fetching a transaction receipt).
Including this could have significant performance implications with potentially limited benefit. I think we should all consider the options:

  1. Ignore this value for this particular endpoint - worst solution imo, since it would make the same transaction look different depending on where it’s queried.
  2. Accept the performance impact (investigate how significant it actually is) and proceed with the full implementation. We should also try to filter the results as much as possible. If filtering by transaction index works (assuming it’s the same transaction.index we’re referring to: I’m not entirely sure, as it sometimes appears to come from logs) or timestamps, we should do it deffinitely.
  3. (Keep the current “fake” value for cumulativeGasUsed - but clearly document that behavior and do this operation on the MN side.)


if (Array.isArray(blockContractResults)) {
for (const cr of blockContractResults) {
if (Utils.isRejectedDueToHederaSpecificValidation(cr)) {
continue;
}

if (cr.transaction_index == null || cr.gas_used == null) {
continue;
}

// Only sum gas for transactions that come before this one in the block
if (cr.transaction_index < receiptResponse.transaction_index) {
blockGasUsedBeforeTransaction += cr.gas_used;
}
}
}
}

return TransactionReceiptFactory.createRegularReceipt({
effectiveGas,
from,
logs,
receiptResponse,
to,
blockGasUsedBeforeTransaction,
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/relay/tests/lib/eth/eth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,5 +817,5 @@ export const DETAILD_CONTRACT_RESULT_NOT_FOUND = {
export const EMPTY_RES = {
results: [],
};
export const DEFAULT_BLOCK_RECEIPTS_ROOT_HASH = '0xc9854d764adf76676b7a2b04f36a865ba50ec6cad6807a31188d65693cdc187d';
export const DEFAULT_BLOCK_RECEIPTS_ROOT_HASH = '0x41542e0787e2d9004e4ef54714c7b0fbf800c0ca921fb7a06917eeda28c3abf5';
//
35 changes: 24 additions & 11 deletions packages/relay/tests/lib/eth/eth_getBlockReceipts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MirrorNodeClient, SDKClient } from '../../../src/lib/clients';
import type { ICacheClient } from '../../../src/lib/clients/cache/ICacheClient';
import { EthImpl } from '../../../src/lib/eth';
import HAPIService from '../../../src/lib/services/hapiService/hapiService';
import { RequestDetails } from '../../../src/lib/types';
import { ITransactionReceipt, RequestDetails } from '../../../src/lib/types';
import { defaultContractResults, defaultContractResultsOnlyHash2, defaultLogs1, mockWorkersPool } from '../../helpers';
import {
BLOCK_HASH,
Expand Down Expand Up @@ -91,11 +91,16 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () {
});
}

function expectValidReceipt(receipt, contractResult) {
function expectValidReceipt(receipt, contractResult, blockGasUsedBeforeTransaction: number) {
expect(receipt.blockHash).to.equal(BLOCK_HASH_TRIMMED);
expect(receipt.blockNumber).to.equal(BLOCK_NUMBER_HEX);
expect(receipt.transactionHash).to.equal(contractResult.hash);
expect(receipt.gasUsed).to.equal(numberTo0x(contractResult.gas_used));
expect(receipt.cumulativeGasUsed).to.equal(numberTo0x(blockGasUsedBeforeTransaction + contractResult.gas_used));
}

function sortReceiptsByTransactionIndex(receipts: ITransactionReceipt[]): ITransactionReceipt[] {
return receipts?.sort((a, b) => Number(a.transactionIndex) - Number(b.transactionIndex));
}

describe('Success cases', () => {
Expand All @@ -106,9 +111,11 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () {
expect(receipts).to.exist;
expect(receipts.length).to.equal(2);

receipts.forEach((receipt, index) => {
let blockGasUsedBeforeTransaction = 0;
sortReceiptsByTransactionIndex(receipts!).forEach((receipt, index) => {
const contractResult = results[index];
expectValidReceipt(receipt, contractResult);
expectValidReceipt(receipt, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
});
});

Expand All @@ -119,9 +126,11 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () {
expect(receipts).to.exist;
expect(receipts.length).to.equal(2);

receipts.forEach((receipt, index) => {
let blockGasUsedBeforeTransaction = 0;
sortReceiptsByTransactionIndex(receipts!).forEach((receipt, index) => {
const contractResult = results[index];
expectValidReceipt(receipt, contractResult);
expectValidReceipt(receipt, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
});
});

Expand All @@ -132,9 +141,11 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () {
expect(receipts).to.exist;
expect(receipts.length).to.equal(2);

receipts.forEach((receipt, index) => {
let blockGasUsedBeforeTransaction = 0;
sortReceiptsByTransactionIndex(receipts!).forEach((receipt, index) => {
const contractResult = results[index];
expectValidReceipt(receipt, contractResult);
expectValidReceipt(receipt, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
});
});

Expand All @@ -147,9 +158,11 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () {
expect(receipts).to.exist;
expect(receipts.length).to.equal(2);

receipts.forEach((receipt, index) => {
let blockGasUsedBeforeTransaction = 0;
sortReceiptsByTransactionIndex(receipts!).forEach((receipt, index) => {
const contractResult = results[index];
expectValidReceipt(receipt, contractResult);
expectValidReceipt(receipt, contractResult, blockGasUsedBeforeTransaction);
blockGasUsedBeforeTransaction += contractResult.gas_used;
});
});

Expand All @@ -174,7 +187,7 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () {
expect(receipts.length).to.equal(1);
expect(receipts[0].transactionHash).to.equal(results[0].hash);

expectValidReceipt(receipts[0], results[0]);
expectValidReceipt(receipts[0], results[0], 0);
});
});

Expand Down
71 changes: 66 additions & 5 deletions packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import constants from '../../../src/lib/constants';
import { RequestDetails } from '../../../src/lib/types';
import RelayAssertions from '../../assertions';
import { defaultErrorMessageHex } from '../../helpers';
import { DEFAULT_BLOCK, EMPTY_LOGS_RESPONSE } from './eth-config';
import { BLOCK_HASH, BLOCK_NUMBER, DEFAULT_BLOCK, EMPTY_LOGS_RESPONSE, GAS_USED_1, GAS_USED_2 } from './eth-config';
import { generateEthTestEnv } from './eth-helpers';

use(chaiAsPromised);
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func
},
],
result: 'SUCCESS',
transaction_index: 1,
transaction_index: 0,
hash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392',
state_changes: [
{
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func
const defaultReceipt = {
blockHash: '0xd693b532a80fed6392b428604171fb32fdbf953728a3a7ecc7d4062b1652c042',
blockNumber: '0x11',
cumulativeGasUsed: '0x2faf080',
cumulativeGasUsed: '0x7b', //assuming this is the first transaction in the block
effectiveGasPrice: '0xad78ebc5ac620000',
from: '0x0000000000000000000000000000000000001f41',
to: '0x0000000000000000000000000000000000001389',
Expand All @@ -101,13 +101,13 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func
removed: false,
topics: ['0x97c1fc0a6ed5551bc831571325e9bdb365d06803100dc20648640ba24ce69750'],
transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392',
transactionIndex: '0x1',
transactionIndex: '0x0',
},
],
logsBloom: emptyBloom,
status: '0x1',
transactionHash: '0x4a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6392',
transactionIndex: '0x1',
transactionIndex: '0x0',
contractAddress: '0xd8db0b1dbf8ba6721ef5256ad5fe07d72d1d04b9',
root: undefined,
};
Expand Down Expand Up @@ -372,4 +372,65 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func
expect(receipt).to.exist;
expect(receipt?.to).to.be.null;
});

it('should handle cumulative gas used for receipt with multiple transactions in the block', async function () {
const tx1GasUsed = GAS_USED_1;
const tx2GasUsed = GAS_USED_2;
const blockGasUsed = tx1GasUsed + tx2GasUsed;

const secondTxHash = '0xbcfc47c474ebcf39f71f47414713325b37b81df00b5d0eed6703dd7bf6a80a7e';

const secondTxContractResult = {
...defaultDetailedContractResultByHash,
hash: secondTxHash,
block_hash: BLOCK_HASH,
block_number: BLOCK_NUMBER,
gas_used: tx2GasUsed,
block_gas_used: blockGasUsed,
transaction_index: 1,
};

restMock.onGet(`accounts/${secondTxContractResult.from}?transactions=false`).reply(200);
restMock.onGet(`accounts/${secondTxContractResult.from}?transactions=false`).reply(200);
restMock.onGet(`accounts/${secondTxContractResult.to}?transactions=false`).reply(200);
restMock.onGet(`accounts/${secondTxContractResult.to}?transactions=false`).reply(200);
restMock.onGet(`contracts/${secondTxContractResult.to}`).reply(200);
restMock.onGet(`contracts/${secondTxContractResult.to}`).reply(200);
restMock.onGet(`tokens/${secondTxContractResult.contract_id}`).reply(200);
restMock.onGet(`tokens/${secondTxContractResult.contract_id}`).reply(200);
restMock.onGet(`contracts/${secondTxContractResult.created_contract_ids[0]}`).reply(404);

stubBlockAndFeesFunc(sandbox);

restMock.onGet(`contracts/results/${secondTxHash}`).reply(200, JSON.stringify(secondTxContractResult));

restMock.onGet(`contracts/results?block.number=${BLOCK_NUMBER}&limit=100&order=asc`).reply(
200,
JSON.stringify({
results: [
{
...defaultDetailedContractResultByHash,
block_hash: BLOCK_HASH,
block_number: BLOCK_NUMBER,
block_gas_used: blockGasUsed,
gas_used: tx1GasUsed,
transaction_index: 0,
},
secondTxContractResult, // tx2
],
links: { next: null },
}),
);

const receipt = await ethImpl.getTransactionReceipt(secondTxHash, requestDetails);

expect(receipt).to.exist;
if (!receipt) return;

const expectedGasUsedHex = '0x' + tx2GasUsed.toString(16);
expect(receipt.gasUsed).to.equal(expectedGasUsedHex);

const expectedCumulativeHex = '0x' + blockGasUsed.toString(16);
expect(receipt.cumulativeGasUsed).to.equal(expectedCumulativeHex);
});
});
Loading
Loading