diff --git a/.changeset/famous-ties-know.md b/.changeset/famous-ties-know.md new file mode 100644 index 000000000..92d358d1c --- /dev/null +++ b/.changeset/famous-ties-know.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/edr": minor +--- + +Normalise JSON-RPC format for rpcDebugTrace on the EDR side. diff --git a/crates/edr_napi/src/provider.rs b/crates/edr_napi/src/provider.rs index 372d0c812..08b488630 100644 --- a/crates/edr_napi/src/provider.rs +++ b/crates/edr_napi/src/provider.rs @@ -209,6 +209,7 @@ impl Provider { trace, contract_decoder: Arc::clone(&self.contract_decoder), }); + Response { solidity_trace, data, diff --git a/crates/edr_napi/test/provider.ts b/crates/edr_napi/test/provider.ts index 3fc3e37e2..97691ca5d 100644 --- a/crates/edr_napi/test/provider.ts +++ b/crates/edr_napi/test/provider.ts @@ -405,6 +405,104 @@ describe("Provider", () => { const rawTraces = traceCallResponse.traces; assert.lengthOf(rawTraces, 1); }); + + it("should have its JSON-RPC format normalised when debug_traceTransaction is used", async function () { + const provider = await Provider.withConfig( + context, + providerConfig, + loggerConfig, + {}, + (_event: SubscriptionEvent) => {} + ); + + const sendTxResponse = await provider.handleRequest( + JSON.stringify({ + id: 1, + jsonrpc: "2.0", + method: "eth_sendTransaction", + params: [ + { + from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + // PUSH1 0x42 + // PUSH0 + // MSTORE + // PUSH1 0x20 + // PUSH0 + // RETURN + data: "0x60425f5260205ff3", + gas: "0x" + 1_000_000n.toString(16), + }, + ], + }) + ); + + let responseData; + + if (typeof sendTxResponse.data === "string") { + responseData = JSON.parse(sendTxResponse.data); + } else { + responseData = sendTxResponse.data; + } + + const txHash = responseData.result; + + const traceTransactionResponse = await provider.handleRequest( + JSON.stringify({ + id: 1, + jsonrpc: "2.0", + method: "debug_traceTransaction", + params: [txHash], + }) + ); + + let edrTrace; + if (typeof traceTransactionResponse.data === "string") { + edrTrace = JSON.parse(traceTransactionResponse.data).result; + } else { + edrTrace = traceTransactionResponse.data.result; + } + + assertJsonRpcFormatNormalised(edrTrace); + }); + + it("should have its JSON-RPC format normalised when debug_traceCall is used", async function () { + const provider = await Provider.withConfig( + context, + providerConfig, + loggerConfig, + {}, + (_event: SubscriptionEvent) => {} + ); + + const traceCallResponse = await provider.handleRequest( + JSON.stringify({ + id: 1, + jsonrpc: "2.0", + method: "debug_traceCall", + params: [ + { + from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + // PUSH1 0x42 + // PUSH0 + // MSTORE + // PUSH1 0x20 + // PUSH0 + // RETURN + data: "0x60425f5260205ff3", + gas: "0x" + 1_000_000n.toString(16), + }, + ], + }) + ); + + let edrTrace; + if (typeof traceCallResponse.data === "string") { + edrTrace = JSON.parse(traceCallResponse.data).result; + } else { + edrTrace = traceCallResponse.data.result; + } + assertJsonRpcFormatNormalised(edrTrace); + }); }); }); @@ -415,3 +513,48 @@ function assertEqualMemory(stepMemory: Buffer | undefined, expected: Buffer) { assert.isTrue(stepMemory.equals(expected)); } + +function assertJsonRpcFormatNormalised(trace: any) { + assert.isBoolean(trace.failed); + assert.typeOf(trace.gas, "number"); + assert.isString(trace.returnValue); + assert.isArray(trace.structLogs); + + trace.structLogs.forEach((log: any) => { + assert.typeOf(log.pc, "number"); + assert.typeOf(log.op, "string"); + assert.typeOf(log.gas, "number"); + assert.typeOf(log.gasCost, "number"); + assert.typeOf(log.depth, "number"); + assert.typeOf(log.memSize, "number"); + + if ("stack" in log) { + assert.isArray(log.stack); + log.stack?.forEach((item: any) => { + assert.isString(item); + // assert.isFalse(item.startsWith("0x")); + }); + } + + if ("memory" in log) { + assert.isArray(log.memory); + log.memory?.forEach((item: any) => { + assert.isString(item); + }); + } + + if ("storage" in log) { + assert.isObject(log.storage); + Object.entries(log.storage!).forEach(([key, value]) => { + assert.isString(key); + assert.isString(value); + // assert.isFalse(key.startsWith("0x")); + // assert.isFalse(value.startsWith("0x")); + }); + } + + if ("error" in log) { + assert.isString(log.error); + } + }); +} diff --git a/crates/edr_provider/src/requests/debug.rs b/crates/edr_provider/src/requests/debug.rs index 760643b30..c356a411e 100644 --- a/crates/edr_provider/src/requests/debug.rs +++ b/crates/edr_provider/src/requests/debug.rs @@ -1,4 +1,5 @@ use core::fmt::Debug; +use std::collections::HashMap; use edr_eth::{BlockSpec, B256}; use edr_evm::{state::StateOverrides, trace::Trace, DebugTraceResult, DebugTraceResultWithTraces}; @@ -19,7 +20,7 @@ pub fn handle_debug_trace_transaction, transaction_hash: B256, config: Option, -) -> Result<(DebugTraceResult, Vec), ProviderError> { +) -> Result<(RpcDebugTraceResult, Vec), ProviderError> { let DebugTraceResultWithTraces { result, traces } = data .debug_trace_transaction( &transaction_hash, @@ -32,6 +33,8 @@ pub fn handle_debug_trace_transaction error, })?; + let result = normalise_rpc_debug_trace(result); + Ok((result, traces)) } @@ -40,7 +43,7 @@ pub fn handle_debug_trace_call, config: Option, -) -> Result<(DebugTraceResult, Vec), ProviderError> { +) -> Result<(RpcDebugTraceResult, Vec), ProviderError> { let block_spec = resolve_block_spec_for_call_request(block_spec); validate_call_request(data.spec_id(), &call_request, &block_spec)?; @@ -53,6 +56,8 @@ pub fn handle_debug_trace_call for edr_evm::DebugTraceConfig { } } } + +/// This is the JSON-RPC Debug trace format +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcDebugTraceResult { + pub failed: bool, + pub gas: u64, + // Adding pass and gass used since Hardhat tests still + // depend on them + pub pass: bool, + pub gas_used: u64, + pub return_value: String, + pub struct_logs: Vec, +} + +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcDebugTraceLogItem { + /// Program Counter + pub pc: u64, + /// Name of the operation + pub op: String, + /// Name of the operation (Needed for Hardhat tests) + pub op_name: String, + /// Gas left before executing this operation as hex number. + pub gas: u64, + /// Gas cost of this operation as hex number. + pub gas_cost: u64, + /// Array of all values (hex numbers) on the stack + #[serde(skip_serializing_if = "Option::is_none")] + pub stack: Option>, + /// Depth of the call stack + pub depth: u64, + /// Size of memory array + pub mem_size: u64, + /// Description of an error as a hex string. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Array of all allocated values as hex strings. + #[serde(skip_serializing_if = "Option::is_none")] + pub memory: Option>, + /// Map of all stored values with keys and values encoded as hex strings. + #[serde(skip_serializing_if = "Option::is_none")] + pub storage: Option>, +} + +// Rust port of https://github.com/NomicFoundation/hardhat/blob/024d72b09c6edefb00c012e9514a0948c255d0ab/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts#L176 +/// This normalization is done because this is the format Hardhat expects +fn normalise_rpc_debug_trace(trace: DebugTraceResult) -> RpcDebugTraceResult { + let mut struct_logs = Vec::new(); + + for log in trace.logs { + let rpc_log = RpcDebugTraceLogItem { + pc: log.pc, + op: log.op_name.clone(), + op_name: log.op_name, + gas: u64::from_str_radix(log.gas.trim_start_matches("0x"), 16).unwrap_or(0), + gas_cost: u64::from_str_radix(log.gas_cost.trim_start_matches("0x"), 16).unwrap_or(0), + stack: log.stack.map(|values| { + values + .into_iter() + // Removing this trim temporarily as the Hardhat test assumes 0x is there + // .map(|value| value.trim_start_matches("0x").to_string()) + .collect() + }), + depth: log.depth, + mem_size: log.mem_size, + error: log.error, + memory: log.memory, + storage: log.storage.map(|storage| { + storage + .into_iter() + // Removing this trim temporarily as the Hardhat test assumes 0x is there + // .map(|(key, value)| { + // let stripped_key = key.strip_prefix("0x").unwrap_or(&key).to_string(); + // let stripped_value = + // value.strip_prefix("0x").unwrap_or(&value).to_string(); + // (stripped_key, stripped_value) + // }) + .collect() + }), + }; + + struct_logs.push(rpc_log); + } + + // REVM trace adds initial STOP that Hardhat doesn't expect + if !struct_logs.is_empty() && struct_logs[0].op == "STOP" { + struct_logs.remove(0); + } + + let return_value = trace + .output + .map(|b| b.to_string().trim_start_matches("0x").to_string()) + .unwrap_or_default(); + + RpcDebugTraceResult { + failed: !trace.pass, + gas: trace.gas_used, + pass: trace.pass, + gas_used: trace.gas_used, + return_value, + struct_logs, + } +}