Skip to content

feat: add debug_getRawReceipts method#4919

Open
BartoszSolkaBD wants to merge 16 commits intohiero-ledger:mainfrom
BartoszSolkaBD:4892-Add-debug_getRawReceipts-method
Open

feat: add debug_getRawReceipts method#4919
BartoszSolkaBD wants to merge 16 commits intohiero-ledger:mainfrom
BartoszSolkaBD:4892-Add-debug_getRawReceipts-method

Conversation

@BartoszSolkaBD
Copy link
Contributor

@BartoszSolkaBD BartoszSolkaBD commented Feb 13, 2026

Description

This PR adds support for the debug_getRawReceipts JSON-RPC method. It returns an array of EIP-2718 binary-encoded transaction receipts for a given block.

Changes:

  • Relay: New getRawReceipts(blockNumber, requestDetails) on the Debug interface.
  • Receipt encoding: New packages/relay/src/lib/receiptSerialization.ts that encodes a single receipt to EIP-2718 hex (Yellow Paper 4-field RLP: postState/root, cumulativeGasUsed, logsBloom, logs; optional type-byte prefix for typed transactions).
  • Docs: debug_getRawReceipts added to docs/openrpc.json and docs/rpc-api.md.
  • Tests:
    • Unit (packages/relay/tests/lib/debug.spec.ts): empty array when block not found, empty array when block has no receipts, multiple receipts, single receipt with logs; UNSUPPORTED_METHOD when debug API is disabled; invalid block number → INVALID_PARAMETER.
    • Acceptance (packages/server/tests/acceptance/debug.spec.ts): happy path for a block with transactions (array of hex strings), invalid block number.
    • Conformity: Overwrite packages/server/tests/acceptance/data/conformity/overwrites/debug_getRawReceipts/get-block-n.io.
    • debug_getRawReceipts.spec.ts - round-trip decode assertions (RLP decode and compare to stubbed receipt fields)

Related issue(s)

Fixes #4892

Testing Guide

  1. Run the relay against mainnet
    Start the JSON-RPC relay with mainnet config (and DEBUG_API_ENABLED=true if required). Ensure it is connected and serving requests.
  2. Call debug_getRawReceipts for block with non-empty receipts.
   {"jsonrpc":"2.0","id":1,"method":"debug_getRawReceipts","params":["0x56e86ab"]}
  1. Validate against eth_getBlockReceipts for the same block
   {"jsonrpc":"2.0","id":1,"method":"eth_getBlockReceipts","params":["0x56e86ab"]}

Changes from original design (optional)

N/A

Additional work needed (optional)

N/A

Checklist

  • I've assigned an assignee to this PR and related issue(s) (if applicable)
  • I've assigned a label to this PR and related issue(s) (if applicable)
  • I've assigned a milestone to this PR and related issue(s) (if applicable)
  • I've updated documentation (code comments, README, etc. if applicable)
  • I've done sufficient testing (unit, integration, etc.)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ero-ledger#4889)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…r#4889)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…4889)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Signed-off-by: BartoszSolkaBD <bartosz.solka@blockydevs.com>
@jasuwienas jasuwienas added the enhancement New feature or request label Feb 13, 2026
@jasuwienas jasuwienas added this to the 0.75.0 milestone Feb 13, 2026
Copy link
Contributor

@jasuwienas jasuwienas left a comment

Choose a reason for hiding this comment

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

LG!

The algorithm appears to match my understanding from the Yellow Paper (and the comments align as well). However, it’s all a little bit complicated for me, that's why I proposed this: https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4919/changes#r2804276011 .

