diff --git a/docs/openrpc.json b/docs/openrpc.json index e898abc6f5..643f7758d9 100644 --- a/docs/openrpc.json +++ b/docs/openrpc.json @@ -1064,29 +1064,11 @@ }, "description": "The hash of the transaction to trace." }, - { - "name": "tracer", - "required": false, - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/TracerType" - }, - { - "$ref": "#/components/schemas/TracerConfig" - }, - { - "$ref": "#/components/schemas/TracerConfigWrapper" - } - ] - }, - "description": "Specifies the type of tracer or configuration object for the tracer." - }, { "name": "tracerConfig", "required": false, "schema": { - "$ref": "#/components/schemas/TracerConfig" + "$ref": "#/components/schemas/TracerConfigWrapper" }, "description": "Configuration object for the tracer." } diff --git a/packages/relay/src/index.ts b/packages/relay/src/index.ts index f08ef6ae55..a13d24165e 100644 --- a/packages/relay/src/index.ts +++ b/packages/relay/src/index.ts @@ -1,6 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 - -import { TracerType } from './lib/constants'; import { JsonRpcError, predefined } from './lib/errors/JsonRpcError'; import { MirrorNodeClientError } from './lib/errors/MirrorNodeClientError'; import WebSocketError from './lib/errors/WebSocketError'; @@ -10,9 +8,9 @@ import { IContractCallRequest, IGetLogsParams, INewFilterParams, - ITracerConfig, ITransactionReceipt, RequestDetails, + TransactionTracerConfig, } from './lib/types'; export { JsonRpcError, predefined, MirrorNodeClientError, WebSocketError }; @@ -22,8 +20,7 @@ export { Relay } from './lib/relay'; export interface Debug { traceTransaction: ( transactionIdOrHash: string, - tracer: TracerType, - tracerConfig: ITracerConfig, + tracerObject: TransactionTracerConfig, requestDetails: RequestDetails, ) => Promise; diff --git a/packages/relay/src/lib/debug.ts b/packages/relay/src/lib/debug.ts index 217305d335..6f8006711a 100644 --- a/packages/relay/src/lib/debug.ts +++ b/packages/relay/src/lib/debug.ts @@ -19,11 +19,11 @@ import { EntityTraceStateMap, ICallTracerConfig, IOpcodeLoggerConfig, - ITracerConfig, MirrorNodeContractResult, ParamType, RequestDetails, TraceBlockByNumberTxResult, + TransactionTracerConfig, } from './types'; /** @@ -106,26 +106,34 @@ export class DebugImpl implements Debug { @rpcMethod @rpcParamValidationRules({ 0: { type: ParamType.TRANSACTION_HASH_OR_ID, required: true }, - 1: { type: ParamType.COMBINED_TRACER_TYPE, required: false }, - 2: { type: ParamType.TRACER_CONFIG, required: false }, + 1: { type: ParamType.TRACER_CONFIG_WRAPPER, required: false }, }) + @rpcParamLayoutConfig(RPC_LAYOUT.custom((params) => [params[0], params[1]])) @cache(CacheService.getInstance(CACHE_LEVEL.L1)) async traceTransaction( transactionIdOrHash: string, - tracer: TracerType, - tracerConfig: ITracerConfig, + tracerObject: TransactionTracerConfig, requestDetails: RequestDetails, ): Promise { + if (tracerObject?.tracer === TracerType.PrestateTracer) { + throw predefined.INVALID_PARAMETER(1, 'Prestate tracer is not yet supported on debug_traceTransaction'); + } + if (this.logger.isLevelEnabled('trace')) { this.logger.trace(`${requestDetails.formattedRequestId} traceTransaction(${transactionIdOrHash})`); } + + //we use a wrapper since we accept a transaction where a second param with tracer/tracerConfig may not be provided + //and we will still default to opcodeLogger + const tracer = tracerObject?.tracer ?? TracerType.OpcodeLogger; + const tracerConfig = tracerObject?.tracerConfig ?? {}; + try { DebugImpl.requireDebugAPIEnabled(); if (tracer === TracerType.CallTracer) { return await this.callTracer(transactionIdOrHash, tracerConfig as ICallTracerConfig, requestDetails); - } else if (tracer === TracerType.OpcodeLogger) { - return await this.callOpcodeLogger(transactionIdOrHash, tracerConfig as IOpcodeLoggerConfig, requestDetails); } + return await this.callOpcodeLogger(transactionIdOrHash, tracerConfig as IOpcodeLoggerConfig, requestDetails); } catch (e) { throw this.common.genericErrorHandler(e); } diff --git a/packages/relay/src/lib/types/debug.ts b/packages/relay/src/lib/types/debug.ts index b4c69733ff..ff3bbe7235 100644 --- a/packages/relay/src/lib/types/debug.ts +++ b/packages/relay/src/lib/types/debug.ts @@ -1,18 +1,22 @@ // SPDX-License-Identifier: Apache-2.0 import { TracerType } from '../constants'; -import { ICallTracerConfig } from './ITracerConfig'; +import { ICallTracerConfig, ITracerConfig } from './ITracerConfig'; /** * Configuration object for block tracing operations. */ -export interface BlockTracerConfig { - /** The type of tracer to use for block tracing. */ +interface TracerConfig { + /** The type of tracer to use for tracing. */ tracer: TracerType; - /** Optional configuration for the call tracer. */ - tracerConfig?: ICallTracerConfig; + /** Optional configuration for the tracer. */ + tracerConfig?: T; } +// Public exports +export type BlockTracerConfig = TracerConfig; +export type TransactionTracerConfig = TracerConfig; + /** * Represents the state of an entity during a trace operation. */ diff --git a/packages/relay/src/lib/validators/objectTypes.ts b/packages/relay/src/lib/validators/objectTypes.ts index 6fab78c66a..34ae1f06cd 100644 --- a/packages/relay/src/lib/validators/objectTypes.ts +++ b/packages/relay/src/lib/validators/objectTypes.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -import { Validator } from '.'; +import { TracerType } from '../constants'; import { predefined } from '../errors/JsonRpcError'; import { ICallTracerConfig, @@ -9,6 +9,7 @@ import { IOpcodeLoggerConfig, ITracerConfigWrapper, } from '../types'; +import { Validator } from '.'; export const OBJECTS_VALIDATIONS: { [key: string]: IObjectSchema } = { blockHashObject: { @@ -305,4 +306,39 @@ export class TracerConfigWrapper extends DefaultValidation constructor(config: any) { super(OBJECTS_VALIDATIONS.tracerConfigWrapper, config); } + + validate() { + const valid = super.validate(); + + const { tracer, tracerConfig } = this.object; + + if (!tracerConfig) { + return valid; + } + + const callTracerKeys = Object.keys(OBJECTS_VALIDATIONS.callTracerConfig.properties); + const opcodeLoggerKeys = Object.keys(OBJECTS_VALIDATIONS.opcodeLoggerConfig.properties); + + const configKeys = Object.keys(tracerConfig); + const hasCallTracerKeys = configKeys.some((k) => callTracerKeys.includes(k)); + const hasOpcodeLoggerKeys = configKeys.some((k) => opcodeLoggerKeys.includes(k)); + + // we want to accept ICallTracerConfig only if the tracer is callTracer + // this config is not valid for opcodeLogger and vice versa + // accept only IOpcodeLoggerConfig with opcodeLogger tracer + if (hasCallTracerKeys && tracer === TracerType.OpcodeLogger) { + throw predefined.INVALID_PARAMETER( + 1, + `callTracer 'tracerConfig' for ${this.name()} is only valid when tracer=${TracerType.CallTracer}`, + ); + } + + if (hasOpcodeLoggerKeys && tracer !== TracerType.OpcodeLogger) { + throw predefined.INVALID_PARAMETER( + 1, + `opcodeLogger 'tracerConfig' for ${this.name()} is only valid when tracer=${TracerType.OpcodeLogger}`, + ); + } + return valid; + } } diff --git a/packages/relay/src/utils.ts b/packages/relay/src/utils.ts index cfe211f9e0..ba091526e2 100644 --- a/packages/relay/src/utils.ts +++ b/packages/relay/src/utils.ts @@ -8,10 +8,9 @@ import createHash from 'keccak'; import { Logger } from 'pino'; import { hexToASCII, prepend0x, strip0x } from './formatters'; -import constants, { TracerType } from './lib/constants'; +import constants from './lib/constants'; import { RPC_LAYOUT, RPC_PARAM_LAYOUT_KEY } from './lib/decorators'; -import { ITracerConfig, RequestDetails } from './lib/types'; -import { TYPES } from './lib/validators'; +import { RequestDetails } from './lib/types'; export class Utils { public static readonly IP_ADDRESS_REGEX = /\b((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}\b/g; @@ -174,31 +173,6 @@ export class Utils { public static arrangeRpcParams(method: Function, rpcParams: any[] = [], requestDetails: RequestDetails): any[] { const layout = method[RPC_PARAM_LAYOUT_KEY]; - if (method.name === 'traceTransaction') { - const transactionIdOrHash = rpcParams[0]; - let tracer: TracerType = TracerType.OpcodeLogger; - let tracerConfig: ITracerConfig = {}; - - // Second param can be either a TracerType string, or an object for TracerConfig or TracerConfigWrapper - if (TYPES.tracerType.test(rpcParams[1])) { - tracer = rpcParams[1]; - if (TYPES.tracerConfig.test(rpcParams[2])) { - tracerConfig = rpcParams[2]; - } - } else if (TYPES.tracerConfig.test(rpcParams[1])) { - tracerConfig = rpcParams[1]; - } else if (TYPES.tracerConfigWrapper.test(rpcParams[1])) { - if (TYPES.tracerType.test(rpcParams[1].tracer)) { - tracer = rpcParams[1].tracer; - } - if (TYPES.tracerConfig.test(rpcParams[1].tracerConfig)) { - tracerConfig = rpcParams[1].tracerConfig; - } - } - - return [transactionIdOrHash, tracer, tracerConfig, requestDetails]; - } - // Method only needs requestDetails if (layout === RPC_LAYOUT.REQUEST_DETAILS_ONLY) { return [requestDetails]; diff --git a/packages/relay/tests/lib/debug.spec.ts b/packages/relay/tests/lib/debug.spec.ts index d2d8a68a78..b4513807cc 100644 --- a/packages/relay/tests/lib/debug.spec.ts +++ b/packages/relay/tests/lib/debug.spec.ts @@ -50,6 +50,8 @@ describe('Debug API Test Suite', async function () { const tracerConfigFalse = { onlyTopCall: false }; const callTracer: TracerType = TracerType.CallTracer; const opcodeLogger: TracerType = TracerType.OpcodeLogger; + const tracerObjectCallTracerFalse = { tracer: callTracer, tracerConfig: tracerConfigFalse }; + const tracerObjectCallTracerTrue = { tracer: callTracer, tracerConfig: tracerConfigTrue }; const CONTRACTS_RESULTS_OPCODES = `contracts/results/${transactionHash}/opcodes`; const CONTARCTS_RESULTS_ACTIONS = `contracts/results/${transactionHash}/actions`; const CONTRACTS_RESULTS_BY_HASH = `contracts/results/${transactionHash}`; @@ -362,8 +364,7 @@ describe('Debug API Test Suite', async function () { it('should successfully debug a transaction', async function () { const traceTransaction = await debugService.traceTransaction( transactionHash, - callTracer, - tracerConfigFalse, + tracerObjectCallTracerFalse, requestDetails, ); expect(traceTransaction).to.exist; @@ -396,8 +397,7 @@ describe('Debug API Test Suite', async function () { const result = await debugService.traceTransaction( transactionHash, - callTracer, - tracerConfigFalse, + tracerObjectCallTracerFalse, requestDetails, ); @@ -418,8 +418,7 @@ describe('Debug API Test Suite', async function () { }; const result = await debugService.traceTransaction( transactionHash, - callTracer, - tracerConfigTrue, + tracerObjectCallTracerTrue, requestDetails, ); @@ -431,8 +430,7 @@ describe('Debug API Test Suite', async function () { const result = await debugService.traceTransaction( transactionHash, - callTracer, - tracerConfigFalse, + tracerObjectCallTracerFalse, requestDetails, ); @@ -472,7 +470,8 @@ describe('Debug API Test Suite', async function () { })), }; - const result = await debugService.traceTransaction(transactionHash, opcodeLogger, config, requestDetails); + const tracerObject = { tracer: opcodeLogger, tracerConfig: config }; + const result = await debugService.traceTransaction(transactionHash, tracerObject, requestDetails); expect(result).to.deep.equal(expectedResult); }); @@ -508,8 +507,7 @@ describe('Debug API Test Suite', async function () { await RelayAssertions.assertRejection(expectedError, debugService.traceTransaction, true, debugService, [ nonExistentTransactionHash, - callTracer, - tracerConfigTrue, + tracerObjectCallTracerTrue, requestDetails, ]); }); diff --git a/packages/relay/tests/lib/utils.spec.ts b/packages/relay/tests/lib/utils.spec.ts index d3ea954e6c..02401cd0ae 100644 --- a/packages/relay/tests/lib/utils.spec.ts +++ b/packages/relay/tests/lib/utils.spec.ts @@ -252,67 +252,34 @@ describe('Utils', () => { // Define test cases as [testName, params, expectedTracer, expectedConfig] const testCases = [ - ['should handle traceTransaction with only transaction hash', [], TracerType.OpcodeLogger, {}], - [ - 'should handle traceTransaction with tracer type as second parameter', - [TracerType.CallTracer], - TracerType.CallTracer, - {}, - ], - [ - 'should handle traceTransaction with tracer type and config', - [TracerType.CallTracer, tracerConfig], - TracerType.CallTracer, - tracerConfig, - ], - [ - 'should handle traceTransaction with tracerConfig as second parameter', - [tracerConfig], - TracerType.OpcodeLogger, - tracerConfig, - ], - [ - 'should handle traceTransaction with tracerConfigWrapper as second parameter', - [ - { - tracer: TracerType.CallTracer, - tracerConfig: tracerConfig, - }, - ], - TracerType.CallTracer, - tracerConfig, - ], - [ - 'should handle traceTransaction with partial tracerConfigWrapper (only tracer)', - [ - { - tracer: TracerType.CallTracer, - }, - ], - TracerType.CallTracer, - {}, - ], - [ - 'should handle traceTransaction with partial tracerConfigWrapper (only tracerConfig)', - [ - { - tracerConfig, - }, - ], - TracerType.OpcodeLogger, - tracerConfig, - ], + { + name: 'should handle traceTransaction with only transaction hash', + params: [], + expected: [transactionHash, requestDetails], + }, + { + name: 'should handle traceTransaction with tracerConfigWrapper as second parameter', + params: [{ tracer: TracerType.CallTracer, tracerConfig: tracerConfig }], + expected: [transactionHash, { tracer: TracerType.CallTracer, tracerConfig: tracerConfig }, requestDetails], + }, + { + name: 'should handle traceTransaction with partial tracerConfigWrapper (only tracer)', + params: [{ tracer: TracerType.CallTracer }], + expected: [transactionHash, { tracer: TracerType.CallTracer }, requestDetails], + }, + { + name: 'should handle traceTransaction with empty tracerConfig', + params: [{ tracer: TracerType.CallTracer, tracerConfig: {} }], + expected: [transactionHash, { tracer: TracerType.CallTracer, tracerConfig: {} }, requestDetails], + }, ]; // Loop through test cases and create tests - testCases.forEach(([testName, params, expectedTracer, expectedConfig]) => { - it(testName as string, () => { - const result = Utils.arrangeRpcParams( - traceTransactionMethod, - [transactionHash, ...(params as any[])], - requestDetails, - ); - expect(result).to.deep.equal([transactionHash, expectedTracer, expectedConfig, requestDetails]); + testCases.forEach(({ name, params, expected }) => { + it(name, () => { + const result = Utils.arrangeRpcParams(traceTransactionMethod, [transactionHash, ...params], requestDetails); + + expect(result).to.deep.equal(expected); }); }); }); diff --git a/packages/server/tests/integration/server.spec.ts b/packages/server/tests/integration/server.spec.ts index ad8e6b35de..70dc17b590 100644 --- a/packages/server/tests/integration/server.spec.ts +++ b/packages/server/tests/integration/server.spec.ts @@ -11,7 +11,6 @@ import { DebugImpl } from '@hashgraph/json-rpc-relay/dist/lib/debug'; import { CacheService } from '@hashgraph/json-rpc-relay/dist/lib/services/cacheService/cacheService'; import { Validator } from '@hashgraph/json-rpc-relay/dist/lib/validators'; import * as Constants from '@hashgraph/json-rpc-relay/dist/lib/validators'; -import { JsonRpcError } from '@hashgraph/json-rpc-relay/src'; import { CommonService } from '@hashgraph/json-rpc-relay/src/lib/services'; import Axios, { AxiosInstance } from 'axios'; import { expect } from 'chai'; @@ -2479,7 +2478,7 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, TracerType.CallTracer, { onlyTopCall: true }], + params: [contractHash1, { tracer: TracerType.CallTracer, tracerConfig: { onlyTopCall: true } }], id: 1, }), ).to.not.throw; @@ -2492,8 +2491,10 @@ describe('RPC Server', function () { method: 'debug_traceTransaction', params: [ contractHash1, - TracerType.OpcodeLogger, - { disableStack: false, disableStorage: false, enableMemory: true }, + { + tracer: TracerType.OpcodeLogger, + tracerConfig: { disableStack: false, disableStorage: false, enableMemory: true }, + }, ], id: 1, }), @@ -2516,7 +2517,7 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, TracerType.CallTracer], + params: [contractHash1, { tracer: TracerType.CallTracer }], id: '2', }), ).to.not.throw; @@ -2527,7 +2528,7 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, TracerType.CallTracer, {}], + params: [contractHash1, { tracer: TracerType.CallTracer, tracerConfig: {} }], id: '2', }), ).to.not.throw; @@ -2544,39 +2545,6 @@ describe('RPC Server', function () { ).to.not.throw; }); - it('should execute with unknown property in TracerConfig', async () => { - expect( - await testClient.post('/', { - jsonrpc: '2.0', - method: 'debug_traceTransaction', - params: [contractHash1, { disableMemory: true, disableStack: true }], - id: '2', - }), - ).to.not.throw; - }); - - it('should execute with unknown property in TracerConfigWrapper.tracerConfig', async () => { - expect( - await testClient.post('/', { - jsonrpc: '2.0', - method: 'debug_traceTransaction', - params: [contractHash1, { tracerConfig: { disableMemory: true, disableStorage: true } }], - id: '2', - }), - ).to.not.throw; - }); - - it('should execute with empty TracerConfigWrapper.tracerConfig', async function () { - expect( - await testClient.post('/', { - jsonrpc: '2.0', - method: 'debug_traceTransaction', - params: [contractHash1, { tracerConfig: {} }], - id: '2', - }), - ).to.not.throw; - }); - it('should fail with missing transaction hash', async () => { try { await testClient.post('/', { @@ -2597,7 +2565,7 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: ['invalidHash', TracerType.OpcodeLogger], + params: ['invalidHash', { tracer: TracerType.OpcodeLogger }], id: '2', }); @@ -2625,7 +2593,7 @@ describe('RPC Server', function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 1: The value passed is not valid: invalidTracerType. ${Validator.TYPES.tracerType.error} OR ${Validator.TYPES.tracerConfig.error} OR ${Validator.TYPES.tracerConfigWrapper.error}`, + `Invalid parameter 1: Expected TracerConfigWrapper which contains a valid TracerType and/or TracerConfig, value: invalidTracerType`, ); } }); @@ -2635,7 +2603,7 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, TracerType.CallTracer, { invalidConfig: true }], + params: [contractHash1, { tracer: TracerType.CallTracer, tracerConfig: { invalidConfig: true } }], id: '2', }); @@ -2644,7 +2612,7 @@ describe('RPC Server', function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 2: ${Validator.TYPES.tracerConfig.error}, value: ${JSON.stringify({ + `Invalid parameter 'tracerConfig' for TracerConfigWrapper: Expected TracerConfig, value: ${JSON.stringify({ invalidConfig: true, })}`, ); @@ -2656,7 +2624,10 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, { enableMemory: 'must be a boolean' }], + params: [ + contractHash1, + { tracer: TracerType.OpcodeLogger, tracerConfig: { enableMemory: 'must be a boolean' } }, + ], id: '2', }); @@ -2665,7 +2636,9 @@ describe('RPC Server', function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 'enableMemory' for OpcodeLoggerConfig: ${Validator.TYPES.boolean.error}, value: must be a boolean`, + `Invalid parameter 'tracerConfig' for TracerConfigWrapper: Expected TracerConfig, value: ${JSON.stringify({ + enableMemory: 'must be a boolean', + })}`, ); } }); @@ -2675,7 +2648,10 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, { disableStack: 'must be a boolean' }], + params: [ + contractHash1, + { tracer: TracerType.OpcodeLogger, tracerConfig: { disableStack: 'must be a boolean' } }, + ], id: '2', }); @@ -2684,7 +2660,9 @@ describe('RPC Server', function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 'disableStack' for OpcodeLoggerConfig: ${Validator.TYPES.boolean.error}, value: must be a boolean`, + `Invalid parameter 'tracerConfig' for TracerConfigWrapper: Expected TracerConfig, value: ${JSON.stringify({ + disableStack: 'must be a boolean', + })}`, ); } }); @@ -2694,7 +2672,10 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, { disableStorage: 'must be a boolean' }], + params: [ + contractHash1, + { tracer: TracerType.OpcodeLogger, tracerConfig: { disableStorage: 'must be a boolean' } }, + ], id: '2', }); @@ -2703,7 +2684,9 @@ describe('RPC Server', function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 'disableStorage' for OpcodeLoggerConfig: ${Validator.TYPES.boolean.error}, value: must be a boolean`, + `Invalid parameter 'tracerConfig' for TracerConfigWrapper: Expected TracerConfig, value: ${JSON.stringify({ + disableStorage: 'must be a boolean', + })}`, ); } }); @@ -2751,7 +2734,7 @@ describe('RPC Server', function () { await testClient.post('/', { jsonrpc: '2.0', method: 'debug_traceTransaction', - params: [contractHash1, TracerType.CallTracer, { invalidProperty: true }], + params: [contractHash1, { tracer: TracerType.CallTracer, tracerConfig: { invalidProperty: true } }], id: '2', }); @@ -2760,7 +2743,9 @@ describe('RPC Server', function () { BaseTest.invalidParamError( error.response, Validator.ERROR_CODE, - `Invalid parameter 2: ${Validator.TYPES.tracerConfig.error}, value: ${JSON.stringify({ + `Invalid parameter 'tracerConfig' for TracerConfigWrapper: ${ + Validator.TYPES.tracerConfig.error + }, value: ${JSON.stringify({ invalidProperty: true, })}`, );