diff --git a/docs/openrpc.json b/docs/openrpc.json index 2e36170fa9..deca50140e 100644 --- a/docs/openrpc.json +++ b/docs/openrpc.json @@ -1316,6 +1316,28 @@ } } }, + { + "name": "debug_getRawHeader", + "summary": "Returns an RLP-encoded header.", + "description": "![](https://raw.githubusercontent.com/hiero-ledger/hiero-json-rpc-relay/main/docs/images/http_label.png)", + "params": [ + { + "name": "block", + "required": true, + "schema": { + "$ref": "#/components/schemas/BlockNumberOrTagOrHash" + } + } + ], + "result": { + "name": "Header RLP", + "description": "The RLP-encoded block header, or 0x if the block is not found", + "schema": { + "title": "raw block", + "$ref": "#/components/schemas/bytes" + } + } + }, { "name": "debug_traceBlockByNumber", "summary": "Returns the tracing result for all transactions in the block specified by number with a tracer.", diff --git a/docs/rpc-api.md b/docs/rpc-api.md index 004f1bad74..c20a6068e6 100644 --- a/docs/rpc-api.md +++ b/docs/rpc-api.md @@ -130,7 +130,7 @@ These methods are extensions provided by various Ethereum clients but are not pa | [admin_config](https://github.com/hiero-ledger/hiero-json-rpc-relay/tree/main#admin-api) | **Implemented** | N/A | Returns relay and upstream dependency configuration | | [debug_getBadBlocks](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debuggetbadblocks) | **Implemented** - Requires `DEBUG_API_ENABLED=true` | N/A | Always returns empty array | | [debug_getRawBlock](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debuggetrawblock) | **Implemented** - Requires `DEBUG_API_ENABLED=true` | N/A | | -| [debug_getRawHeader](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debuggetrawheader) | **Not Implemented** | N/A | | +| [debug_getRawHeader](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debuggetrawheader) | **Implemented** - Requires `DEBUG_API_ENABLED=true` | N/A | | | [debug_getRawReceipts](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debuggetrawreceipts) | **Not Implemented** | N/A | | | [debug_getRawTransaction](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debuggetrawtransaction) | **Not Implemented** | N/A | | | [debug_traceBlockByHash](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtraceblockbyhash) | **Implemented** - Requires `DEBUG_API_ENABLED=true` | Mirror Node | Supports CallTracer and PrestateTracer, caches results | diff --git a/packages/relay/src/index.ts b/packages/relay/src/index.ts index 8e68dc9c8f..a8403e5cbb 100644 --- a/packages/relay/src/index.ts +++ b/packages/relay/src/index.ts @@ -35,6 +35,8 @@ export interface Debug { getRawBlock(blockNrOrHash: string, requestDetails: RequestDetails): Promise; + getRawHeader(blockNrOrHash: string, requestDetails: RequestDetails): Promise; + traceBlockByHash(blockHash: string, tracerObject: BlockTracerConfig, requestDetails: RequestDetails): Promise; } diff --git a/packages/relay/src/lib/debug.ts b/packages/relay/src/lib/debug.ts index 41fa5d465d..fca54a2fd8 100644 --- a/packages/relay/src/lib/debug.ts +++ b/packages/relay/src/lib/debug.ts @@ -3,7 +3,15 @@ import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; import type { Logger } from 'pino'; -import { decodeErrorMessage, mapKeysAndValues, numberTo0x, prepend0x, strip0x, tinybarsToWeibars } from '../formatters'; +import { + decodeErrorMessage, + mapKeysAndValues, + numberTo0x, + prepend0x, + strip0x, + tinybarsToWeibars, + toHexString, +} from '../formatters'; import { type Debug } from '../index'; import { JsonRpcError } from '../index'; import { Utils } from '../utils'; @@ -136,6 +144,41 @@ export class DebugImpl implements Debug { return constants.EMPTY_HEX + Buffer.from(BlockFactory.rlpEncode(block)).toString('hex'); } + /** + * Get a raw block header for debugging purposes. + * + * @async + * @rpcMethod Exposed as debug_getRawHeader RPC endpoint + * @rpcParamValidationRules Applies JSON-RPC parameter validation according to the API specification + * + * @param {string} blockNrOrHash - The block number, tag or hash. Possible values are 'earliest', 'pending', 'latest', hex block number or 32 bytes hash. + * @param {RequestDetails} requestDetails - Request details for logging and tracking + * + * @example + * const result = await getRawHeader('0x160c', requestDetails); + */ + @rpcMethod + @rpcParamValidationRules({ + 0: { type: ['blockNumber', 'blockHash'], required: true }, + }) + @cache({ + skipParams: [{ index: '0', value: constants.NON_CACHABLE_BLOCK_PARAMS }], + }) + async getRawHeader(blockNrOrHash: string, requestDetails: RequestDetails): Promise { + DebugImpl.requireDebugAPIEnabled(); + + const block: Block | null = + blockNrOrHash.length === 66 + ? await this.blockService.getBlockByHash(blockNrOrHash, false, requestDetails) + : await this.blockService.getBlockByNumber(blockNrOrHash, false, requestDetails); + + if (!block) { + return constants.EMPTY_HEX; + } + + return prepend0x(toHexString(BlockFactory.rlpEncode(block, true))); + } + /** * Trace a transaction for debugging purposes. * diff --git a/packages/relay/src/lib/factories/blockFactory.ts b/packages/relay/src/lib/factories/blockFactory.ts index b945238cf7..f2710d22e6 100644 --- a/packages/relay/src/lib/factories/blockFactory.ts +++ b/packages/relay/src/lib/factories/blockFactory.ts @@ -138,10 +138,12 @@ export class BlockFactory { * RLP encode a block based on Ethereum Yellow Paper. * * @param { Block } block - The block object from eth_getBlockByNumber/Hash + * @param {boolean} headerOnly - A flag that determines whether to encode the entire block or only the block header + * * @returns {Uint8Array} - RLP encoded block as Uint8 array */ - static rlpEncode(block: Block): Uint8Array { - if (typeof block.transactions[0] === 'string') { + static rlpEncode(block: Block, headerOnly: boolean = false): Uint8Array { + if (!headerOnly && typeof block.transactions[0] === 'string') { throw new Error('Block transactions must include full transaction objects for RLP encoding'); } @@ -150,7 +152,8 @@ export class BlockFactory { // -- BT - block transactions (RLP encoded transactions array) // -- BU - ommers (empty array) // -- BW - withdrawals (empty array) - return RLP.encode([ + + const header = [ // Hp - parentHash block.parentHash, // Ho - ommersHash @@ -185,6 +188,14 @@ export class BlockFactory { block.baseFeePerGas, // Hw - withdrawalsRoot block.withdrawalsRoot, + ]; + + if (headerOnly) { + return RLP.encode(header); + } + + return RLP.encode([ + ...header, // BT - block transactions (RLP encoded transactions array) [...block.transactions.map((tx) => BlockFactory.rlpEncodeTx(tx as Transaction))], // BU - ommers (empty array) diff --git a/packages/relay/tests/lib/debug.spec.ts b/packages/relay/tests/lib/debug.spec.ts index 42c456a5ba..30c2aff2c9 100644 --- a/packages/relay/tests/lib/debug.spec.ts +++ b/packages/relay/tests/lib/debug.spec.ts @@ -315,6 +315,61 @@ describe('Debug API Test Suite', async function () { evm_address: '0x91b1c451777122afc9b83f9b96160d7e59847ad7', }; + const blockInfo = { + timestamp: '0x698afa66', + difficulty: '0x0', + extraData: '0x', + gasLimit: '0x1c9c380', + baseFeePerGas: '0xd63445f000', + gasUsed: '0xa32c1', + logsBloom: + '0x0000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000', + miner: '0x0000000000000000000000000000000000000000', + mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + nonce: '0x0000000000000000', + receiptsRoot: '0x26c9ecffe4aa9e2e19f814a570bd1e9093ff55e9e6c18f39f4192de6e36153db', + sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x1b81', + stateRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + totalDifficulty: '0x0', + transactions: [ + { + blockHash: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', + blockNumber: '0x1de1f54', + chainId: '0x128', + from: '0xbe04a4900b02fe715c75ff307f0b531894184c91', + gas: '0xc2860', + gasPrice: '0x0', + hash: '0x4454bdc6328e6cafb477c76af5e6a72dcb9f97e5aa79d76900f8ca65712a8151', + input: + '0xef7615ce00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000085614ea608c5dd326ba83aeaaacc7eb9d090e0d40000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000019800000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000002436623739366561382d303635622d343133322d383266642d38653766613334626338623900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000672616e616a69000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001586b615258536e5a6f62384c6c5a6c3270327351367134426337385977553175564473746b42756b66724652306556304f6e3750504c4a324a7262655157717443474c5170797579367559686a725138685a6c4f646242485831484741616277544147397a61483068504d4765336c7136682f665a5a5266316b6161626b356c34476d352f4b6a516e4746654a52776a55753565546775305242507338314b416238444735304e6c77524544616f5547635762376c514c504b656e6b354d4f5064662f31504c58546f383461793333307a77446e61786a46584f30783239373761786e4548365879696c5941784b636c7954397963793766477a6b4d724a6a757a376850486767436d4652315a68664a5252334778684c647a366f4336424b497554506154524b52566e63345742585432454577494c2f514d4542422f764d4a695a326733665a576e563572595962446c6e42326338773d3d000000000000000000000000000000000000000000000000000000000000000000000000000000415f0770f2c509e8cb0c3dacceca295e43657f1232c62c9f2d542d8754a6a94720500abc4b95446945a686675fc1e1768506390f5aa2be98ef2e58727d8893b99f1c00000000000000000000000000000000000000000000000000000000000000', + nonce: '0x168a', + r: '0xabbfb012c0b774997edcf782a256e55590325962f7a96ffb64467a323c84733f', + s: '0x60627cc8fc5be8d28dbec3de0835769f1140604eae6bb732dbc60b7aba4274aa', + to: '0xdd902a9d02d570d92e5d94b095bf6b7a4106773a', + transactionIndex: '0xf', + type: '0x2', + v: '0x0', + value: '0x0', + yParity: '0x0', + accessList: [], + maxPriorityFeePerGas: '0x62', + maxFeePerGas: '0x62', + }, + ], + transactionsRoot: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', + uncles: [], + withdrawals: [], + withdrawalsRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', + number: '0x1de1f54', + hash: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', + parentHash: '0xd7dbe6b1379e3e1d71729a92e167af28d6b79aa9e40b0f6d845fe7b85c500bfa', + }; + + const blockNumber = '0x160c'; + + const blockHash = '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce'; + this.beforeAll(() => { mirrorNodeInstance = new MirrorNodeClient( ConfigService.get('MIRROR_NODE_URL')!, @@ -349,10 +404,68 @@ describe('Debug API Test Suite', async function () { cacheService.clear(); }); - describe('debug_getRawBlock', async function () { - const blockNumber = '0x160c'; - const blockHash = '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce'; + describe('debug_getRawHeader', async function () { + beforeEach(() => { + sinon.restore(); + }); + + withOverriddenEnvsInMochaTest({ DEBUG_API_ENABLED: true }, () => { + it('should return "0x" when block is not found using block number', async function () { + sinon.stub(debugService['blockService'], 'getBlockByNumber').resolves(null); + const result = await debugService.getRawHeader(blockNumber, requestDetails); + expect(result).to.equal('0x'); + }); + + it('should return "0x" when block is not found using block hash', async function () { + sinon.stub(debugService['blockService'], 'getBlockByHash').resolves(null); + const result = await debugService.getRawHeader(blockHash, requestDetails); + expect(result).to.equal('0x'); + }); + + it('should return a RLP block for existing block', async () => { + const expectedRlpHex = + '0xf90223' + + 'a0' + + 'd7dbe6b1379e3e1d71729a92e167af28d6b79aa9e40b0f6d845fe7b85c500bfa' + // parent hash + 'a0' + + '1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347' + // ommersHash + '94' + + '0000000000000000000000000000000000000321' + // beneficiary + 'a0' + + '56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421' + // stateRoot + 'a0' + + 'cf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce' + // transactionsRoot + 'a0' + + '26c9ecffe4aa9e2e19f814a570bd1e9093ff55e9e6c18f39f4192de6e36153db' + // receiptsRoot + 'b9' + + '01000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000' + // logsBloom + '00' + // difficulty + '84' + + '01de1f54' + // number + '84' + + '01c9c380' + // gasLimit + '83' + + '0a32c1' + // gasUsed + '84' + + '698afa66' + // timestamp + '80' + // extraData + 'a0' + + '0000000000000000000000000000000000000000000000000000000000000000' + // prevrandao + '88' + + '0000000000000000' + // nonce + '85' + + 'd63445f000' + // baseFeePerGas + 'a0' + + '0000000000000000000000000000000000000000000000000000000000000000'; // withdrawalsRoot + sinon.stub(debugService['blockService'], 'getBlockByHash').resolves(blockInfo as Block); + const result = await debugService.getRawHeader(blockHash, requestDetails); + expect(result).to.equal(expectedRlpHex); + }); + }); + }); + + describe('debug_getRawBlock', async function () { beforeEach(() => { sinon.restore(); }); @@ -371,57 +484,6 @@ describe('Debug API Test Suite', async function () { }); it('should return a RLP block for existing block', async () => { - const blockInfo = { - timestamp: '0x698afa66', - difficulty: '0x0', - extraData: '0x', - gasLimit: '0x1c9c380', - baseFeePerGas: '0xd63445f000', - gasUsed: '0xa32c1', - logsBloom: - '0x0000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000', - miner: '0x0000000000000000000000000000000000000000', - mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', - nonce: '0x0000000000000000', - receiptsRoot: '0x26c9ecffe4aa9e2e19f814a570bd1e9093ff55e9e6c18f39f4192de6e36153db', - sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', - size: '0x1b81', - stateRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', - totalDifficulty: '0x0', - transactions: [ - { - blockHash: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', - blockNumber: '0x1de1f54', - chainId: '0x128', - from: '0xbe04a4900b02fe715c75ff307f0b531894184c91', - gas: '0xc2860', - gasPrice: '0x0', - hash: '0x4454bdc6328e6cafb477c76af5e6a72dcb9f97e5aa79d76900f8ca65712a8151', - input: - '0xef7615ce00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000085614ea608c5dd326ba83aeaaacc7eb9d090e0d40000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000019800000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000002436623739366561382d303635622d343133322d383266642d38653766613334626338623900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000672616e616a69000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001586b615258536e5a6f62384c6c5a6c3270327351367134426337385977553175564473746b42756b66724652306556304f6e3750504c4a324a7262655157717443474c5170797579367559686a725138685a6c4f646242485831484741616277544147397a61483068504d4765336c7136682f665a5a5266316b6161626b356c34476d352f4b6a516e4746654a52776a55753565546775305242507338314b416238444735304e6c77524544616f5547635762376c514c504b656e6b354d4f5064662f31504c58546f383461793333307a77446e61786a46584f30783239373761786e4548365879696c5941784b636c7954397963793766477a6b4d724a6a757a376850486767436d4652315a68664a5252334778684c647a366f4336424b497554506154524b52566e63345742585432454577494c2f514d4542422f764d4a695a326733665a576e563572595962446c6e42326338773d3d000000000000000000000000000000000000000000000000000000000000000000000000000000415f0770f2c509e8cb0c3dacceca295e43657f1232c62c9f2d542d8754a6a94720500abc4b95446945a686675fc1e1768506390f5aa2be98ef2e58727d8893b99f1c00000000000000000000000000000000000000000000000000000000000000', - nonce: '0x168a', - r: '0xabbfb012c0b774997edcf782a256e55590325962f7a96ffb64467a323c84733f', - s: '0x60627cc8fc5be8d28dbec3de0835769f1140604eae6bb732dbc60b7aba4274aa', - to: '0xdd902a9d02d570d92e5d94b095bf6b7a4106773a', - transactionIndex: '0xf', - type: '0x2', - v: '0x0', - value: '0x0', - yParity: '0x0', - accessList: [], - maxPriorityFeePerGas: '0x62', - maxFeePerGas: '0x62', - }, - ], - transactionsRoot: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', - uncles: [], - withdrawals: [], - withdrawalsRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', - number: '0x1de1f54', - hash: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', - parentHash: '0xd7dbe6b1379e3e1d71729a92e167af28d6b79aa9e40b0f6d845fe7b85c500bfa', - }; - const expectedRlpHex = '0xf905fc' + 'a0' + diff --git a/packages/relay/tests/lib/factories/blockFactory.spec.ts b/packages/relay/tests/lib/factories/blockFactory.spec.ts new file mode 100644 index 0000000000..4311cd8225 --- /dev/null +++ b/packages/relay/tests/lib/factories/blockFactory.spec.ts @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { RLP } from '@ethereumjs/rlp'; +import { expect } from 'chai'; + +import { numberTo0x } from '../../../src/formatters'; +import constants from '../../../src/lib/constants'; +import { BlockFactory } from '../../../src/lib/factories/blockFactory'; +import { Block, Transaction } from '../../../src/lib/model'; + +const blockInfo = { + timestamp: '0x698afa66', + difficulty: '0x0', + extraData: '0x', + gasLimit: '0x1c9c380', + baseFeePerGas: '0xd63445f000', + gasUsed: '0xa32c1', + logsBloom: + '0x0000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000', + miner: '0x0000000000000000000000000000000000000000', + mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + nonce: '0x0000000000000000', + receiptsRoot: '0x26c9ecffe4aa9e2e19f814a570bd1e9093ff55e9e6c18f39f4192de6e36153db', + sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x1b81', + stateRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + totalDifficulty: '0x0', + transactions: [ + { + blockHash: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', + blockNumber: '0x1de1f54', + chainId: '0x128', + from: '0xbe04a4900b02fe715c75ff307f0b531894184c91', + gas: '0xc2860', + gasPrice: '0x0', + hash: '0x4454bdc6328e6cafb477c76af5e6a72dcb9f97e5aa79d76900f8ca65712a8151', + input: + '0xef7615ce00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000085614ea608c5dd326ba83aeaaacc7eb9d090e0d40000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000019800000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000002436623739366561382d303635622d343133322d383266642d38653766613334626338623900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000672616e616a69000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001586b615258536e5a6f62384c6c5a6c3270327351367134426337385977553175564473746b42756b66724652306556304f6e3750504c4a324a7262655157717443474c5170797579367559686a725138685a6c4f646242485831484741616277544147397a61483068504d4765336c7136682f665a5a5266316b6161626b356c34476d352f4b6a516e4746654a52776a55753565546775305242507338314b416238444735304e6c77524544616f5547635762376c514c504b656e6b354d4f5064662f31504c58546f383461793333307a77446e61786a46584f30783239373761786e4548365879696c5941784b636c7954397963793766477a6b4d724a6a757a376850486767436d4652315a68664a5252334778684c647a366f4336424b497554506154524b52566e63345742585432454577494c2f514d4542422f764d4a695a326733665a576e563572595962446c6e42326338773d3d000000000000000000000000000000000000000000000000000000000000000000000000000000415f0770f2c509e8cb0c3dacceca295e43657f1232c62c9f2d542d8754a6a94720500abc4b95446945a686675fc1e1768506390f5aa2be98ef2e58727d8893b99f1c00000000000000000000000000000000000000000000000000000000000000', + nonce: '0x168a', + r: '0xabbfb012c0b774997edcf782a256e55590325962f7a96ffb64467a323c84733f', + s: '0x60627cc8fc5be8d28dbec3de0835769f1140604eae6bb732dbc60b7aba4274aa', + to: '0xdd902a9d02d570d92e5d94b095bf6b7a4106773a', + transactionIndex: '0xf', + type: '0x2', + v: '0x0', + value: '0x0', + yParity: '0x0', + accessList: [], + maxPriorityFeePerGas: '0x62', + maxFeePerGas: '0x62', + }, + ], + transactionsRoot: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', + uncles: [], + withdrawals: [], + withdrawalsRoot: '0x0000000000000000000000000000000000000000000000000000000000000000', + number: '0x1de1f54', + hash: '0xcf55eb0655b5d21c38413afe1919099a83140514cb6c531aebd77e3d2c5506ce', + parentHash: '0xd7dbe6b1379e3e1d71729a92e167af28d6b79aa9e40b0f6d845fe7b85c500bfa', +}; + +const blockResponse: any = { + hash: blockInfo.hash, + timestamp: { from: '1770715750.000000000' }, + gas_used: parseInt(blockInfo.gasUsed, 16), + logs_bloom: blockInfo.logsBloom, + number: parseInt(blockInfo.number, 16), + previous_hash: blockInfo.parentHash, + size: parseInt(blockInfo.size, 16), +}; + +const hexToData = (buf) => `0x${Buffer.from(buf).toString('hex')}`; + +const hexToQuantity = (buf) => { + if (buf.length === 0) return '0x0'; + return `0x${Buffer.from(buf).toString('hex').replace(/^0+/, '') || '0'}`; +}; + +describe('BlockFactory', () => { + describe('createBlock', () => { + it('should map MirrorNodeBlock fields correctly', async () => { + const block = await BlockFactory.createBlock({ + blockResponse, + txArray: blockInfo.transactions, + gasPrice: blockInfo.baseFeePerGas, + receiptsRoot: blockInfo.receiptsRoot, + }); + + expect(block.hash).to.equal(blockInfo.hash); + expect(block.parentHash).to.equal(blockInfo.parentHash); + expect(block.number).to.equal(numberTo0x(parseInt(blockInfo.number, 16))); + expect(block.gasUsed).to.equal(numberTo0x(parseInt(blockInfo.gasUsed, 16))); + expect(block.baseFeePerGas).to.equal(blockInfo.baseFeePerGas); + expect(block.transactions).to.deep.equal(blockInfo.transactions); + expect(block.timestamp).to.equal(blockInfo.timestamp); + expect(block.logsBloom).to.equal(blockInfo.logsBloom); + expect(block.receiptsRoot).to.equal(blockInfo.receiptsRoot); + }); + + it('should set transactionsRoot to default when txArray is empty', async () => { + const block = await BlockFactory.createBlock({ + blockResponse, + txArray: [], + gasPrice: blockInfo.baseFeePerGas, + receiptsRoot: blockInfo.receiptsRoot, + }); + + expect(block.transactions).to.have.length(0); + expect(block.transactionsRoot).to.equal(constants.DEFAULT_ROOT_HASH); + }); + }); + + describe('rlpEncodeTx', () => { + it('should produce deterministic serialization for EIP-1559 tx', () => { + const tx = blockInfo.transactions[0] as Transaction; + const encoded1 = BlockFactory.rlpEncodeTx(tx); + const encoded2 = BlockFactory.rlpEncodeTx(tx); + + expect(encoded1).to.equal(encoded2); + expect(encoded1.startsWith('0x02')).to.be.true; + }); + + it('should pad empty signature r and s to canonical zero encoding', () => { + const tx: any = { + ...blockInfo.transactions[0], + r: '0x', + s: '0x0', + }; + + const encoded = BlockFactory.rlpEncodeTx(tx); + + const hex = encoded.slice(2); + const typeByte = hex.slice(0, 2); + expect(typeByte).to.equal('02'); + + const rlpPayload = Buffer.from(hex.slice(2), 'hex'); + const decoded: any[] = RLP.decode(rlpPayload) as any[]; + + const rField: Uint8Array = decoded[decoded.length - 3]; + const sField: Uint8Array = decoded[decoded.length - 2]; + + expect(rField.length).to.equal(0); + expect(sField.length).to.equal(0); + }); + + it('should encode legacy transaction with gasPrice', () => { + const tx: any = { + ...blockInfo.transactions[0], + type: '0x0', + gasPrice: '0x1', + }; + + const encoded = BlockFactory.rlpEncodeTx(tx); + + expect(encoded.startsWith('0x')).to.be.true; + expect(encoded.startsWith('0x02')).to.be.false; + }); + + it('should encode EIP-2930 with accessList', () => { + const tx: any = { + ...blockInfo.transactions[0], + type: '0x1', + gasPrice: '0x1', + accessList: [], + }; + + const encoded = BlockFactory.rlpEncodeTx(tx); + expect(encoded.startsWith('0x01')).to.be.true; + }); + + it('should encode EIP-7702 with authorization list entries', () => { + const tx: any = { + ...blockInfo.transactions[0], + type: '0x4', + authorizationList: [ + { + chainId: 1, + nonce: 1, + address: '0x000000000000000000000000000000000000dead', + r: blockInfo.transactions[0].r, + s: blockInfo.transactions[0].s, + yParity: '0x0', + }, + ], + }; + + const encoded = BlockFactory.rlpEncodeTx(tx); + expect(encoded.startsWith('0x04')).to.be.true; + expect(encoded.length).to.be.greaterThan(10); + }); + }); + + describe('rlpEncode', () => { + let block: Block; + + beforeEach(async () => { + block = await BlockFactory.createBlock({ + blockResponse, + txArray: blockInfo.transactions, + gasPrice: blockInfo.baseFeePerGas, + receiptsRoot: blockInfo.receiptsRoot, + }); + }); + + it('should RLP encode header with exactly 17 fields', () => { + const encoded = BlockFactory.rlpEncode(block, true); + const decoded = RLP.decode(encoded) as Uint8Array[]; + + expect(decoded).to.have.length(17); + + expect(hexToData(decoded[0])).to.equal(blockInfo.parentHash); + expect(hexToData(decoded[1])).to.equal(constants.EMPTY_ARRAY_HEX); + expect(hexToData(decoded[2])).to.equal(constants.HEDERA_NODE_REWARD_ACCOUNT_ADDRESS); + expect(hexToData(decoded[3])).to.equal(blockInfo.stateRoot); + expect(hexToData(decoded[4])).to.equal(blockInfo.transactionsRoot); + expect(hexToData(decoded[5])).to.equal(blockInfo.receiptsRoot); + expect(hexToData(decoded[6])).to.equal(blockInfo.logsBloom); + expect(hexToQuantity(decoded[7])).to.equal(blockInfo.difficulty); + expect(hexToQuantity(decoded[8])).to.equal(blockInfo.number); + expect(hexToQuantity(decoded[9])).to.equal(blockInfo.gasLimit); + expect(hexToQuantity(decoded[10])).to.equal(blockInfo.gasUsed); + expect(hexToData(decoded[11])).to.equal(blockInfo.timestamp); + expect(hexToData(decoded[12])).to.equal(blockInfo.extraData); + expect(hexToData(decoded[13])).to.equal(blockInfo.mixHash); + expect(hexToData(decoded[14])).to.equal(blockInfo.nonce); + expect(hexToData(decoded[15])).to.equal(blockInfo.baseFeePerGas); + expect(hexToData(decoded[16])).to.equal(blockInfo.withdrawalsRoot); + }); + + it('should RLP encode full block including transactions array', () => { + const encoded = BlockFactory.rlpEncode(block, false); + const decoded = RLP.decode(encoded) as any[]; + + // header (17) + txs + ommers + withdrawals + expect(decoded.length).to.equal(20); + + const txArray = decoded[17]; + expect(txArray).to.be.an('array'); + expect(txArray).to.have.length(1); + }); + + it('should throw when transactions are only hashes', () => { + const invalidBlock: any = { + ...block, + transactions: ['0xabc'], + }; + + expect(() => BlockFactory.rlpEncode(invalidBlock)).to.throw( + 'Block transactions must include full transaction objects for RLP encoding', + ); + }); + }); +}); diff --git a/packages/server/tests/acceptance/debug.spec.ts b/packages/server/tests/acceptance/debug.spec.ts index 89cb6ab91a..2dd17766b2 100644 --- a/packages/server/tests/acceptance/debug.spec.ts +++ b/packages/server/tests/acceptance/debug.spec.ts @@ -65,6 +65,7 @@ describe('@debug API Acceptance Tests', function () { const DEBUG_TRACE_BLOCK_BY_HASH = 'debug_traceBlockByHash'; const DEBUG_TRACE_TRANSACTION = 'debug_traceTransaction'; const DEBUG_GET_RAW_BLOCK = 'debug_getRawBlock'; + const DEBUG_GET_RAW_HEADER = 'debug_getRawHeader'; const TRACER_CONFIGS = { CALL_TRACER_TOP_ONLY_FALSE: { tracer: TracerType.CallTracer, tracerConfig: { onlyTopCall: false } }, @@ -989,20 +990,69 @@ describe('@debug API Acceptance Tests', function () { }); }); - describe('debug_getRawBlock', async () => { - const toHex = (n) => '0x' + n.toString(16); - const hexToData = (buf) => `0x${Buffer.from(buf).toString('hex')}`; - const hexToQuantity = (buf) => { - if (buf.length === 0) return '0x0'; - return `0x${Buffer.from(buf).toString('hex').replace(/^0+/, '') || '0'}`; - }; + const toHex = (n) => '0x' + n.toString(16); + const hexToData = (buf) => `0x${Buffer.from(buf).toString('hex')}`; + const hexToQuantity = (buf) => { + if (buf.length === 0) return '0x0'; + return `0x${Buffer.from(buf).toString('hex').replace(/^0+/, '') || '0'}`; + }; + + const assertBlockInfoMatchesGetRawBlockInfo = (blockInfo, decodedRawBlock, headerOnly = false) => { + expect(hexToData(decodedRawBlock[0])).to.equal(blockInfo.parentHash); + expect(hexToData(decodedRawBlock[1])).to.equal(constants.EMPTY_ARRAY_HEX); + expect(hexToData(decodedRawBlock[2])).to.equal(constants.HEDERA_NODE_REWARD_ACCOUNT_ADDRESS); + expect(hexToData(decodedRawBlock[3])).to.equal(blockInfo.stateRoot); + expect(hexToData(decodedRawBlock[4])).to.equal(blockInfo.transactionsRoot); + expect(hexToData(decodedRawBlock[5])).to.equal(blockInfo.receiptsRoot); + expect(hexToData(decodedRawBlock[6])).to.equal(blockInfo.logsBloom); + expect(hexToQuantity(decodedRawBlock[7])).to.equal(blockInfo.difficulty); + expect(hexToQuantity(decodedRawBlock[8])).to.equal(blockInfo.number); + expect(hexToQuantity(decodedRawBlock[9])).to.equal(blockInfo.gasLimit); + expect(hexToQuantity(decodedRawBlock[10])).to.equal(blockInfo.gasUsed); + expect(hexToData(decodedRawBlock[11])).to.equal(blockInfo.timestamp); + expect(hexToData(decodedRawBlock[12])).to.equal(blockInfo.extraData); + expect(hexToData(decodedRawBlock[13])).to.equal(blockInfo.mixHash); + expect(hexToData(decodedRawBlock[14])).to.equal(blockInfo.nonce); + expect(hexToData(decodedRawBlock[15])).to.equal(blockInfo.baseFeePerGas); + expect(hexToData(decodedRawBlock[16])).to.equal(blockInfo.withdrawalsRoot); + + if (headerOnly) return; + + for (const [i, tx] of blockInfo.transactions.entries()) { + const decodedTx = ethers.Transaction.from(hexToData(decodedRawBlock[17][i])); + + expect(decodedTx.to?.toLowerCase()).to.equal(tx.to); + expect(decodedTx.data).to.equal(tx.input); + expect(toHex(decodedTx.nonce)).to.equal(tx.nonce); + expect(toHex(decodedTx.value)).to.equal(tx.value); + + if (decodedTx.signature) { + // handle ethereum transaction + expect(toHex(decodedTx.maxPriorityFeePerGas)).to.equal(tx.maxPriorityFeePerGas); + expect(toHex(decodedTx.maxFeePerGas)).to.equal(tx.maxFeePerGas); + expect(toHex(decodedTx.chainId)).to.equal(tx.chainId); + expect(decodedTx.signature.r).to.equal(tx.r); + expect(decodedTx.signature.s).to.equal(tx.s); + expect(toHex(decodedTx.signature.v - 27)).to.equal(tx.v); + } else { + // handle synthetic transaction + expect(toHex(decodedTx.gasLimit)).to.equal(tx.gas); + expect(toHex(decodedTx.gasPrice)).to.equal(tx.gasPrice); + } + } + + expect(hexToData(decodedRawBlock[18])).to.equal(constants.EMPTY_HEX); + expect(hexToData(decodedRawBlock[19])).to.equal(constants.EMPTY_HEX); + }; + describe('debug_getRawHeader', async () => { let blockInfo; + before(async () => { blockInfo = await relay.call('eth_getBlockByNumber', [numberTo0x(htsTransferBlockNumber), true]); }); - const assertBlockInfoMatchesGetRawBlockInfo = (blockInfo, decodedRawBlock) => { + const assertBlockInfoMatchesGetRawBlockInfo = (blockInfo, decodedRawBlock, headerOnly = false) => { expect(hexToData(decodedRawBlock[0])).to.equal(blockInfo.parentHash); expect(hexToData(decodedRawBlock[1])).to.equal(constants.EMPTY_ARRAY_HEX); expect(hexToData(decodedRawBlock[2])).to.equal('0x0000000000000000000000000000000000000321'); @@ -1020,8 +1070,8 @@ describe('@debug API Acceptance Tests', function () { expect(hexToData(decodedRawBlock[14])).to.equal(blockInfo.nonce); expect(hexToData(decodedRawBlock[15])).to.equal(blockInfo.baseFeePerGas); expect(hexToData(decodedRawBlock[16])).to.equal(blockInfo.withdrawalsRoot); - expect(hexToData(decodedRawBlock[18])).to.equal(constants.EMPTY_HEX); - expect(hexToData(decodedRawBlock[19])).to.equal(constants.EMPTY_HEX); + + if (headerOnly) return; for (const [i, tx] of blockInfo.transactions.entries()) { const decodedTx = ethers.Transaction.from(hexToData(decodedRawBlock[17][i])); @@ -1045,8 +1095,51 @@ describe('@debug API Acceptance Tests', function () { expect(toHex(decodedTx.gasPrice)).to.equal(tx.gasPrice); } } + + expect(hexToData(decodedRawBlock[18])).to.equal(constants.EMPTY_HEX); + expect(hexToData(decodedRawBlock[19])).to.equal(constants.EMPTY_HEX); }; + it('should return 0x for non-existent block', async () => { + const res = await relay.call(DEBUG_GET_RAW_HEADER, ['0xffffffffffff']); + expect(res).to.equal('0x'); + }); + + it('should be able to pass blockTags (earliest, latest, pending, finalized, safe)', async () => { + const earliestRes = await relay.call(DEBUG_GET_RAW_HEADER, ['earliest']); + const latestRes = await relay.call(DEBUG_GET_RAW_HEADER, ['latest']); + const pendingRes = await relay.call(DEBUG_GET_RAW_HEADER, ['pending']); + const finalizedRes = await relay.call(DEBUG_GET_RAW_HEADER, ['finalized']); + const safeRes = await relay.call(DEBUG_GET_RAW_HEADER, ['safe']); + expect(earliestRes).to.not.equal('0x'); + expect(latestRes).to.not.equal('0x'); + expect(pendingRes).to.not.equal('0x'); + expect(finalizedRes).to.not.equal('0x'); + expect(safeRes).to.not.equal('0x'); + }); + + it('should check whether the RLP block header has correct info using block number for debug_getRawHeader parameter', async () => { + const decodedRawBlock = RLP.decode( + await relay.call(DEBUG_GET_RAW_HEADER, [numberTo0x(htsTransferBlockNumber)]), + ); + + assertBlockInfoMatchesGetRawBlockInfo(blockInfo, decodedRawBlock, true); + }); + + it('should check whether the RLP block header has correct info using block hash for debug_getRawHeader parameter ', async () => { + const decodedRawBlock = RLP.decode(await relay.call(DEBUG_GET_RAW_HEADER, [htsTransferBlockHash])); + + assertBlockInfoMatchesGetRawBlockInfo(blockInfo, decodedRawBlock, true); + }); + }); + + describe('debug_getRawBlock', async () => { + let blockInfo; + + before(async () => { + blockInfo = await relay.call('eth_getBlockByNumber', [numberTo0x(htsTransferBlockNumber), true]); + }); + it('should return 0x for non-existent block', async () => { const res = await relay.call(DEBUG_GET_RAW_BLOCK, ['0xffffffffffff']); expect(res).to.equal('0x'); diff --git a/scripts/openrpc-json-updater/config.js b/scripts/openrpc-json-updater/config.js index 6f685a9ae4..32fed694dc 100644 --- a/scripts/openrpc-json-updater/config.js +++ b/scripts/openrpc-json-updater/config.js @@ -28,7 +28,6 @@ export const UNSUPPORTED_METHODS = [ ]; export const NOT_IMPLEMENTED_METHODS = [ - 'debug_getRawHeader', 'debug_getRawReceipts', 'debug_getRawTransaction', ];