From b379762d0547958d1b16488a15e98b89a74ee738 Mon Sep 17 00:00:00 2001 From: elmattic Date: Fri, 24 Oct 2025 15:15:50 +0200 Subject: [PATCH 01/15] Add type definitions --- src/rpc/methods/eth.rs | 66 ++++++++++++++++++++++++++++++++++++++++++ src/rpc/mod.rs | 1 + 2 files changed, 67 insertions(+) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index a59703f6bcac..cb2a0698f6ea 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -449,6 +449,54 @@ impl ExtBlockNumberOrHash { } } +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct EthTransactionCall { + from: Option, + to: Option, + gas: Option, + gas_price: Option, + value: Option, + data: Option, +} + +lotus_json_with_self!(EthTransactionCall); + +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum EthTraceType { + Trace, + StateDiff, +} + +lotus_json_with_self!(EthTraceType); + +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct EthTransactionTrace { + name: String, +} + +lotus_json_with_self!(EthTransactionTrace); + +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthVmTrace { + code: EthBytes, + //ops: Vec, +} + +lotus_json_with_self!(EthVmTrace); + +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EthTraceResults { + output: Option, + state_diff: Option, + trace: Vec, + vm_trace: Option, +} + +lotus_json_with_self!(EthTraceResults); + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] // try a Vec, then a Vec pub enum Transactions { @@ -3314,6 +3362,24 @@ async fn trace_block( Ok(all_traces) } +pub enum EthTraceCall {} +impl RpcMethod<3> for EthTraceCall { + const NAME: &'static str = "Filecoin.EthTraceCall"; + const NAME_ALIAS: Option<&'static str> = Some("trace_call"); + const N_REQUIRED_PARAMS: usize = 1; + const PARAM_NAMES: [&'static str; 3] = ["transaction", "traceTypes", "blockParam"]; + const API_PATHS: BitFlags = ApiPaths::all(); + const PERMISSION: Permission = Permission::Read; + type Params = (EthTransactionCall, Vec, ExtBlockNumberOrHash); + type Ok = Vec; + async fn handle( + ctx: Ctx, + (transaction, trace_types, block_param): Self::Params, + ) -> Result { + Ok(vec![]) + } +} + pub enum EthTraceTransaction {} impl RpcMethod<1> for EthTraceTransaction { const NAME: &'static str = "Filecoin.EthTraceTransaction"; diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 460a0121b252..ac4e86718a8b 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -130,6 +130,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::eth::EthSubscribe); $callback!($crate::rpc::eth::EthSyncing); $callback!($crate::rpc::eth::EthTraceBlock); + $callback!($crate::rpc::eth::EthTraceCall); $callback!($crate::rpc::eth::EthTraceFilter); $callback!($crate::rpc::eth::EthTraceTransaction); $callback!($crate::rpc::eth::EthTraceReplayBlockTransactions); From d3ae43782b68bd7c92e1fa3a673ce2d4d23cfc07 Mon Sep 17 00:00:00 2001 From: elmattic Date: Fri, 24 Oct 2025 15:39:53 +0200 Subject: [PATCH 02/15] Start impl of method (wip) --- src/rpc/methods/eth.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index cb2a0698f6ea..8a21eb0bc84f 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -449,18 +449,6 @@ impl ExtBlockNumberOrHash { } } -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct EthTransactionCall { - from: Option, - to: Option, - gas: Option, - gas_price: Option, - value: Option, - data: Option, -} - -lotus_json_with_self!(EthTransactionCall); - #[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum EthTraceType { @@ -3367,15 +3355,29 @@ impl RpcMethod<3> for EthTraceCall { const NAME: &'static str = "Filecoin.EthTraceCall"; const NAME_ALIAS: Option<&'static str> = Some("trace_call"); const N_REQUIRED_PARAMS: usize = 1; - const PARAM_NAMES: [&'static str; 3] = ["transaction", "traceTypes", "blockParam"]; + const PARAM_NAMES: [&'static str; 3] = ["tx", "traceTypes", "blockParam"]; const API_PATHS: BitFlags = ApiPaths::all(); const PERMISSION: Permission = Permission::Read; - type Params = (EthTransactionCall, Vec, ExtBlockNumberOrHash); + type Params = (EthCallMessage, Vec, BlockNumberOrHash); type Ok = Vec; async fn handle( ctx: Ctx, - (transaction, trace_types, block_param): Self::Params, + (tx, trace_types, block_param): Self::Params, ) -> Result { + // dbg!(&tx); + // dbg!(&trace_types); + // dbg!(&block_param); + + let msg = Message::try_from(tx)?; + let ts = tipset_by_block_number_or_hash( + ctx.chain_store(), + block_param, + ResolveNullTipset::TakeOlder, + )?; + let invoke_result = apply_message(&ctx, Some(ts), msg.clone()).await?; + + dbg!(invoke_result); + Ok(vec![]) } } From c8af63512befe096de8a0262a2742c36d441ff24 Mon Sep 17 00:00:00 2001 From: elmattic Date: Sat, 25 Oct 2025 09:34:12 +0200 Subject: [PATCH 03/15] Add comments and doc comments --- src/rpc/methods/eth.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 8a21eb0bc84f..c9bfc8709f7f 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -450,9 +450,15 @@ impl ExtBlockNumberOrHash { } #[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "camelCase")] pub enum EthTraceType { + /// Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`) + /// with details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`. Trace, + /// Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`) + /// caused by the simulated transaction. + /// + /// It shows `"from"` and `"to"` values for modified fields, using `"+"`, `"-"`, or `"="` for code changes. StateDiff, } @@ -480,6 +486,8 @@ pub struct EthTraceResults { output: Option, state_diff: Option, trace: Vec, + // This should always be empty since we don't support `vmTrace` atm (this + // would likely need changes in the FEVM) vm_trace: Option, } @@ -3364,6 +3372,11 @@ impl RpcMethod<3> for EthTraceCall { ctx: Ctx, (tx, trace_types, block_param): Self::Params, ) -> Result { + // Note: tx.to should not be null, it should always be set to something + // (contract address or EOA) + + // Note: Should we support nonce? + // dbg!(&tx); // dbg!(&trace_types); // dbg!(&block_param); From e9aeec262847d85f7ffa1d4f62ec36e0eb290d5a Mon Sep 17 00:00:00 2001 From: elmattic Date: Sat, 25 Oct 2025 09:49:16 +0200 Subject: [PATCH 04/15] Use non-empty vec --- src/rpc/methods/eth.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index c9bfc8709f7f..a2b8da3d9089 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -75,6 +75,8 @@ use std::sync::{Arc, LazyLock}; use tracing::log; use utils::{decode_payload, lookup_eth_address}; +use nunny::Vec as NonEmpty; + static FOREST_TRACE_FILTER_MAX_RESULT: LazyLock = LazyLock::new(|| env_or_default("FOREST_TRACE_FILTER_MAX_RESULT", 500)); @@ -3366,7 +3368,7 @@ impl RpcMethod<3> for EthTraceCall { const PARAM_NAMES: [&'static str; 3] = ["tx", "traceTypes", "blockParam"]; const API_PATHS: BitFlags = ApiPaths::all(); const PERMISSION: Permission = Permission::Read; - type Params = (EthCallMessage, Vec, BlockNumberOrHash); + type Params = (EthCallMessage, NonEmpty, BlockNumberOrHash); type Ok = Vec; async fn handle( ctx: Ctx, From 4a38045d42adc13d288016d0ccf4da2c0b11f458 Mon Sep 17 00:00:00 2001 From: elmattic Date: Sat, 25 Oct 2025 11:53:23 +0200 Subject: [PATCH 05/15] Set output to results --- src/rpc/methods/eth.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index a2b8da3d9089..d239afc6e6d0 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -482,7 +482,7 @@ pub struct EthVmTrace { lotus_json_with_self!(EthVmTrace); -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct EthTraceResults { output: Option, @@ -3360,6 +3360,21 @@ async fn trace_block( Ok(all_traces) } +fn get_output(msg: &Message, invoke_result: ApiInvocResult) -> Result { + if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR { + Ok(EthBytes::default()) + } else { + let msg_rct = invoke_result.msg_rct.context("no message receipt")?; + let return_data = msg_rct.return_data(); + if return_data.is_empty() { + Ok(Default::default()) + } else { + let bytes = decode_payload(&return_data, CBOR)?; + Ok(bytes) + } + } +} + pub enum EthTraceCall {} impl RpcMethod<3> for EthTraceCall { const NAME: &'static str = "Filecoin.EthTraceCall"; @@ -3369,7 +3384,7 @@ impl RpcMethod<3> for EthTraceCall { const API_PATHS: BitFlags = ApiPaths::all(); const PERMISSION: Permission = Permission::Read; type Params = (EthCallMessage, NonEmpty, BlockNumberOrHash); - type Ok = Vec; + type Ok = EthTraceResults; async fn handle( ctx: Ctx, (tx, trace_types, block_param): Self::Params, @@ -3390,10 +3405,15 @@ impl RpcMethod<3> for EthTraceCall { ResolveNullTipset::TakeOlder, )?; let invoke_result = apply_message(&ctx, Some(ts), msg.clone()).await?; + dbg!(&invoke_result); - dbg!(invoke_result); + let mut trace_results: EthTraceResults = Default::default(); + if trace_types.contains(&EthTraceType::Trace) { + let output = get_output(&msg, invoke_result)?; + trace_results.output = Some(output); + } - Ok(vec![]) + Ok(trace_results) } } From cb00829b1ce4bcf116630a6d24b87cacc994608d Mon Sep 17 00:00:00 2001 From: elmattic Date: Sat, 25 Oct 2025 12:08:25 +0200 Subject: [PATCH 06/15] Fix logic --- src/rpc/methods/eth.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index d239afc6e6d0..2f38b61c06e3 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3408,9 +3408,11 @@ impl RpcMethod<3> for EthTraceCall { dbg!(&invoke_result); let mut trace_results: EthTraceResults = Default::default(); + let output = get_output(&msg, invoke_result)?; + // output is always present, should we remove option? + trace_results.output = Some(output); if trace_types.contains(&EthTraceType::Trace) { - let output = get_output(&msg, invoke_result)?; - trace_results.output = Some(output); + // Built trace objects } Ok(trace_results) From 07295402a01f1d8e25bbafbe74b31dd8e9c0c558 Mon Sep 17 00:00:00 2001 From: elmattic Date: Sat, 25 Oct 2025 17:31:35 +0200 Subject: [PATCH 07/15] Add building of trace entries --- src/rpc/methods/eth.rs | 59 ++++++++++++++++++++++++++++------ src/rpc/methods/state/types.rs | 44 +++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 2f38b61c06e3..4151353e9498 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -33,7 +33,7 @@ use crate::rpc::eth::filter::{ }; use crate::rpc::eth::types::{EthBlockTrace, EthTrace}; use crate::rpc::eth::utils::decode_revert_reason; -use crate::rpc::state::ApiInvocResult; +use crate::rpc::state::{Action, ApiInvocResult, ExecutionTrace, ResultData, TraceEntry}; use crate::rpc::types::{ApiTipsetKey, EventEntry, MessageLookup}; use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod}; use crate::rpc::{EthEventHandler, LOOKBACK_NO_LIMIT}; @@ -466,13 +466,6 @@ pub enum EthTraceType { lotus_json_with_self!(EthTraceType); -#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct EthTransactionTrace { - name: String, -} - -lotus_json_with_self!(EthTransactionTrace); - #[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct EthVmTrace { @@ -487,7 +480,7 @@ lotus_json_with_self!(EthVmTrace); pub struct EthTraceResults { output: Option, state_diff: Option, - trace: Vec, + trace: Vec, // This should always be empty since we don't support `vmTrace` atm (this // would likely need changes in the FEVM) vm_trace: Option, @@ -3375,6 +3368,46 @@ fn get_output(msg: &Message, invoke_result: ApiInvocResult) -> Result } } +fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[EthUint64]) -> Vec { + let mut entries = Vec::new(); + + // Build entry for current trace + let gas = trace.sum_gas().total_gas; + let entry = TraceEntry { + action: Action { + call_type: "call".to_string(), // (e.g., "create" for contract creation) + from: trace.msg.from.clone(), + to: trace.msg.to.clone(), + gas: gas.into(), + input: trace.msg.params.clone(), + value: trace.msg.value.clone(), + }, + result: if trace.msg_rct.exit_code.is_success() { + let gas_used = trace.sum_gas().total_gas.into(); + Some(ResultData { + gas_used, + output: trace.msg_rct.r#return.clone().into(), + }) + } else { + // Revert case + None + }, + subtraces: EthUint64(trace.subcalls.len() as _), + trace_address: parent_trace_address.to_vec(), + type_: "call".to_string(), + }; + entries.push(entry); + + // Recursively build subcall traces + for (i, subcall) in trace.subcalls.iter().enumerate() { + let mut sub_trace_address = parent_trace_address.to_vec(); + sub_trace_address.push(EthUint64(i as _)); + entries.extend(get_entries(subcall, &sub_trace_address)); + } + + entries +} + pub enum EthTraceCall {} impl RpcMethod<3> for EthTraceCall { const NAME: &'static str = "Filecoin.EthTraceCall"; @@ -3408,11 +3441,17 @@ impl RpcMethod<3> for EthTraceCall { dbg!(&invoke_result); let mut trace_results: EthTraceResults = Default::default(); - let output = get_output(&msg, invoke_result)?; + let output = get_output(&msg, invoke_result.clone())?; // output is always present, should we remove option? trace_results.output = Some(output); if trace_types.contains(&EthTraceType::Trace) { // Built trace objects + let entries = if let Some(exec_trace) = invoke_result.execution_trace { + get_entries(&exec_trace, &[EthUint64(0)]) + } else { + Default::default() + }; + trace_results.trace = entries; } Ok(trace_results) diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 36cd381a1273..9518f49605e5 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -4,6 +4,8 @@ use crate::blocks::TipsetKey; use crate::lotus_json::{LotusJson, lotus_json_with_self}; use crate::message::Message as _; +use crate::rpc::eth::EthUint64; +use crate::rpc::eth::types::EthBytes; use crate::shim::executor::ApplyRet; use crate::shim::{ address::Address, @@ -237,6 +239,48 @@ impl PartialEq for GasTrace { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Action { + pub call_type: String, // E.g., "call", "delegatecall", "create" + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson
")] + pub from: Address, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson
")] + pub to: Address, + pub gas: EthUint64, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub input: RawBytes, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub value: TokenAmount, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ResultData { + pub gas_used: EthUint64, + pub output: EthBytes, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct TraceEntry { + /// Call parameters + pub action: Action, + /// Call result or `None` for reverts + pub result: Option, + /// Number of nested calls + pub subtraces: EthUint64, + /// Path in call hierarchy (e.g., [], [0], [0, 1]) + pub trace_address: Vec, + /// Call type, e.g., "call", "delegatecall", "create" + #[serde(rename = "type")] + pub type_: String, +} + #[derive(PartialEq, Serialize, Deserialize, Clone, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct InvocResult { From a672926065502b15574d29307554c0373b4f0c23 Mon Sep 17 00:00:00 2001 From: elmattic Date: Wed, 29 Oct 2025 14:41:14 +0100 Subject: [PATCH 08/15] Add script to generate test references --- .../tests/api_compare/gen_trace_call_refs.sh | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 scripts/tests/api_compare/gen_trace_call_refs.sh diff --git a/scripts/tests/api_compare/gen_trace_call_refs.sh b/scripts/tests/api_compare/gen_trace_call_refs.sh new file mode 100644 index 000000000000..067ac5c4621d --- /dev/null +++ b/scripts/tests/api_compare/gen_trace_call_refs.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +# Load .env +source .env || { echo "Failed to load .env"; exit 1; } + +# Validate script arguments +[[ -z "$ADDRESS" || -z "$TRACER" || -z "$SEPOLIA_RPC_URL" ]] && { + echo "ERROR: Set ADDRESS, TRACER, SEPOLIA_RPC_URL in .env" + exit 1 +} + +echo "Generating trace_call test suite..." +echo "Tracer: $TRACER" +echo "Caller: $ADDRESS" + +BALANCE=$(cast balance "$ADDRESS" --rpc-url "$SEPOLIA_RPC_URL") +echo "Caller balance: $BALANCE wei" +echo + +# The array of test cases +declare -a TESTS=( + # id:function_name:args:value_hex + "1:setX(uint256):999:" + "2:deposit():" + "3:transfer(address,uint256):0x1111111111111111111111111111111111111111 500:" + "4:callSelf(uint256):999:" + "5:delegateSelf(uint256):777:" + "6:staticRead():" + "7:createChild():" + "8:destroyAndSend():" + "9:keccakIt(bytes32):0x000000000000000000000000000000000000000000000000000000000000abcd:" + "10:doRevert():" +) + +# Generate each test reference +for TEST in "${TESTS[@]}"; do + IFS=':' read -r ID FUNC ARGS VALUE_HEX <<< "$TEST" + + echo "test$ID: $FUNC" + + # Encode calldata + if [[ -z "$ARGS" ]]; then + CALLDATA=$(cast calldata "$FUNC") + else + CALLDATA=$(cast calldata "$FUNC" $ARGS) + fi + + # Build payload + if [[ -n "$VALUE_HEX" ]]; then + PAYLOAD=$(jq -n \ + --arg from "$ADDRESS" \ + --arg to "$TRACER" \ + --arg data "$CALLDATA" \ + --arghex value "$VALUE_HEX" \ + '{ + jsonrpc: "2.0", + id: ($id | tonumber), + method: "trace_call", + params: [ + { from: $from, to: $to, data: $data, value: $value }, + ["trace"], + "latest" + ] + }' --arg id "$ID") + else + PAYLOAD=$(jq -n \ + --arg from "$ADDRESS" \ + --arg to "$TRACER" \ + --arg data "$CALLDATA" \ + '{ + jsonrpc: "2.0", + id: ($id | tonumber), + method: "trace_call", + params: [ + { from: $from, to: $to, data: $data }, + ["trace"], + "latest" + ] + }' --arg id "$ID") + fi + + # Send request + RESPONSE=$(curl -s -X POST \ + -H "Content-Type: application/json" \ + --data "$PAYLOAD" \ + "$SEPOLIA_RPC_URL") + + # Combine request + response + JSON_TEST=$(jq -n \ + --argjson request "$(echo "$PAYLOAD" | jq '.')" \ + --argjson response "$(echo "$RESPONSE" | jq '.')" \ + '{ request: $request, response: $response }') + + # Save reference file + FILENAME="./refs/test${ID}.json" + echo "$JSON_TEST" | jq . > "$FILENAME" + echo "Saved to $FILENAME" + + echo +done + +echo "All test references have been generated." From e8064efa3eb206f4eb7139a0315a2a19487a3c04 Mon Sep 17 00:00:00 2001 From: elmattic Date: Wed, 29 Oct 2025 15:01:23 +0100 Subject: [PATCH 09/15] Add contract --- .../api_cmd/contracts/tracer/Tracer.sol | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol diff --git a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol new file mode 100644 index 000000000000..2dc6b066532a --- /dev/null +++ b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +contract Tracer { + uint256 public x; + mapping(address => uint256) public balances; + event Transfer(address indexed from, address indexed to, uint256 value); + + constructor() payable { + x = 42; + } + + // 1. Simple storage write + function setX(uint256 _x) external { + x = _x; + } + + // 2. Balance update (SSTORE) + function deposit() external payable { + balances[msg.sender] = msg.value; + } + + // 3. Transfer between two accounts (SSTORE x2) + function transfer(address to, uint256 amount) external { + require(balances[msg.sender] >= amount, "insufficient balance"); + balances[msg.sender] -= amount; + balances[to] += amount; + emit Transfer(msg.sender, to, amount); + } + + // 4. CALL (external call to self – creates CALL opcode) + function callSelf(uint256 _x) external { + (bool ok, ) = address(this).call( + abi.encodeWithSelector(this.setX.selector, _x) + ); + require(ok, "call failed"); + } + + // 5. DELEGATECALL (to self – shows delegatecall trace) + function delegateSelf(uint256 _x) external { + (bool ok, ) = address(this).delegatecall( + abi.encodeWithSelector(this.setX.selector, _x) + ); + require(ok, "delegatecall failed"); + } + + // 6. STATICCALL (read-only) + function staticRead() external view returns (uint256) { + return x; + } + + // 7. CREATE (deploy a tiny contract) + function createChild() external returns (address child) { + bytes + memory code = hex"6080604052348015600f57600080fd5b5060019050601c806100226000396000f3fe6080604052"; + assembly { + child := create(0, add(code, 0x20), 0x1c) + } + } + + // 8. SELFDESTRUCT (send ETH to caller) + // Deprecated (EIP-6780): selfdestruct only sends ETH (code & storage stay) + function destroyAndSend() external { + selfdestruct(payable(msg.sender)); + } + + // 9. Precompile use – keccak256 + function keccakIt(bytes32 input) external pure returns (bytes32) { + return keccak256(abi.encodePacked(input)); + } + + // 10. Revert + function doRevert() external pure { + revert("from some fiasco"); + } +} From 30f1eab3d3b0ac58f9b5b667ff9933c647f6b200 Mon Sep 17 00:00:00 2001 From: elmattic Date: Wed, 29 Oct 2025 19:17:41 +0100 Subject: [PATCH 10/15] Add a first api-compare test --- .../subcommands/api_cmd/api_compare_tests.rs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index f280812e3736..8862184b4585 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -12,8 +12,8 @@ use crate::rpc::FilterList; use crate::rpc::auth::AuthNewParams; use crate::rpc::beacon::BeaconGetEntry; use crate::rpc::eth::{ - BlockNumberOrHash, EthInt64, ExtBlockNumberOrHash, ExtPredefined, Predefined, - new_eth_tx_from_signed_message, types::*, + BlockNumberOrHash, EthInt64, EthTraceType, EthUint64, ExtBlockNumberOrHash, ExtPredefined, + Predefined, new_eth_tx_from_signed_message, types::*, }; use crate::rpc::gas::GasEstimateGasLimit; use crate::rpc::miner::BlockTemplate; @@ -1412,6 +1412,28 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset let block_hash: EthHash = block_cid.into(); let mut tests = vec![ + RpcTest::identity( + EthTraceCall::request(( + EthCallMessage { + from: Some( + EthAddress::from_str("0x1111111111111111111111111111111111111111").unwrap(), + ), + to: Some( + EthAddress::from_str("0x4A38E58A3602D057c8aC2c4D76f0C45CFF3b5f56").unwrap(), + ), + data: Some( + EthBytes::from_str("0x4018d9aa00000000000000000000000000000000000000000000000000000000000003e7").unwrap() + ), + gas: Some( + EthUint64(0x13880) // 80,000 + ), + ..Default::default() + }, + nunny::vec![EthTraceType::Trace], + BlockNumberOrHash::PredefinedBlock(Predefined::Latest), + )) + .unwrap(), + ), RpcTest::identity( EthGetBalance::request(( EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa").unwrap(), From 44dade6de282b760d99bea231035c8580b6a2725 Mon Sep 17 00:00:00 2001 From: elmattic Date: Wed, 29 Oct 2025 19:23:10 +0100 Subject: [PATCH 11/15] Add some comments explaining gas differences --- .../tests/api_compare/gen_trace_call_refs.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/tests/api_compare/gen_trace_call_refs.sh b/scripts/tests/api_compare/gen_trace_call_refs.sh index 067ac5c4621d..8e41b2505731 100644 --- a/scripts/tests/api_compare/gen_trace_call_refs.sh +++ b/scripts/tests/api_compare/gen_trace_call_refs.sh @@ -32,6 +32,19 @@ declare -a TESTS=( "10:doRevert():" ) +# 0x13880 is 80,000 + +# Remember: trace_call is not a real transaction +# +# It’s a simulation! +# RPC nodes limit gas to prevent: +# - Infinite loops +# - DoS attacks +# - Memory exhaustion + +# We generated reference results using Alchemy provider, so you will likely see params.gas != action.gas +# in the first trace + # Generate each test reference for TEST in "${TESTS[@]}"; do IFS=':' read -r ID FUNC ARGS VALUE_HEX <<< "$TEST" @@ -57,7 +70,7 @@ for TEST in "${TESTS[@]}"; do id: ($id | tonumber), method: "trace_call", params: [ - { from: $from, to: $to, data: $data, value: $value }, + { from: $from, to: $to, data: $data, value: $value, gas: "0x13880" }, ["trace"], "latest" ] @@ -72,7 +85,7 @@ for TEST in "${TESTS[@]}"; do id: ($id | tonumber), method: "trace_call", params: [ - { from: $from, to: $to, data: $data }, + { from: $from, to: $to, data: $data, gas: "0x13880" }, ["trace"], "latest" ] From e0b23f3018dff3e2bd2a4d344a9445ecf9d5453c Mon Sep 17 00:00:00 2001 From: elmattic Date: Wed, 29 Oct 2025 23:03:07 +0100 Subject: [PATCH 12/15] Fix some trace discrepancies --- src/rpc/methods/eth.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 4151353e9498..2e90e04ce79b 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3368,17 +3368,16 @@ fn get_output(msg: &Message, invoke_result: ApiInvocResult) -> Result } } -fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[EthUint64]) -> Vec { +fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[usize]) -> Vec { let mut entries = Vec::new(); // Build entry for current trace - let gas = trace.sum_gas().total_gas; let entry = TraceEntry { action: Action { call_type: "call".to_string(), // (e.g., "create" for contract creation) from: trace.msg.from.clone(), to: trace.msg.to.clone(), - gas: gas.into(), + gas: trace.msg.gas_limit.unwrap_or_default().into(), input: trace.msg.params.clone(), value: trace.msg.value.clone(), }, @@ -3392,7 +3391,7 @@ fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[EthUint64]) -> Ve // Revert case None }, - subtraces: EthUint64(trace.subcalls.len() as _), + subtraces: trace.subcalls.len(), trace_address: parent_trace_address.to_vec(), type_: "call".to_string(), }; @@ -3401,7 +3400,7 @@ fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[EthUint64]) -> Ve // Recursively build subcall traces for (i, subcall) in trace.subcalls.iter().enumerate() { let mut sub_trace_address = parent_trace_address.to_vec(); - sub_trace_address.push(EthUint64(i as _)); + sub_trace_address.push(i); entries.extend(get_entries(subcall, &sub_trace_address)); } @@ -3447,7 +3446,7 @@ impl RpcMethod<3> for EthTraceCall { if trace_types.contains(&EthTraceType::Trace) { // Built trace objects let entries = if let Some(exec_trace) = invoke_result.execution_trace { - get_entries(&exec_trace, &[EthUint64(0)]) + get_entries(&exec_trace, &[]) } else { Default::default() }; From f5d9262fa6e5788323ab2a1a100c61747ef409b0 Mon Sep 17 00:00:00 2001 From: elmattic Date: Wed, 29 Oct 2025 23:20:05 +0100 Subject: [PATCH 13/15] Fix build --- src/rpc/methods/state/types.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 9518f49605e5..57cb2ba01ab6 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -272,10 +272,12 @@ pub struct TraceEntry { pub action: Action, /// Call result or `None` for reverts pub result: Option, - /// Number of nested calls - pub subtraces: EthUint64, - /// Path in call hierarchy (e.g., [], [0], [0, 1]) - pub trace_address: Vec, + /// How many subtraces this trace has. + pub subtraces: usize, + /// The identifier of this transaction trace in the set. + /// + /// This gives the exact location in the call trace. + pub trace_address: Vec, /// Call type, e.g., "call", "delegatecall", "create" #[serde(rename = "type")] pub type_: String, From db158d639bc5e20177f0f46b98409440d48af46c Mon Sep 17 00:00:00 2001 From: elmattic Date: Thu, 30 Oct 2025 10:12:00 +0100 Subject: [PATCH 14/15] Use eth types --- src/rpc/methods/eth.rs | 17 +++++++++-------- src/rpc/methods/state/types.rs | 20 ++++++-------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 2e90e04ce79b..ea8f564923fd 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3368,18 +3368,19 @@ fn get_output(msg: &Message, invoke_result: ApiInvocResult) -> Result } } -fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[usize]) -> Vec { +fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[usize]) -> Result> { let mut entries = Vec::new(); // Build entry for current trace let entry = TraceEntry { action: Action { call_type: "call".to_string(), // (e.g., "create" for contract creation) - from: trace.msg.from.clone(), - to: trace.msg.to.clone(), + from: EthAddress::from_filecoin_address(&trace.msg.from)?, + to: EthAddress::from_filecoin_address(&trace.msg.to)?, gas: trace.msg.gas_limit.unwrap_or_default().into(), - input: trace.msg.params.clone(), - value: trace.msg.value.clone(), + // input needs proper decoding + input: trace.msg.params.clone().into(), + value: trace.msg.value.clone().into(), }, result: if trace.msg_rct.exit_code.is_success() { let gas_used = trace.sum_gas().total_gas.into(); @@ -3401,10 +3402,10 @@ fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[usize]) -> Vec for EthTraceCall { if trace_types.contains(&EthTraceType::Trace) { // Built trace objects let entries = if let Some(exec_trace) = invoke_result.execution_trace { - get_entries(&exec_trace, &[]) + get_entries(&exec_trace, &[])? } else { Default::default() }; diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 57cb2ba01ab6..193427cf17b4 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -4,8 +4,8 @@ use crate::blocks::TipsetKey; use crate::lotus_json::{LotusJson, lotus_json_with_self}; use crate::message::Message as _; -use crate::rpc::eth::EthUint64; -use crate::rpc::eth::types::EthBytes; +use crate::rpc::eth::types::{EthAddress, EthBytes}; +use crate::rpc::eth::{EthBigInt, EthUint64}; use crate::shim::executor::ApplyRet; use crate::shim::{ address::Address, @@ -243,19 +243,11 @@ impl PartialEq for GasTrace { #[serde(rename_all = "camelCase")] pub struct Action { pub call_type: String, // E.g., "call", "delegatecall", "create" - #[serde(with = "crate::lotus_json")] - #[schemars(with = "LotusJson
")] - pub from: Address, - #[serde(with = "crate::lotus_json")] - #[schemars(with = "LotusJson
")] - pub to: Address, + pub from: EthAddress, + pub to: EthAddress, pub gas: EthUint64, - #[serde(with = "crate::lotus_json")] - #[schemars(with = "LotusJson")] - pub input: RawBytes, - #[serde(with = "crate::lotus_json")] - #[schemars(with = "LotusJson")] - pub value: TokenAmount, + pub input: EthBytes, + pub value: EthBigInt, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] From 001ae4bf91f450f1326d7677928453f34c70af6a Mon Sep 17 00:00:00 2001 From: elmattic Date: Thu, 30 Oct 2025 11:24:12 +0100 Subject: [PATCH 15/15] Refactor --- src/rpc/methods/eth.rs | 2 +- .../subcommands/api_cmd/api_compare_tests.rs | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index ea8f564923fd..9b07057708af 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3410,7 +3410,7 @@ fn get_entries(trace: &ExecutionTrace, parent_trace_address: &[usize]) -> Result pub enum EthTraceCall {} impl RpcMethod<3> for EthTraceCall { - const NAME: &'static str = "Filecoin.EthTraceCall"; + const NAME: &'static str = "Forest.EthTraceCall"; const NAME_ALIAS: Option<&'static str> = Some("trace_call"); const N_REQUIRED_PARAMS: usize = 1; const PARAM_NAMES: [&'static str; 3] = ["tx", "traceTypes", "blockParam"]; diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 8862184b4585..4c31d4db93a4 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -1366,6 +1366,42 @@ fn eth_tests() -> Vec { .unwrap(), )); } + + let cases = [( + EthBytes::from_str( + "0x4018d9aa00000000000000000000000000000000000000000000000000000000000003e7", + ) + .unwrap(), + false, + )]; + + for (input, state_diff) in cases { + tests.push(RpcTest::identity( + EthTraceCall::request(( + EthCallMessage { + from: Some( + EthAddress::from_str("0x1111111111111111111111111111111111111111").unwrap(), + ), + to: Some( + EthAddress::from_str("0x4A38E58A3602D057c8aC2c4D76f0C45CFF3b5f56").unwrap(), + ), + data: Some(input), + gas: Some( + EthUint64(0x13880), // 80,000 + ), + ..Default::default() + }, + if state_diff { + nunny::vec![EthTraceType::Trace, EthTraceType::StateDiff] + } else { + nunny::vec![EthTraceType::Trace] + }, + BlockNumberOrHash::PredefinedBlock(Predefined::Latest), + )) + .unwrap(), + )); + } + tests }