feat: add debug_getRawReceipts method#4919
feat: add debug_getRawReceipts method#4919BartoszSolkaBD wants to merge 16 commits intohiero-ledger:mainfrom
debug_getRawReceipts method#4919Conversation
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>
There was a problem hiding this comment.
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 .
packages/server/tests/acceptance/data/conformity/overwrites/debug_getRawReceipts/get-block-n.io
Outdated
Show resolved
Hide resolved
…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>
jasuwienas
left a comment
There was a problem hiding this comment.
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>
…penrpc-json-updated (hiero-ledger#4892) Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
…Receipts-method Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
Codecov Report❌ Patch coverage is
❌ 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.
@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 93 files with indirect coverage changes 🚀 New features to boost your workflow:
|
| }); | ||
| }); | ||
|
|
||
| describe('debug_getRawReceipts', () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/relay/src/lib/debug.ts
Outdated
| this.common = new CommonService(mirrorNodeClient, logger, cacheService); | ||
| this.mirrorNodeClient = mirrorNodeClient; | ||
| this.cacheService = cacheService; | ||
| this.eth = eth; |
There was a problem hiding this comment.
I have made a comment about circular dependency issue here #4909 (comment). I think there's a better way to deal with this.
packages/relay/src/lib/debug.ts
Outdated
| @rpcParamValidationRules({ | ||
| 0: { type: 'blockNumber', required: true }, | ||
| }) | ||
| @cache() |
There was a problem hiding this comment.
Should we still have skipParams for @cache() as currently it could serve stale cache for latest or pending block tags.
packages/relay/src/lib/debug.ts
Outdated
| */ | ||
| @rpcMethod | ||
| @rpcParamValidationRules({ | ||
| 0: { type: 'blockNumber', required: true }, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
docs/openrpc.json
Outdated
| "name": "Block", | ||
| "required": true, | ||
| "schema": { | ||
| "$ref": "#/components/schemas/BlockNumberOrTag" |
There was a problem hiding this comment.
If we also support blockHash we should make this use BlockNumberOrTag instead
packages/relay/src/index.ts
Outdated
|
|
||
| traceBlockByHash(blockHash: string, tracerObject: BlockTracerConfig, requestDetails: RequestDetails): Promise<any>; | ||
|
|
||
| getRawReceipts(blockHashOrNumber: string, requestDetails: RequestDetails): Promise<string[] | null>; |
There was a problem hiding this comment.
Oh then this interface shows blockHashOrNumber param. We should fix this as it could be misleading
packages/relay/src/lib/debug.ts
Outdated
| DebugImpl.requireDebugAPIEnabled(); | ||
| const receipts = await this.eth.getBlockReceipts(blockNumber, requestDetails); | ||
| if (!receipts) { | ||
| return null; |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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) |
packages/relay/src/lib/debug.ts
Outdated
| const receipts = await this.eth.getBlockReceipts(blockNumber, requestDetails); | ||
| if (!receipts) { | ||
| return null; | ||
| } | ||
| return receipts.map((receipt) => encodeReceiptToHex(receipt)); | ||
| } |
There was a problem hiding this comment.
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.
…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>
| } | ||
| } | ||
|
|
||
| async function loadBlockExecutionData( |
| requestDetails: RequestDetails, | ||
| ): Promise<{ | ||
| block: MirrorNodeBlock | null; | ||
| contractResults: any[]; |
There was a problem hiding this comment.
We have a ContractResult type right? Let's avoid any
| return { block, contractResults, logsByHash }; | ||
| } | ||
|
|
||
| function groupLogsByHash(logs: Log[]): Map<string, Log[]> { |
There was a problem hiding this comment.
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( |
| if ((!contractResults || contractResults.length === 0) && logsByHash.size === 0) { | ||
| return []; | ||
| } | ||
|
|
There was a problem hiding this comment.
I see in getBlockReceipts we check for block === null, we don't need it for debug_getRawReceipts?
| const transactionReceiptParams: IRegularTransactionReceiptRlpInputParams = { | ||
| logs: contractResult.logs, | ||
| receiptResponse: contractResult, | ||
| blockGasUsedBeforeTransaction, | ||
| }; | ||
| const receiptRlpInput = TransactionReceiptFactory.createReceiptRlpInput( | ||
| transactionReceiptParams, | ||
| ) as IReceiptRlpInput; | ||
| blockGasUsedBeforeTransaction += contractResult.gas_used; |
There was a problem hiding this comment.
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 { |
| * 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). | ||
| * |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Do we need this extra cast?
|
|
||
| /** | ||
| * 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 | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
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?
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:
Related issue(s)
Fixes #4892
Testing Guide
Start the JSON-RPC relay with mainnet config (and DEBUG_API_ENABLED=true if required). Ensure it is connected and serving requests.
{"jsonrpc":"2.0","id":1,"method":"debug_getRawReceipts","params":["0x56e86ab"]}{"jsonrpc":"2.0","id":1,"method":"eth_getBlockReceipts","params":["0x56e86ab"]}Changes from original design (optional)
N/A
Additional work needed (optional)
N/A
Checklist