…eipts and eth_getBlockReceipts (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…larity (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Copy link
Contributor

@jasuwienas jasuwienas left a comment

Choose a reason for hiding this comment

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

LG! The data we used in one of the unit test as input is the same as:

curl -sS "https://docs-demo.monad-mainnet.quiknode.pro/"  -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_getRawReceipts","params":["0x23592c0"]}'

and we get the expect result the same as:

curl -sS "https://docs-demo.monad-mainnet.quiknode.pro/"  -H 'Content-Type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"debug_getRawReceipts","params":["0x23592c0"]}'

It’s not directly related to this change, but debug.spec.ts has grown quite large. We could make it much smaller by moving the test cases into separate file(s).

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ementation (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@simzzz simzzz modified the milestones: 0.75.0, 0.76.0 Feb 16, 2026
…penrpc-json-updated (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
jasuwienas
jasuwienas previously approved these changes Feb 17, 2026
…Receipts-method

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

❌ Patch coverage is 59.61538% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/relay/src/lib/receiptSerialization.ts 41.66% 35 Missing ⚠️
packages/relay/src/lib/debug.ts 82.92% 7 Missing ⚠️

❌ Your patch check has failed because the patch coverage (59.61%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (68.54%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

❗ There is a different number of reports uploaded between BASE (3795bdf) and HEAD (26d47da). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (3795bdf) HEAD (26d47da)
config-service 1 0
relay 1 0
@@             Coverage Diff             @@
##             main    #4919       +/-   ##
===========================================
- Coverage   92.64%   68.54%   -24.10%     
===========================================
  Files         118      144       +26     
  Lines       20530    23871     +3341     
  Branches     1479      616      -863     
===========================================
- Hits        19020    16363     -2657     
- Misses       1443     7487     +6044     
+ Partials       67       21       -46     
Flag Coverage Δ
config-service ?
relay ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/relay/src/index.ts 100.00% <100.00%> (ø)
packages/relay/src/lib/relay.ts 91.51% <100.00%> (ø)
packages/relay/src/lib/debug.ts 34.34% <82.92%> (-58.49%) ⬇️
packages/relay/src/lib/receiptSerialization.ts 41.66% <41.66%> (ø)

... and 93 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

simzzz
simzzz previously approved these changes Feb 17, 2026
});
});

describe('debug_getRawReceipts', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth adding more advanced checks like these here https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4909/changes#diff-ce8ccd45e52cf90b1bd467b689269510421319c309b4e08a8232707ebe3aa63bR1005?

I think the most beneficial tests are e2e, and from the beginning, we're trying to make them as precise as we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we have a test here https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4919/changes#diff-a0d7caed18547ec84f2f466ff3e64154ff1eae3f7d6c6f8ae25c9a3153f5e52cR2030 that validates the fields individually. Please let me know if you think this is sufficient, or if we should also add similar checks to the acceptance tests.

this.common = new CommonService(mirrorNodeClient, logger, cacheService);
this.mirrorNodeClient = mirrorNodeClient;
this.cacheService = cacheService;
this.eth = eth;
Copy link
Contributor

Choose a reason for hiding this comment

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

I have made a comment about circular dependency issue here #4909 (comment). I think there's a better way to deal with this.

@rpcParamValidationRules({
0: { type: 'blockNumber', required: true },
})
@cache()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we still have skipParams for @cache() as currently it could serve stale cache for latest or pending block tags.

*/
@rpcMethod
@rpcParamValidationRules({
0: { type: 'blockNumber', required: true },
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't debug_getRawReceipts accept both block number or block hash? Quicknode and Alchemy shows they do support blockHash, same for GETH https://github.com/ethereum/go-ethereum/blob/master/internal/ethapi/api.go#L1970.

What's the reason behind this deviation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was based on the parameters specification here: https://ethereum.github.io/execution-apis/api/methods/debug_getRawReceipts

I think we can very easily support both, so I will add it

"name": "Block",
"required": true,
"schema": {
"$ref": "#/components/schemas/BlockNumberOrTag"
Copy link
Contributor

Choose a reason for hiding this comment

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

If we also support blockHash we should make this use BlockNumberOrTag instead


traceBlockByHash(blockHash: string, tracerObject: BlockTracerConfig, requestDetails: RequestDetails): Promise<any>;

getRawReceipts(blockHashOrNumber: string, requestDetails: RequestDetails): Promise<string[] | null>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh then this interface shows blockHashOrNumber param. We should fix this as it could be misleading

DebugImpl.requireDebugAPIEnabled();
const receipts = await this.eth.getBlockReceipts(blockNumber, requestDetails);
if (!receipts) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Just wanted to flag the discrepancy, in the GitHub issue it mentioned Note: for not found receipts (e.g. future block) return "results": [], but here we return null if block not found. Let's be clear and make sure we have the consistent and correct return type (null or empty array)

}

/**
* Encodes a single receipt to EIP-2718 binary form (hex string).
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's enhance TSDoc so it could be clearer and more comprehensive

import constants from './constants';
import type { ITransactionReceipt } from './types';

// Log shape used for encoding: address, topics[], data (per Yellow Paper log structure)
Copy link
Contributor

Choose a reason for hiding this comment

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

let's enhance TSDoc

Comment on lines 287 to 292
const receipts = await this.eth.getBlockReceipts(blockNumber, requestDetails);
if (!receipts) {
return null;
}
return receipts.map((receipt) => encodeReceiptToHex(receipt));
}
Copy link
Contributor

@quiet-node quiet-node Feb 17, 2026

Choose a reason for hiding this comment

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

I think we're taking advantage of the convenience of reusing this.eth.getBlockReceipts(blockNumber, requestDetails), but we're trading that convenience for a significant performance hit that could look really bad on blocks with tens of thousands of transactions.

The core issue is that debug_getRawReceipts routes through the full eth_getBlockReceipts pipeline, which is designed to return rich, user-facing JSON receipts with resolved addresses. But raw receipts only need 5 primitive fields that are already available in the mirror node response, no address resolution required. So for a block with 10,000 transactions, that means roughly 20,000 unnecessary resolveEvmAddress calls, plus all the wasted object construction for fields like from, to, effectiveGasPrice, and others that encodeReceiptToHex might never touch.

There is also a double-loop problem with the current implementation. After this.eth.getBlockReceipts() fully resolves and returns the receipt list, it has already looped through each receipt once. Then getRawReceipts() in debug.ts has to loop through the entire array a second time to encode each receipt. Given that this second loop runs on the main thread, it can easily crash the Relay under heavy network load with large blocks, which has actually happened to us before.

I think a better approach would be to add a dedicated getRawReceipts function directly in the block worker. This function would reuse the existing lightweight methods, strip out everything we do not need such as resolveEvmAddress calls or unnecessary object construction, and handle the receipt encoding in the same loop to keep it to a single pass. From there, BlockService can expose a getRawReceipts() method that delegates to the worker thread, and debug.ts can simply call blockService.getRawReceipts(). This bypasses eth.getBlockReceipts entirely and avoids all the overhead that comes with it.

Let me know what you think.

@BartoszSolkaBD BartoszSolkaBD marked this pull request as draft February 18, 2026 10:33
…ceipts functionality (hiero-ledger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…ger#4892)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@BartoszSolkaBD BartoszSolkaBD marked this pull request as ready for review February 19, 2026 12:02
}
}

async function loadBlockExecutionData(
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add docs

requestDetails: RequestDetails,
): Promise<{
block: MirrorNodeBlock | null;
contractResults: any[];
Copy link
Contributor

Choose a reason for hiding this comment

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

We have a ContractResult type right? Let's avoid any

return { block, contractResults, logsByHash };
}

function groupLogsByHash(logs: Log[]): Map<string, Log[]> {
Copy link
Contributor

Choose a reason for hiding this comment

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

In my opinion, the logic to construct logsByHash is fairly straightforward and not particularly long or messy, and since it is not reused anywhere else, extracting it into a separate method may add unnecessary indirection without much benefit. Would it make sense to just inline it inside loadBlockExecutionData to keep things more direct?

}
}

export async function getRawReceipts(
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add docs

Comment on lines +409 to +412
if ((!contractResults || contractResults.length === 0) && logsByHash.size === 0) {
return [];
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I see in getBlockReceipts we check for block === null, we don't need it for debug_getRawReceipts?

Comment on lines +428 to +436
const transactionReceiptParams: IRegularTransactionReceiptRlpInputParams = {
logs: contractResult.logs,
receiptResponse: contractResult,
blockGasUsedBeforeTransaction,
};
const receiptRlpInput = TransactionReceiptFactory.createReceiptRlpInput(
transactionReceiptParams,
) as IReceiptRlpInput;
blockGasUsedBeforeTransaction += contractResult.gas_used;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to pass params, contractResult.logs, contractResult, and blockGasUsedBeforeTransaction directly into TransactionReceiptFactory.createReceiptRlpInput() instead of bundling them into an object first? The current approach adds unnecessary overhead for a couple of reasons.

First, we are spending effort constructing an object only to immediately destructure it inside createReceiptRlpInput(), which feels wasteful. Second, wrapping these values in a new object requires defining a new interface, IRegularTransactionReceiptRlpInputParams, which does not seem worth the added complexity.

Passing the parameters directly into the method would eliminate all of this overhead.


import { Log } from '../model';

export interface IReceiptRlpInput {
Copy link
Contributor

Choose a reason for hiding this comment

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

lets add tsdoc

Comment on lines +38 to +41
* Produces the RLP-encoded 4-tuple (receipt_root_or_status, cumulative_gas_used,
* logs_bloom, logs) per the Ethereum Yellow Paper. For typed transactions (type !== 0),
* the output is the single-byte type prefix followed by that RLP payload (EIP-2718).
*
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it possible to include a link to the exact resource we're using to construct this encode logic?

};
const receiptRlpInput = TransactionReceiptFactory.createReceiptRlpInput(
transactionReceiptParams,
) as IReceiptRlpInput;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this extra cast?

Comment on lines 66 to +91

/**
* Creates a minimal receipt payload for RLP-encoding of a synthetic transaction.
*
* Builds an `IReceiptRlpInput` from synthetic logs only, without resolving any
* addresses or constructing a full `ITransactionReceipt`. The returned shape
* contains the fields required for Yellow Paper receipt encoding, including a zero
* cumulative gas used, zero gas used, a logs bloom computed from the first
* synthetic log, default root and status values, the transaction index from
* the first log, and a fallback type of `0x0`.
*
* @param syntheticLogs - Logs belonging to the synthetic transaction.
* @returns Minimal receipt data suitable for RLP encoding.
*/
public static createSyntheticReceiptRlpInput(syntheticLogs: Log[]): IReceiptRlpInput {
return {
cumulativeGasUsed: constants.ZERO_HEX,
logs: syntheticLogs,
logsBloom: LogsBloomUtils.buildLogsBloom(syntheticLogs[0].address, syntheticLogs[0].topics),
root: constants.DEFAULT_ROOT_HASH,
status: constants.ONE_HEX,
transactionIndex: syntheticLogs[0].transactionIndex,
type: constants.ZERO_HEX, // fallback to 0x0 from HAPI transactions
};
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a bit hesitant about this method and the createReceiptRlpInput() below being in this class. It feels like these two might belong in the blockWorker itself as private methods, since they appear to be helper utilities for constructing input for a specific method. TransactionReceiptFactory, on the other hand, seems more focused on working with actual receipts. With that in mind, would it be worth keeping these helpers out of TransactionReceiptFactory to avoid adding responsibilities that don't quite fit its purpose?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add debug_getRawReceipts method

5 participants

Comments