From 4a99dc2a2b111c7a49b9db26c6a22e02587574f9 Mon Sep 17 00:00:00 2001 From: elmattic Date: Fri, 24 Oct 2025 15:15:50 +0200 Subject: [PATCH 01/19] Start impl of EthTrace call API (wip) --- .../tests/api_compare/gen_trace_call_refs.sh | 115 ++++++++++++++ src/rpc/methods/eth.rs | 146 +++++++++++++++++- src/rpc/methods/state/types.rs | 38 +++++ src/rpc/mod.rs | 1 + .../subcommands/api_cmd/api_compare_tests.rs | 62 +++++++- .../api_cmd/contracts/tracer/Tracer.sol | 76 +++++++++ 6 files changed, 435 insertions(+), 3 deletions(-) create mode 100644 scripts/tests/api_compare/gen_trace_call_refs.sh create mode 100644 src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol 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..8e41b2505731 --- /dev/null +++ b/scripts/tests/api_compare/gen_trace_call_refs.sh @@ -0,0 +1,115 @@ +#!/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():" +) + +# 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" + + 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, gas: "0x13880" }, + ["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, gas: "0x13880" }, + ["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." diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 8b219bf132cb..8fad587bab1d 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -34,7 +34,7 @@ use crate::rpc::eth::filter::{ use crate::rpc::eth::types::{EthBlockTrace, EthTrace}; use crate::rpc::eth::utils::decode_revert_reason; use crate::rpc::methods::chain::ChainGetTipSetV2; -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}; @@ -82,6 +82,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)); @@ -469,6 +471,43 @@ impl ExtBlockNumberOrHash { } } +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[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, +} + +lotus_json_with_self!(EthTraceType); + +#[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, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +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, +} + +lotus_json_with_self!(EthTraceResults); + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, GetSize)] #[serde(untagged)] // try a Vec, then a Vec pub enum Transactions { @@ -3911,6 +3950,111 @@ where 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) + } + } +} + +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: 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 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(); + Some(ResultData { + gas_used, + output: trace.msg_rct.r#return.clone().into(), + }) + } else { + // Revert case + None + }, + subtraces: trace.subcalls.len(), + 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(i); + entries.extend(get_entries(subcall, &sub_trace_address)?); + } + + Ok(entries) +} + +pub enum EthTraceCall {} +impl RpcMethod<3> for 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"]; + const API_PATHS: BitFlags = ApiPaths::all(); + const PERMISSION: Permission = Permission::Read; + type Params = (EthCallMessage, NonEmpty, BlockNumberOrHash); + type Ok = EthTraceResults; + async fn handle( + 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); + + 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); + + let mut trace_results: EthTraceResults = Default::default(); + 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, &[])? + } else { + Default::default() + }; + trace_results.trace = entries; + } + + Ok(trace_results) + } +} + pub enum EthTraceTransaction {} impl RpcMethod<1> for EthTraceTransaction { const NAME: &'static str = "Filecoin.EthTraceTransaction"; diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 65a9f20efb60..6c682d75d221 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::types::{EthAddress, EthBytes}; +use crate::rpc::eth::{EthBigInt, EthUint64}; use crate::shim::executor::ApplyRet; use crate::shim::{ address::Address, @@ -237,6 +239,42 @@ 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" + pub from: EthAddress, + pub to: EthAddress, + pub gas: EthUint64, + pub input: EthBytes, + pub value: EthBigInt, +} + +#[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, + /// 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, +} + #[derive(PartialEq, Serialize, Deserialize, Clone, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct InvocResult { diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 6986ae7e05aa..9307c737c825 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -153,6 +153,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::eth::EthSyncing); $callback!($crate::rpc::eth::EthTraceBlock); $callback!($crate::rpc::eth::EthTraceBlockV2); + $callback!($crate::rpc::eth::EthTraceCall); $callback!($crate::rpc::eth::EthTraceFilter); $callback!($crate::rpc::eth::EthTraceTransaction); $callback!($crate::rpc::eth::EthTraceReplayBlockTransactions); diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 65ad68986ba9..4741b144ac1c 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, GasEstimateMessageGas}; use crate::rpc::miner::BlockTemplate; @@ -1529,6 +1529,42 @@ fn eth_tests() -> Vec { FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F4_ADDRESS, None)).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 } @@ -1585,6 +1621,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(), 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 2425477c1a0df62eac392c8919195771f7d59a3b Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Tue, 27 Jan 2026 18:49:23 +0530 Subject: [PATCH 02/19] refactor the eth trace call impl [skip ci] --- src/rpc/methods/eth.rs | 134 +++++++++++---------------------- src/rpc/methods/state/types.rs | 38 ---------- 2 files changed, 46 insertions(+), 126 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 8fad587bab1d..32ff1ca8a8b7 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -34,7 +34,7 @@ use crate::rpc::eth::filter::{ use crate::rpc::eth::types::{EthBlockTrace, EthTrace}; use crate::rpc::eth::utils::decode_revert_reason; use crate::rpc::methods::chain::ChainGetTipSetV2; -use crate::rpc::state::{Action, ApiInvocResult, ExecutionTrace, ResultData, TraceEntry}; +use crate::rpc::state::ApiInvocResult; use crate::rpc::types::{ApiTipsetKey, EventEntry, MessageLookup}; use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod}; use crate::rpc::{EthEventHandler, LOOKBACK_NO_LIMIT}; @@ -486,24 +486,12 @@ pub enum EthTraceType { lotus_json_with_self!(EthTraceType); -#[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, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] 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, + trace: Vec, } lotus_json_with_self!(EthTraceResults); @@ -3950,61 +3938,6 @@ where 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) - } - } -} - -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: 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 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(); - Some(ResultData { - gas_used, - output: trace.msg_rct.r#return.clone().into(), - }) - } else { - // Revert case - None - }, - subtraces: trace.subcalls.len(), - 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(i); - entries.extend(get_entries(subcall, &sub_trace_address)?); - } - - Ok(entries) -} - pub enum EthTraceCall {} impl RpcMethod<3> for EthTraceCall { const NAME: &'static str = "Forest.EthTraceCall"; @@ -4013,6 +3946,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, NonEmpty, BlockNumberOrHash); type Ok = EthTraceResults; async fn handle( @@ -4021,12 +3955,7 @@ impl RpcMethod<3> for EthTraceCall { ) -> 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); + // trace_call has to be rate limited as it is a debug method or it could be permissioned, not sure right now. let msg = Message::try_from(tx)?; let ts = tipset_by_block_number_or_hash( @@ -4034,27 +3963,56 @@ impl RpcMethod<3> for EthTraceCall { block_param, ResolveNullTipset::TakeOlder, )?; - let invoke_result = apply_message(&ctx, Some(ts), msg.clone()).await?; - dbg!(&invoke_result); - let mut trace_results: EthTraceResults = Default::default(); - let output = get_output(&msg, invoke_result.clone())?; - // output is always present, should we remove option? - trace_results.output = Some(output); + let invoke_result = ctx + .state_manager + .apply_on_state_with_gas(Some(ts.clone()), msg.clone()) + .await + .map_err(|e| anyhow::anyhow!("failed to apply message: {e}"))?; + + // Get state tree for building proper traces + let (state_root, _) = ctx + .state_manager + .tipset_state(&ts) + .await + .map_err(|e| anyhow::anyhow!("failed to get tipset state: {e}"))?; + let state = StateTree::new_from_root(ctx.store_owned(), &state_root)?; + + let mut trace_results = EthTraceResults::default(); + + // Get output from the execution result + trace_results.output = get_trace_output(&msg, &invoke_result); + + // Build traces if requested if trace_types.contains(&EthTraceType::Trace) { - // Built trace objects - let entries = if let Some(exec_trace) = invoke_result.execution_trace { - get_entries(&exec_trace, &[])? - } else { - Default::default() - }; - trace_results.trace = entries; + if let Some(exec_trace) = invoke_result.execution_trace { + let mut env = trace::base_environment(&state, &msg.from()) + .map_err(|e| anyhow::anyhow!("failed to create trace environment: {e}"))?; + trace::build_traces(&mut env, &[], exec_trace)?; + trace_results.trace = env.traces; + } } Ok(trace_results) } } +/// Get output bytes from trace execution result. +fn get_trace_output(msg: &Message, invoke_result: &ApiInvocResult) -> Option { + if msg.to() == FilecoinAddress::ETHEREUM_ACCOUNT_MANAGER_ACTOR { + return Some(EthBytes::default()); + } + + let msg_rct = invoke_result.msg_rct.as_ref()?; + let return_data = msg_rct.return_data(); + + if return_data.is_empty() { + return Some(EthBytes::default()); + } + + decode_payload(&return_data, CBOR).ok() +} + pub enum EthTraceTransaction {} impl RpcMethod<1> for EthTraceTransaction { const NAME: &'static str = "Filecoin.EthTraceTransaction"; diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index 6c682d75d221..65a9f20efb60 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -4,8 +4,6 @@ use crate::blocks::TipsetKey; use crate::lotus_json::{LotusJson, lotus_json_with_self}; use crate::message::Message as _; -use crate::rpc::eth::types::{EthAddress, EthBytes}; -use crate::rpc::eth::{EthBigInt, EthUint64}; use crate::shim::executor::ApplyRet; use crate::shim::{ address::Address, @@ -239,42 +237,6 @@ 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" - pub from: EthAddress, - pub to: EthAddress, - pub gas: EthUint64, - pub input: EthBytes, - pub value: EthBigInt, -} - -#[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, - /// 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, -} - #[derive(PartialEq, Serialize, Deserialize, Clone, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct InvocResult { From 442052f079813bab29e94b533e763fc114f84110 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Tue, 27 Jan 2026 23:12:26 +0530 Subject: [PATCH 03/19] add deep trace into the tracer contract --- src/rpc/methods/eth.rs | 3 - .../api_cmd/contracts/tracer/Tracer.sol | 126 ++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 32ff1ca8a8b7..1113c30a8cf3 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -79,7 +79,6 @@ use std::num::NonZeroUsize; use std::ops::RangeInclusive; use std::str::FromStr; use std::sync::{Arc, LazyLock}; -use tracing::log; use utils::{decode_payload, lookup_eth_address}; use nunny::Vec as NonEmpty; @@ -2132,9 +2131,7 @@ where Err(anyhow::anyhow!("failed to estimate gas: {err}").into()) } Ok(gassed_msg) => { - log::info!("correct gassed_msg: do eth_gas_search {gassed_msg:?}"); let expected_gas = eth_gas_search(ctx, gassed_msg, &tipset.key().into()).await?; - log::info!("trying eth_gas search: {expected_gas}"); Ok(expected_gas.into()) } } diff --git a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol index 2dc6b066532a..5826998f6945 100644 --- a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol +++ b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol @@ -73,4 +73,130 @@ contract Tracer { function doRevert() external pure { revert("from some fiasco"); } + + // ========== DEEP TRACE FUNCTIONS ========== + + // 11. Deep recursive CALL trace + // Creates trace depth of `depth` levels + function deepTrace(uint256 depth) external returns (uint256) { + if (depth == 0) { + x = x + 1; // Storage write at deepest level + return x; + } + (bool ok, bytes memory result) = address(this).call( + abi.encodeWithSelector(this.deepTrace.selector, depth - 1) + ); + require(ok, "deep call failed"); + return abi.decode(result, (uint256)); + } + + // 12. Mixed call types trace + // Alternates between CALL, DELEGATECALL, and STATICCALL + function mixedTrace(uint256 depth) external returns (uint256) { + if (depth == 0) { + return x; + } + + uint256 callType = depth % 3; + + if (callType == 0) { + // Regular CALL + (bool ok, bytes memory result) = address(this).call( + abi.encodeWithSelector(this.mixedTrace.selector, depth - 1) + ); + require(ok, "call failed"); + return abi.decode(result, (uint256)); + } else if (callType == 1) { + // DELEGATECALL + (bool ok, bytes memory result) = address(this).delegatecall( + abi.encodeWithSelector(this.mixedTrace.selector, depth - 1) + ); + require(ok, "delegatecall failed"); + return abi.decode(result, (uint256)); + } else { + // STATICCALL (read-only) + (bool ok, bytes memory result) = address(this).staticcall( + abi.encodeWithSelector(this.mixedTrace.selector, depth - 1) + ); + require(ok, "staticcall failed"); + return abi.decode(result, (uint256)); + } + } + + // 13. Wide trace - multiple sibling calls at same level + // Creates `width` parallel calls, each going `depth` levels deep + // Example: wideTrace(3, 2) creates 3 siblings, each 2 levels deep + function wideTrace(uint256 width, uint256 depth) external returns (uint256 sum) { + if (depth == 0) { + return 1; + } + + for (uint256 i = 0; i < width; i++) { + (bool ok, bytes memory result) = address(this).call( + abi.encodeWithSelector(this.wideTrace.selector, width, depth - 1) + ); + require(ok, "wide call failed"); + sum += abi.decode(result, (uint256)); + } + return sum; + } + + // 14. Complex trace - combines everything + // Level 0: CALL to setX + // Level 1: DELEGATECALL to inner + // Level 2: Multiple CALLs + // Level 3: STATICCALL + function complexTrace() external returns (uint256) { + // First: regular call to setX + (bool ok1, ) = address(this).call( + abi.encodeWithSelector(this.setX.selector, 100) + ); + require(ok1, "setX failed"); + + // Second: delegatecall that does more calls + (bool ok2, bytes memory result) = address(this).delegatecall( + abi.encodeWithSelector(this.innerComplex.selector) + ); + require(ok2, "innerComplex failed"); + + return abi.decode(result, (uint256)); + } + + // Helper for complexTrace + function innerComplex() external returns (uint256) { + // Multiple sibling calls + (bool ok1, ) = address(this).call( + abi.encodeWithSelector(this.setX.selector, 200) + ); + require(ok1, "inner call 1 failed"); + + (bool ok2, ) = address(this).call( + abi.encodeWithSelector(this.setX.selector, 300) + ); + require(ok2, "inner call 2 failed"); + + // Staticcall to read + (bool ok3, bytes memory result) = address(this).staticcall( + abi.encodeWithSelector(this.staticRead.selector) + ); + require(ok3, "staticcall failed"); + + return abi.decode(result, (uint256)); + } + + // 15. Failing nested trace - revert at depth + // Useful for testing partial trace on failure + function failAtDepth(uint256 depth, uint256 failAt) external returns (uint256) { + if (depth == failAt) { + revert("intentional failure at depth"); + } + if (depth == 0) { + return x; + } + (bool ok, bytes memory result) = address(this).call( + abi.encodeWithSelector(this.failAtDepth.selector, depth - 1, failAt) + ); + require(ok, "nested call failed"); + return abi.decode(result, (uint256)); + } } From fca553630c6ae0646394cd1f089aa34c22f17a45 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 29 Jan 2026 17:34:09 +0530 Subject: [PATCH 04/19] add stateDiff support in trace call API --- src/rpc/methods/eth.rs | 90 ++- src/rpc/methods/eth/trace.rs | 681 ++++++++++++++++++ src/rpc/methods/eth/types.rs | 108 +++ src/rpc/methods/gas.rs | 2 +- src/shim/actors/builtin/evm/mod.rs | 8 + src/state_manager/mod.rs | 40 +- .../api_cmd/contracts/tracer/Tracer.sol | 51 +- 7 files changed, 931 insertions(+), 49 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 1113c30a8cf3..20eacc96b284 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -488,9 +488,13 @@ lotus_json_with_self!(EthTraceType); #[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct EthTraceResults { - output: Option, - state_diff: Option, - trace: Vec, + /// Output bytes from the transaction execution + pub output: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// State diff showing all account changes (only when StateDiff trace type requested) + pub state_diff: Option, + /// Call trace hierarchy (only when Trace trace type requested) + pub trace: Vec, } lotus_json_with_self!(EthTraceResults); @@ -2145,7 +2149,7 @@ async fn apply_message( where DB: Blockstore + Send + Sync + 'static, { - let invoc_res = ctx + let (invoc_res, _) = ctx .state_manager .apply_on_state_with_gas(tipset, msg, StateLookupPolicy::Enabled) .await @@ -2236,7 +2240,7 @@ where DB: Blockstore + Send + Sync + 'static, { msg.gas_limit = limit; - let (_invoc_res, apply_ret, _) = data + let (_invoc_res, apply_ret, _, _) = data .state_manager .call_with_gas( &mut msg.into(), @@ -3943,6 +3947,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; + const DESCRIPTION: Option<&'static str> = Some("Returns traces created by the transaction."); type Params = (EthCallMessage, NonEmpty, BlockNumberOrHash); type Ok = EthTraceResults; @@ -3950,10 +3955,6 @@ 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) - // trace_call has to be rate limited as it is a debug method or it could be permissioned, not sure right now. - let msg = Message::try_from(tx)?; let ts = tipset_by_block_number_or_hash( ctx.chain_store(), @@ -3961,35 +3962,57 @@ impl RpcMethod<3> for EthTraceCall { ResolveNullTipset::TakeOlder, )?; - let invoke_result = ctx + let (pre_state_root, _) = ctx .state_manager - .apply_on_state_with_gas(Some(ts.clone()), msg.clone()) + .tipset_state(&ts, StateLookupPolicy::Enabled) .await - .map_err(|e| anyhow::anyhow!("failed to apply message: {e}"))?; + .map_err(|e| anyhow::anyhow!("failed to get tipset state: {e}"))?; + let pre_state = StateTree::new_from_root(ctx.store_owned(), &pre_state_root)?; - // Get state tree for building proper traces - let (state_root, _) = ctx + let (invoke_result, post_state_root) = ctx .state_manager - .tipset_state(&ts) + .apply_on_state_with_gas(Some(ts.clone()), msg.clone(), StateLookupPolicy::Enabled) .await - .map_err(|e| anyhow::anyhow!("failed to get tipset state: {e}"))?; - let state = StateTree::new_from_root(ctx.store_owned(), &state_root)?; + .map_err(|e| anyhow::anyhow!("failed to apply message: {e}"))?; + let post_state = StateTree::new_from_root(ctx.store_owned(), &post_state_root)?; let mut trace_results = EthTraceResults::default(); - // Get output from the execution result trace_results.output = get_trace_output(&msg, &invoke_result); - // Build traces if requested + // Extract touched addresses for state diff (do this before consuming exec_trace) + let touched_addresses = invoke_result + .execution_trace + .as_ref() + .map(extract_touched_eth_addresses) + .unwrap_or_default(); + + // Build call traces if requested if trace_types.contains(&EthTraceType::Trace) { if let Some(exec_trace) = invoke_result.execution_trace { - let mut env = trace::base_environment(&state, &msg.from()) + let mut env = trace::base_environment(&post_state, &msg.from()) .map_err(|e| anyhow::anyhow!("failed to create trace environment: {e}"))?; trace::build_traces(&mut env, &[], exec_trace)?; trace_results.trace = env.traces; } } + // Build state diff if requested + if trace_types.contains(&EthTraceType::StateDiff) { + // Add the caller address to touched addresses + let mut all_touched = touched_addresses; + if let Ok(caller_eth) = EthAddress::from_filecoin_address(&msg.from()) { + all_touched.insert(caller_eth); + } + if let Ok(to_eth) = EthAddress::from_filecoin_address(&msg.to()) { + all_touched.insert(to_eth); + } + + let state_diff = + trace::build_state_diff(ctx.store(), &pre_state, &post_state, &all_touched)?; + trace_results.state_diff = Some(state_diff); + } + Ok(trace_results) } } @@ -4010,6 +4033,33 @@ fn get_trace_output(msg: &Message, invoke_result: &ApiInvocResult) -> Option = + LazyLock::new(|| env_or_default("FOREST_TRACE_STATE_DIFF_MAX_ADDRESSES", 1000)); + +/// Extract all unique Ethereum addresses touched during execution from the trace. +fn extract_touched_eth_addresses(trace: &crate::rpc::state::ExecutionTrace) -> HashSet { + let mut addresses = HashSet::default(); + extract_addresses_recursive(trace, &mut addresses); + addresses +} + +fn extract_addresses_recursive( + trace: &crate::rpc::state::ExecutionTrace, + addresses: &mut HashSet, +) { + if let Ok(eth_addr) = EthAddress::from_filecoin_address(&trace.msg.from) { + addresses.insert(eth_addr); + } + if let Ok(eth_addr) = EthAddress::from_filecoin_address(&trace.msg.to) { + addresses.insert(eth_addr); + } + + for subcall in &trace.subcalls { + extract_addresses_recursive(subcall, addresses); + } +} + pub enum EthTraceTransaction {} impl RpcMethod<1> for EthTraceTransaction { const NAME: &'static str = "Filecoin.EthTraceTransaction"; diff --git a/src/rpc/methods/eth/trace.rs b/src/rpc/methods/eth/trace.rs index 032f6c09cf54..dd563ee2c587 100644 --- a/src/rpc/methods/eth/trace.rs +++ b/src/rpc/methods/eth/trace.rs @@ -13,12 +13,17 @@ use crate::rpc::methods::state::ExecutionTrace; use crate::rpc::state::ActorTrace; use crate::shim::fvm_shared_latest::METHOD_CONSTRUCTOR; use crate::shim::{actors::is_evm_actor, address::Address, error::ExitCode, state_tree::StateTree}; +use ahash::HashSet; use fil_actor_eam_state::v12 as eam12; use fil_actor_evm_state::v15 as evm12; use fil_actor_init_state::v12::ExecReturn; use fil_actor_init_state::v15::Method as InitMethod; use fvm_ipld_blockstore::Blockstore; +use crate::rpc::eth::types::{AccountDiff, Delta, StateDiff}; +use crate::rpc::eth::{EthBigInt, EthUint64, MAX_STATE_DIFF_ADDRESSES}; +use crate::shim::actors::{EVMActorStateLoad, evm}; +use crate::shim::state_tree::ActorState; use anyhow::{Context, bail}; use num::FromPrimitive; use tracing::debug; @@ -619,3 +624,679 @@ fn trace_evm_private( } } } + +/// Build state diff by comparing pre and post-execution states for touched addresses. +pub(crate) fn build_state_diff( + store: &S, + pre_state: &StateTree, + post_state: &StateTree, + touched_addresses: &HashSet, +) -> anyhow::Result { + let mut state_diff = StateDiff::new(); + + // Limit the number of addresses for safety + let addresses: Vec<_> = touched_addresses + .iter() + .take(*MAX_STATE_DIFF_ADDRESSES) + .collect(); + + for eth_addr in addresses { + let fil_addr = eth_addr.to_filecoin_address()?; + + // Get actor state before and after + let pre_actor = pre_state + .get_actor(&fil_addr) + .map_err(|e| anyhow::anyhow!("failed to get actor state: {e}"))?; + + let post_actor = post_state + .get_actor(&fil_addr) + .map_err(|e| anyhow::anyhow!("failed to get actor state: {e}"))?; + + let account_diff = build_account_diff(store, pre_actor.as_ref(), post_actor.as_ref())?; + + // Only include it if there were actual changes + state_diff.insert_if_changed(*eth_addr, account_diff); + } + + Ok(state_diff) +} + +/// Build account diff by comparing pre and post actor states. +fn build_account_diff( + store: &DB, + pre_actor: Option<&ActorState>, + post_actor: Option<&ActorState>, +) -> anyhow::Result { + let mut diff = AccountDiff::default(); + + // Compare balance + let pre_balance = pre_actor.map(|a| EthBigInt(a.balance.atto().clone())); + let post_balance = post_actor.map(|a| EthBigInt(a.balance.atto().clone())); + diff.balance = Delta::from_comparison(pre_balance, post_balance); + + // Helper to get nonce from actor (uses EVM nonce for EVM actors) + let get_nonce = |actor: &ActorState| -> EthUint64 { + if is_evm_actor(&actor.code) { + EthUint64::from( + evm::State::load(store, actor.code, actor.state) + .map(|s| s.nonce()) + .unwrap_or(actor.sequence), + ) + } else { + EthUint64::from(actor.sequence) + } + }; + + // Helper to get bytecode from EVM actor + let get_bytecode = |actor: &ActorState| -> Option { + if !is_evm_actor(&actor.code) { + return None; + } + + // Load EVM state and get bytecode CID + let evm_state = evm::State::load(store, actor.code, actor.state).ok()?; + // Load actual bytecode from blockstore + store + .get(&evm_state.bytecode()) + .ok() + .flatten() + .map(EthBytes) + }; + + // Compare nonce + let pre_nonce = pre_actor.map(get_nonce); + let post_nonce = post_actor.map(get_nonce); + diff.nonce = Delta::from_comparison(pre_nonce, post_nonce); + + // Compare code (bytecode for EVM actors) + let pre_code = pre_actor.and_then(get_bytecode); + let post_code = post_actor.and_then(get_bytecode); + diff.code = Delta::from_comparison(pre_code, post_code); + + // TODO: implement EVM storage slot comparison + + Ok(diff) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::MemoryDB; + use crate::networks::ACTOR_BUNDLES_METADATA; + use crate::rpc::eth::types::ChangedType; + use crate::shim::address::Address as FilecoinAddress; + use crate::shim::econ::TokenAmount; + use crate::shim::machine::BuiltinActor; + use crate::shim::state_tree::{StateTree, StateTreeVersion}; + use crate::utils::db::CborStoreExt as _; + use ahash::HashSetExt as _; + use cid::Cid; + use num::BigInt; + use std::sync::Arc; + + fn create_test_actor(balance_atto: u64, sequence: u64) -> ActorState { + ActorState::new( + Cid::default(), // Non-EVM actor code CID + Cid::default(), // State CID (not used for non-EVM) + TokenAmount::from_atto(balance_atto), + sequence, + None, // No delegated address + ) + } + + fn get_evm_actor_code_cid() -> Option { + for bundle in ACTOR_BUNDLES_METADATA.values() { + if bundle.actor_major_version().ok() == Some(17) { + if let Ok(cid) = bundle.manifest.get(BuiltinActor::EVM) { + return Some(cid); + } + } + } + None + } + + fn create_evm_actor_with_bytecode( + store: &MemoryDB, + balance_atto: u64, + actor_sequence: u64, + evm_nonce: u64, + bytecode: Option<&[u8]>, + ) -> Option { + use fvm_ipld_blockstore::Blockstore as _; + + let evm_code_cid = get_evm_actor_code_cid()?; + + // Store bytecode as raw bytes (not CBOR-encoded) + let bytecode_cid = if let Some(code) = bytecode { + use multihash_codetable::MultihashDigest; + let mh = multihash_codetable::Code::Blake2b256.digest(code); + let cid = Cid::new_v1(fvm_ipld_encoding::IPLD_RAW, mh); + store.put_keyed(&cid, code).ok()?; + cid + } else { + Cid::default() + }; + + let bytecode_hash = if let Some(code) = bytecode { + use keccak_hash::keccak; + let hash = keccak(code); + fil_actor_evm_state::v17::BytecodeHash::from(hash.0) + } else { + fil_actor_evm_state::v17::BytecodeHash::EMPTY + }; + + let evm_state = fil_actor_evm_state::v17::State { + bytecode: bytecode_cid, + bytecode_hash, + contract_state: Cid::default(), + transient_data: None, + nonce: evm_nonce, + tombstone: None, + }; + + let state_cid = store.put_cbor_default(&evm_state).ok()?; + + Some(ActorState::new( + evm_code_cid, + state_cid, + TokenAmount::from_atto(balance_atto), + actor_sequence, + None, + )) + } + + fn create_masked_id_eth_address(actor_id: u64) -> EthAddress { + EthAddress::from_actor_id(actor_id) + } + + struct TestStateTrees { + store: Arc, + pre_state: StateTree, + post_state: StateTree, + } + + impl TestStateTrees { + fn new() -> anyhow::Result { + let store = Arc::new(MemoryDB::default()); + // Use V4 which creates FvmV2 state trees that allow direct set_actor + let pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + Ok(Self { + store, + pre_state, + post_state, + }) + } + + /// Create state trees with different actors in pre and post. + fn with_changed_actor( + actor_id: u64, + pre_actor: ActorState, + post_actor: ActorState, + ) -> anyhow::Result { + let store = Arc::new(MemoryDB::default()); + let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let addr = FilecoinAddress::new_id(actor_id); + pre_state.set_actor(&addr, pre_actor)?; + post_state.set_actor(&addr, post_actor)?; + Ok(Self { + store, + pre_state, + post_state, + }) + } + + /// Create state trees with actor only in post (creation scenario). + fn with_created_actor(actor_id: u64, post_actor: ActorState) -> anyhow::Result { + let store = Arc::new(MemoryDB::default()); + let pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let addr = FilecoinAddress::new_id(actor_id); + post_state.set_actor(&addr, post_actor)?; + Ok(Self { + store, + pre_state, + post_state, + }) + } + + /// Create state trees with actor only in pre (deletion scenario). + fn with_deleted_actor(actor_id: u64, pre_actor: ActorState) -> anyhow::Result { + let store = Arc::new(MemoryDB::default()); + let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?; + let addr = FilecoinAddress::new_id(actor_id); + pre_state.set_actor(&addr, pre_actor)?; + Ok(Self { + store, + pre_state, + post_state, + }) + } + + /// Build state diff for given touched addresses. + fn build_diff(&self, touched_addresses: &HashSet) -> anyhow::Result { + build_state_diff( + self.store.as_ref(), + &self.pre_state, + &self.post_state, + touched_addresses, + ) + } + } + + #[test] + fn test_build_state_diff_empty_touched_addresses() { + let trees = TestStateTrees::new().unwrap(); + let touched_addresses = HashSet::new(); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + // No addresses touched = empty state diff + assert!(state_diff.0.is_empty()); + } + + #[test] + fn test_build_state_diff_nonexistent_address() { + let trees = TestStateTrees::new().unwrap(); + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(9999)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + // Address doesn't exist in either state, so no diff (both None = unchanged) + assert!(state_diff.0.is_empty()); + } + + #[test] + fn test_build_state_diff_balance_increase() { + let actor_id = 1001u64; + let pre_actor = create_test_actor(1000, 5); + let post_actor = create_test_actor(2000, 5); + let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + assert_eq!(state_diff.0.len(), 1); + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + match &diff.balance { + Delta::Changed(change) => { + assert_eq!(change.from.0, BigInt::from(1000)); + assert_eq!(change.to.0, BigInt::from(2000)); + } + _ => panic!("Expected Delta::Changed for balance"), + } + assert!(diff.nonce.is_unchanged()); + } + + #[test] + fn test_build_state_diff_balance_decrease() { + let actor_id = 1002u64; + let pre_actor = create_test_actor(5000, 10); + let post_actor = create_test_actor(3000, 10); + let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + match &diff.balance { + Delta::Changed(change) => { + assert_eq!(change.from.0, BigInt::from(5000)); + assert_eq!(change.to.0, BigInt::from(3000)); + } + _ => panic!("Expected Delta::Changed for balance"), + } + assert!(diff.nonce.is_unchanged()); + } + + #[test] + fn test_build_state_diff_nonce_increment() { + let actor_id = 1003u64; + let pre_actor = create_test_actor(1000, 5); + let post_actor = create_test_actor(1000, 6); + let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + assert!(diff.balance.is_unchanged()); + match &diff.nonce { + Delta::Changed(change) => { + assert_eq!(change.from.0, 5); + assert_eq!(change.to.0, 6); + } + _ => panic!("Expected Delta::Changed for nonce"), + } + } + + #[test] + fn test_build_state_diff_both_balance_and_nonce_change() { + let actor_id = 1004u64; + let pre_actor = create_test_actor(10000, 100); + let post_actor = create_test_actor(9000, 101); + let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + match &diff.balance { + Delta::Changed(change) => { + assert_eq!(change.from.0, BigInt::from(10000)); + assert_eq!(change.to.0, BigInt::from(9000)); + } + _ => panic!("Expected Delta::Changed for balance"), + } + match &diff.nonce { + Delta::Changed(change) => { + assert_eq!(change.from.0, 100); + assert_eq!(change.to.0, 101); + } + _ => panic!("Expected Delta::Changed for nonce"), + } + } + + #[test] + fn test_build_state_diff_account_creation() { + let actor_id = 1005u64; + let post_actor = create_test_actor(5000, 0); + let trees = TestStateTrees::with_created_actor(actor_id, post_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + match &diff.balance { + Delta::Added(balance) => { + assert_eq!(balance.0, BigInt::from(5000)); + } + _ => panic!("Expected Delta::Added for balance"), + } + match &diff.nonce { + Delta::Added(nonce) => { + assert_eq!(nonce.0, 0); + } + _ => panic!("Expected Delta::Added for nonce"), + } + } + + #[test] + fn test_build_state_diff_account_deletion() { + let actor_id = 1006u64; + let pre_actor = create_test_actor(3000, 10); + let trees = TestStateTrees::with_deleted_actor(actor_id, pre_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + match &diff.balance { + Delta::Removed(balance) => { + assert_eq!(balance.0, BigInt::from(3000)); + } + _ => panic!("Expected Delta::Removed for balance"), + } + match &diff.nonce { + Delta::Removed(nonce) => { + assert_eq!(nonce.0, 10); + } + _ => panic!("Expected Delta::Removed for nonce"), + } + } + + #[test] + fn test_build_state_diff_multiple_addresses() { + let store = Arc::new(MemoryDB::default()); + let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap(); + let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap(); + + // Actor 1: balance increase + let addr1 = FilecoinAddress::new_id(2001); + pre_state + .set_actor(&addr1, create_test_actor(1000, 0)) + .unwrap(); + post_state + .set_actor(&addr1, create_test_actor(2000, 0)) + .unwrap(); + + // Actor 2: nonce increase + let addr2 = FilecoinAddress::new_id(2002); + pre_state + .set_actor(&addr2, create_test_actor(500, 5)) + .unwrap(); + post_state + .set_actor(&addr2, create_test_actor(500, 6)) + .unwrap(); + + // Actor 3: no change (should not appear in diff) + let addr3 = FilecoinAddress::new_id(2003); + pre_state + .set_actor(&addr3, create_test_actor(100, 1)) + .unwrap(); + post_state + .set_actor(&addr3, create_test_actor(100, 1)) + .unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(2001)); + touched_addresses.insert(create_masked_id_eth_address(2002)); + touched_addresses.insert(create_masked_id_eth_address(2003)); + + let state_diff = + build_state_diff(store.as_ref(), &pre_state, &post_state, &touched_addresses).unwrap(); + + assert_eq!(state_diff.0.len(), 2); + assert!( + state_diff + .0 + .contains_key(&create_masked_id_eth_address(2001)) + ); + assert!( + state_diff + .0 + .contains_key(&create_masked_id_eth_address(2002)) + ); + assert!( + !state_diff + .0 + .contains_key(&create_masked_id_eth_address(2003)) + ); + } + + #[test] + fn test_build_state_diff_evm_actor_scenarios() { + struct TestCase { + name: &'static str, + pre: Option<(u64, u64, Option<&'static [u8]>)>, // balance, nonce, bytecode + post: Option<(u64, u64, Option<&'static [u8]>)>, + expected_balance: Delta, + expected_nonce: Delta, + expected_code: Delta, + } + + let bytecode1: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52]; + let bytecode2: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52, 0x00]; + + let cases = vec![ + TestCase { + name: "No change", + pre: Some((1000, 5, Some(bytecode1))), + post: Some((1000, 5, Some(bytecode1))), + expected_balance: Delta::Unchanged, + expected_nonce: Delta::Unchanged, + expected_code: Delta::Unchanged, + }, + TestCase { + name: "Balance increase", + pre: Some((1000, 5, Some(bytecode1))), + post: Some((2000, 5, Some(bytecode1))), + expected_balance: Delta::Changed(ChangedType { + from: EthBigInt(BigInt::from(1000)), + to: EthBigInt(BigInt::from(2000)), + }), + expected_nonce: Delta::Unchanged, + expected_code: Delta::Unchanged, + }, + TestCase { + name: "Nonce increment", + pre: Some((1000, 5, Some(bytecode1))), + post: Some((1000, 6, Some(bytecode1))), + expected_balance: Delta::Unchanged, + expected_nonce: Delta::Changed(ChangedType { + from: EthUint64(5), + to: EthUint64(6), + }), + expected_code: Delta::Unchanged, + }, + TestCase { + name: "Bytecode change", + pre: Some((1000, 5, Some(bytecode1))), + post: Some((1000, 5, Some(bytecode2))), + expected_balance: Delta::Unchanged, + expected_nonce: Delta::Unchanged, + expected_code: Delta::Changed(ChangedType { + from: EthBytes(bytecode1.to_vec()), + to: EthBytes(bytecode2.to_vec()), + }), + }, + TestCase { + name: "Balance and Nonce change", + pre: Some((1000, 5, Some(bytecode1))), + post: Some((2000, 6, Some(bytecode1))), + expected_balance: Delta::Changed(ChangedType { + from: EthBigInt(BigInt::from(1000)), + to: EthBigInt(BigInt::from(2000)), + }), + expected_nonce: Delta::Changed(ChangedType { + from: EthUint64(5), + to: EthUint64(6), + }), + expected_code: Delta::Unchanged, + }, + TestCase { + name: "Creation", + pre: None, + post: Some((5000, 0, Some(bytecode1))), + expected_balance: Delta::Added(EthBigInt(BigInt::from(5000))), + expected_nonce: Delta::Added(EthUint64(0)), + expected_code: Delta::Added(EthBytes(bytecode1.to_vec())), + }, + TestCase { + name: "Deletion", + pre: Some((3000, 10, Some(bytecode1))), + post: None, + expected_balance: Delta::Removed(EthBigInt(BigInt::from(3000))), + expected_nonce: Delta::Removed(EthUint64(10)), + expected_code: Delta::Removed(EthBytes(bytecode1.to_vec())), + }, + ]; + + for case in cases { + let store = Arc::new(MemoryDB::default()); + let actor_id = 10000u64; // arbitrary ID + + let pre_actor = case.pre.and_then(|(bal, nonce, code)| { + create_evm_actor_with_bytecode(&store, bal, 0, nonce, code) + }); + let post_actor = case.post.and_then(|(bal, nonce, code)| { + create_evm_actor_with_bytecode(&store, bal, 0, nonce, code) + }); + + let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap(); + let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap(); + let addr = FilecoinAddress::new_id(actor_id); + + if let Some(actor) = pre_actor { + pre_state.set_actor(&addr, actor).unwrap(); + } + if let Some(actor) = post_actor { + post_state.set_actor(&addr, actor).unwrap(); + } + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = + build_state_diff(store.as_ref(), &pre_state, &post_state, &touched_addresses) + .unwrap(); + + if case.expected_balance == Delta::Unchanged + && case.expected_nonce == Delta::Unchanged + && case.expected_code == Delta::Unchanged + { + assert!( + state_diff.0.is_empty(), + "Test case '{}' failed: expected empty diff", + case.name + ); + } else { + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap_or_else(|| { + panic!("Test case '{}' failed: missing diff entry", case.name) + }); + + assert_eq!( + diff.balance, case.expected_balance, + "Test case '{}' failed: balance mismatch", + case.name + ); + assert_eq!( + diff.nonce, case.expected_nonce, + "Test case '{}' failed: nonce mismatch", + case.name + ); + assert_eq!( + diff.code, case.expected_code, + "Test case '{}' failed: code mismatch", + case.name + ); + } + } + } + + #[test] + fn test_build_state_diff_non_evm_actor_no_code() { + // Non-EVM actors should have no code in their diff + let actor_id = 4005u64; + let pre_actor = create_test_actor(1000, 5); + let post_actor = create_test_actor(2000, 6); + let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap(); + + let mut touched_addresses = HashSet::new(); + touched_addresses.insert(create_masked_id_eth_address(actor_id)); + + let state_diff = trees.build_diff(&touched_addresses).unwrap(); + + let eth_addr = create_masked_id_eth_address(actor_id); + let diff = state_diff.0.get(ð_addr).unwrap(); + + // Balance and nonce should change + assert!(!diff.balance.is_unchanged()); + assert!(!diff.nonce.is_unchanged()); + + // Code should be unchanged (None -> None for non-EVM actors) + assert!(diff.code.is_unchanged()); + } +} diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index 4db4e44fc967..015b2afae1af 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -11,6 +11,7 @@ use jsonrpsee::types::SubscriptionId; use libsecp256k1::util::FULL_PUBLIC_KEY_SIZE; use rand::Rng; use serde::de::{IntoDeserializer, value::StringDeserializer}; +use std::collections::BTreeMap; use std::{hash::Hash, ops::Deref}; pub const METHOD_GET_BYTE_CODE: u64 = 3; @@ -103,11 +104,14 @@ impl GetStorageAtParams { Eq, Hash, PartialEq, + PartialOrd, + Ord, Debug, Deserialize, Serialize, Default, Clone, + Copy, JsonSchema, derive_more::From, derive_more::Into, @@ -379,12 +383,15 @@ impl TryFrom for Message { #[derive( PartialEq, Eq, + PartialOrd, + Ord, Hash, Debug, Deserialize, Serialize, Default, Clone, + Copy, JsonSchema, derive_more::Display, derive_more::From, @@ -748,6 +755,107 @@ impl EthTrace { } } +/// Represents a changed value with before and after states. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ChangedType { + /// Value before the change + pub from: T, + /// Value after the change + pub to: T, +} + +/// Represents how a value changed during transaction execution. +// Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L84 +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub enum Delta { + /// Existing value didn't change. + #[serde(rename = "=")] + Unchanged, + /// A new value was added (account/storage created). + #[serde(rename = "+")] + Added(T), + /// The existing value was removed (account/storage deleted). + #[serde(rename = "-")] + Removed(T), + /// The existing value changed from one value to another. + #[serde(rename = "*")] + Changed(ChangedType), +} + +impl Default for Delta { + fn default() -> Self { + Delta::Unchanged + } +} + +impl Delta { + pub fn from_comparison(old: Option, new: Option) -> Self { + match (old, new) { + (None, None) => Delta::Unchanged, + (None, Some(new_val)) => Delta::Added(new_val), + (Some(old_val), None) => Delta::Removed(old_val), + (Some(old_val), Some(new_val)) => { + if old_val == new_val { + Delta::Unchanged + } else { + Delta::Changed(ChangedType { + from: old_val, + to: new_val, + }) + } + } + } + } + + pub fn is_unchanged(&self) -> bool { + matches!(self, Delta::Unchanged) + } +} + +/// Account state diff after transaction execution. +/// Tracks changes to balance, nonce, code, and storage. +// Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L156 +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct AccountDiff { + pub balance: Delta, + pub code: Delta, + pub nonce: Delta, + /// All touched/changed storage values (key -> delta) + pub storage: BTreeMap>, +} + +impl AccountDiff { + pub fn is_unchanged(&self) -> bool { + self.balance.is_unchanged() + && self.code.is_unchanged() + && self.nonce.is_unchanged() + && self.storage.is_empty() + } +} + +/// State diff containing all account changes from a transaction. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(transparent)] +pub struct StateDiff(pub BTreeMap); + +impl StateDiff { + pub fn new() -> Self { + Self(BTreeMap::new()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn insert_if_changed(&mut self, addr: EthAddress, diff: AccountDiff) { + if !diff.is_unchanged() { + self.0.insert(addr, diff); + } + } +} + +lotus_json_with_self!(StateDiff); + #[cfg(test)] mod tests { use super::*; diff --git a/src/rpc/methods/gas.rs b/src/rpc/methods/gas.rs index f0a06fd40a93..2edd8ea71b5d 100644 --- a/src/rpc/methods/gas.rs +++ b/src/rpc/methods/gas.rs @@ -254,7 +254,7 @@ impl GasEstimateGasLimit { _ => ChainMessage::Unsigned(msg), }; - let (invoc_res, apply_ret, _) = data + let (invoc_res, apply_ret, _, _) = data .state_manager .call_with_gas( &mut chain_msg, diff --git a/src/shim/actors/builtin/evm/mod.rs b/src/shim/actors/builtin/evm/mod.rs index a8cca3e8309a..68b0f50de27a 100644 --- a/src/shim/actors/builtin/evm/mod.rs +++ b/src/shim/actors/builtin/evm/mod.rs @@ -52,6 +52,14 @@ impl State { pub fn is_alive(&self) -> bool { delegate_state!(self.tombstone.is_none()) } + + pub fn bytecode(&self) -> Cid { + delegate_state!(self.bytecode) + } + + pub fn bytecode_hash(&self) -> [u8; 32] { + delegate_state!(self.bytecode_hash.into()) + } } #[delegated_enum(impl_conversions)] diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 7256d5b04de4..2cf27e0b6f52 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -20,8 +20,7 @@ use crate::chain::{ index::{ChainIndex, ResolveNullTipset}, }; use crate::interpreter::{ - ApplyResult, BlockMessages, CalledAt, ExecutionContext, IMPLICIT_MESSAGE_GAS_LIMIT, VM, - resolve_to_key_addr, + BlockMessages, CalledAt, ExecutionContext, IMPLICIT_MESSAGE_GAS_LIMIT, VM, resolve_to_key_addr, }; use crate::interpreter::{MessageCallbackCtx, VMTrace}; use crate::lotus_json::{LotusJson, lotus_json_with_self}; @@ -684,7 +683,7 @@ where tipset: Option, msg: Message, state_lookup: StateLookupPolicy, - ) -> anyhow::Result { + ) -> anyhow::Result<(ApiInvocResult, Cid)> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); let from_a = self.resolve_to_key_addr(&msg.from, &ts).await?; @@ -706,18 +705,23 @@ where _ => ChainMessage::Unsigned(msg.clone()), }; - let (_invoc_res, apply_ret, duration) = self + let (_invoc_res, apply_ret, duration, state_root) = self .call_with_gas(&mut chain_msg, &[], Some(ts), VMTrace::Traced, state_lookup) .await?; - Ok(ApiInvocResult { - msg_cid: msg.cid(), - msg, - msg_rct: Some(apply_ret.msg_receipt()), - error: apply_ret.failure_info().unwrap_or_default(), - duration: duration.as_nanos().clamp(0, u64::MAX as u128) as u64, - gas_cost: MessageGasCost::default(), - execution_trace: structured::parse_events(apply_ret.exec_trace()).unwrap_or_default(), - }) + + Ok(( + ApiInvocResult { + msg_cid: msg.cid(), + msg, + msg_rct: Some(apply_ret.msg_receipt()), + error: apply_ret.failure_info().unwrap_or_default(), + duration: duration.as_nanos().clamp(0, u64::MAX as u128) as u64, + gas_cost: MessageGasCost::default(), + execution_trace: structured::parse_events(apply_ret.exec_trace()) + .unwrap_or_default(), + }, + state_root, + )) } /// Computes message on the given [Tipset] state, after applying other @@ -729,7 +733,7 @@ where tipset: Option, trace_config: VMTrace, state_lookup: StateLookupPolicy, - ) -> Result<(InvocResult, ApplyRet, Duration), Error> { + ) -> Result<(InvocResult, ApplyRet, Duration, Cid), Error> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); let (st, _) = self .tipset_state(&ts, state_lookup) @@ -743,7 +747,7 @@ where let genesis_info = GenesisInfo::from_chain_config(self.chain_config().clone()); // FVM requires a stack size of 64MiB. The alternative is to use `ThreadedExecutor` from // FVM, but that introduces some constraints, and possible deadlocks. - let (ret, duration) = stacker::grow(64 << 20, || -> ApplyResult { + let (ret, duration, state_cid) = stacker::grow(64 << 20, || -> anyhow::Result<_> { let mut vm = VM::new( ExecutionContext { heaviest_tipset: ts.clone(), @@ -771,14 +775,18 @@ where .get_actor(&message.from()) .map_err(|e| Error::Other(format!("Could not get actor from state: {e}")))? .ok_or_else(|| Error::Other("cant find actor in state tree".to_string()))?; + message.set_sequence(from_actor.sequence); - vm.apply_message(message) + let (ret, duration) = vm.apply_message(message)?; + let state_root = vm.flush()?; + Ok((ret, duration, state_root)) })?; Ok(( InvocResult::new(message.message().clone(), &ret), ret, duration, + state_cid, )) } diff --git a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol index 5826998f6945..e9aef4dc0c24 100644 --- a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol +++ b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol @@ -10,16 +10,33 @@ contract Tracer { x = 42; } + // Allow contract to receive ETH + receive() external payable {} + // 1. Simple storage write function setX(uint256 _x) external { x = _x; } - // 2. Balance update (SSTORE) + // 2. Balance update (SSTORE) - Contract receives ETH function deposit() external payable { balances[msg.sender] = msg.value; } + // 2b. Send ETH to address - Tests balance decrease/increase + function sendEth(address payable to) external payable { + to.transfer(msg.value); + } + + // 2c. Withdraw ETH - Contract sends ETH to caller + function withdraw(uint256 amount) external { + require( + address(this).balance >= amount, + "insufficient contract balance" + ); + payable(msg.sender).transfer(amount); + } + // 3. Transfer between two accounts (SSTORE x2) function transfer(address to, uint256 amount) external { require(balances[msg.sender] >= amount, "insufficient balance"); @@ -96,9 +113,9 @@ contract Tracer { if (depth == 0) { return x; } - + uint256 callType = depth % 3; - + if (callType == 0) { // Regular CALL (bool ok, bytes memory result) = address(this).call( @@ -126,14 +143,21 @@ contract Tracer { // 13. Wide trace - multiple sibling calls at same level // Creates `width` parallel calls, each going `depth` levels deep // Example: wideTrace(3, 2) creates 3 siblings, each 2 levels deep - function wideTrace(uint256 width, uint256 depth) external returns (uint256 sum) { + function wideTrace( + uint256 width, + uint256 depth + ) external returns (uint256 sum) { if (depth == 0) { return 1; } - + for (uint256 i = 0; i < width; i++) { (bool ok, bytes memory result) = address(this).call( - abi.encodeWithSelector(this.wideTrace.selector, width, depth - 1) + abi.encodeWithSelector( + this.wideTrace.selector, + width, + depth - 1 + ) ); require(ok, "wide call failed"); sum += abi.decode(result, (uint256)); @@ -152,13 +176,13 @@ contract Tracer { abi.encodeWithSelector(this.setX.selector, 100) ); require(ok1, "setX failed"); - + // Second: delegatecall that does more calls (bool ok2, bytes memory result) = address(this).delegatecall( abi.encodeWithSelector(this.innerComplex.selector) ); require(ok2, "innerComplex failed"); - + return abi.decode(result, (uint256)); } @@ -169,24 +193,27 @@ contract Tracer { abi.encodeWithSelector(this.setX.selector, 200) ); require(ok1, "inner call 1 failed"); - + (bool ok2, ) = address(this).call( abi.encodeWithSelector(this.setX.selector, 300) ); require(ok2, "inner call 2 failed"); - + // Staticcall to read (bool ok3, bytes memory result) = address(this).staticcall( abi.encodeWithSelector(this.staticRead.selector) ); require(ok3, "staticcall failed"); - + return abi.decode(result, (uint256)); } // 15. Failing nested trace - revert at depth // Useful for testing partial trace on failure - function failAtDepth(uint256 depth, uint256 failAt) external returns (uint256) { + function failAtDepth( + uint256 depth, + uint256 failAt + ) external returns (uint256) { if (depth == failAt) { revert("intentional failure at depth"); } From aa3e558953ae63bba537944757f28d73a2987b0e Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 29 Jan 2026 20:57:22 +0530 Subject: [PATCH 05/19] add trace call api integration test script [skip ci] --- scripts/tests/trace_call_integration_test.sh | 253 +++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100755 scripts/tests/trace_call_integration_test.sh diff --git a/scripts/tests/trace_call_integration_test.sh b/scripts/tests/trace_call_integration_test.sh new file mode 100755 index 000000000000..63a177415e7f --- /dev/null +++ b/scripts/tests/trace_call_integration_test.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash +# Trace Call Comparison Test - Compares Forest's trace_call with Anvil's debug_traceCall +# Usage: ./trace_call_integration_test.sh [--deploy] [--verbose] +set -e + +# --- Parse Flags --- +DEPLOY_CONTRACT=false +VERBOSE=false +while [[ $# -gt 0 ]]; do + case $1 in + --deploy) DEPLOY_CONTRACT=true; shift ;; + --verbose) VERBOSE=true; shift ;; + *) echo "Usage: $0 [--deploy] [--verbose]"; exit 1 ;; + esac +done + +# --- Configuration --- +FOREST_RPC_URL="${FOREST_RPC_URL:-http://localhost:2345/rpc/v1}" +ANVIL_RPC_URL="${ANVIL_RPC_URL:-http://localhost:8545}" +FOREST_ACCOUNT="${FOREST_ACCOUNT:- "0xb7aa1e9c847cda5f60f1ae6f65c3eae44848d41f"}" +FOREST_CONTRACT="${FOREST_CONTRACT:- "0x8724d2eb7f86ebaef34e050b02fac6c268e56775"}" +ANVIL_ACCOUNT="${ANVIL_ACCOUNT:-"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}" +ANVIL_CONTRACT="${ANVIL_CONTRACT:-"0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"}" +ANVIL_PRIVATE_KEY="${ANVIL_PRIVATE_KEY:- ""}" + +GREEN='\033[0;32m' RED='\033[0;31m' BLUE='\033[0;34m' YELLOW='\033[0;33m' NC='\033[0m' +PASS_COUNT=0 FAIL_COUNT=0 + +# --- Dependency Check --- +command -v jq &>/dev/null || { echo "Error: jq is required"; exit 1; } +command -v curl &>/dev/null || { echo "Error: curl is required"; exit 1; } + +# --- Unified RPC Dispatcher --- +# Single entry point for all RPC calls - removes JSON-RPC boilerplate from test logic +call_rpc() { + local url="$1" method="$2" params="$3" + curl -s -X POST "$url" \ + -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" +} + +# --- RPC Health Check --- +check_rpc() { + local name="$1" url="$2" + local resp=$(call_rpc "$url" "eth_chainId" "[]") + if [ -z "$resp" ] || echo "$resp" | jq -e '.error' &>/dev/null; then + echo -e "${RED}Error: Cannot connect to $name at $url${NC}" + return 1 + fi + return 0 +} + +check_rpc "Forest" "$FOREST_RPC_URL" || exit 1 +check_rpc "Anvil" "$ANVIL_RPC_URL" || exit 1 + +# --- Deploy Contract (if requested) --- +if [ "$DEPLOY_CONTRACT" = true ]; then + command -v forge &>/dev/null || { echo "Error: forge is required for --deploy"; exit 1; } + echo -e "${YELLOW}Deploying Tracer contract on Anvil...${NC}" + CONTRACT_PATH="src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol" + ANVIL_CONTRACT=$(forge create "$CONTRACT_PATH:Tracer" \ + --rpc-url "$ANVIL_RPC_URL" \ + --private-key "$ANVIL_PRIVATE_KEY" \ + --broadcast --json 2>/dev/null | jq -r '.deployedTo') + echo -e "Deployed to: ${GREEN}$ANVIL_CONTRACT${NC}" +fi + +# --- Normalization Helpers --- +# Convert different node outputs into a standard format for comparison + +# Normalize empty values: null, "", "0x" -> "0x" +normalize_empty() { + local val="$1" + [[ "$val" == "null" || -z "$val" ]] && echo "0x" || echo "$val" +} + +# Get balance change type from Forest's Parity Delta format +# Returns: "unchanged", "changed", "added", or "removed" +get_balance_type() { + local val="$1" + # Handle unchanged cases + if [[ "$val" == "=" || "$val" == "\"=\"" || "$val" == "null" || -z "$val" ]]; then + echo "unchanged" + return + fi + # Check for Delta types + if echo "$val" | jq -e 'has("*")' &>/dev/null; then + echo "changed" + elif echo "$val" | jq -e 'has("+")' &>/dev/null; then + echo "added" + elif echo "$val" | jq -e 'has("-")' &>/dev/null; then + echo "removed" + else + echo "unchanged" + fi +} + +assert_eq() { + local label="$1" f_val="$2" a_val="$3" + + # Normalize: lowercase and treat null/0x/empty as equivalent + local f_norm=$(echo "$f_val" | tr '[:upper:]' '[:lower:]') + local a_norm=$(echo "$a_val" | tr '[:upper:]' '[:lower:]') + [[ "$f_norm" == "null" || -z "$f_norm" ]] && f_norm="0x" + [[ "$a_norm" == "null" || -z "$a_norm" ]] && a_norm="0x" + + if [ "$f_norm" = "$a_norm" ]; then + echo -e " ${GREEN}[PASS]${NC} $label: $f_val" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo -e " ${RED}[FAIL]${NC} $label: (Forest: $f_val | Anvil: $a_val)" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +assert_both_have_error() { + local f_err="$1" a_err="$2" + if [[ -n "$f_err" && "$f_err" != "null" ]] && [[ -n "$a_err" && "$a_err" != "null" ]]; then + echo -e " ${GREEN}[PASS]${NC} Error: both have error" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo -e " ${RED}[FAIL]${NC} Error: (Forest: '$f_err' | Anvil: '$a_err')" + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +# Compares Forest's trace_call [trace] with Anvil's debug_traceCall (callTracer) +# Types: "standard" (default), "revert", "deep" +test_trace() { + local name="$1" data="$2" type="${3:-standard}" + echo -e "${BLUE}--- $name ---${NC}" + + # Forest: trace_call with trace + local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"trace\"],\"latest\"]" + local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") + + # Anvil: debug_traceCall with callTracer + local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"callTracer\"}]" + local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") + + [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" + + # Extract & compare input (common to all types) + local f_input=$(echo "$f_resp" | jq -r '.result.trace[0].action.input') + local a_input=$(echo "$a_resp" | jq -r '.result.input') + assert_eq "Input" "$f_input" "$a_input" + + # Type-specific comparisons + case $type in + revert) + local f_err=$(echo "$f_resp" | jq -r '.result.trace[0].error // empty') + local a_err=$(echo "$a_resp" | jq -r '.result.error // empty') + assert_both_have_error "$f_err" "$a_err" + ;; + deep) + local f_count=$(echo "$f_resp" | jq -r '.result.trace | length') + local a_count=$(echo "$a_resp" | jq '[.. | objects | select(has("type"))] | length') + assert_eq "TraceCount" "$f_count" "$a_count" + ;; + *) + local f_out=$(normalize_empty "$(echo "$f_resp" | jq -r '.result.trace[0].result.output // .result.output')") + local a_out=$(normalize_empty "$(echo "$a_resp" | jq -r '.result.output')") + local f_sub=$(echo "$f_resp" | jq -r '.result.trace[0].subtraces // 0') + local a_sub=$(echo "$a_resp" | jq -r '.result.calls // [] | length') + assert_eq "Output" "$f_out" "$a_out" + assert_eq "Subcalls" "$f_sub" "$a_sub" + ;; + esac + echo "" +} + +# Compares Forest's trace_call [stateDiff] with Anvil's prestateTracer (diffMode) +test_state_diff() { + local name="$1" data="$2" value="${3:-0x0}" expect="${4:-unchanged}" + echo -e "${BLUE}--- $name (stateDiff) ---${NC}" + + # Forest: trace_call with stateDiff + local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},[\"stateDiff\"],\"latest\"]" + local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") + + # Anvil: prestateTracer with diffMode + local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" + local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") + + [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" + + # Extract contract addresses (lowercase for jq lookup) + local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') + local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') + + # Extract Forest stateDiff balance + local f_diff=$(echo "$f_resp" | jq '.result.stateDiff // {}') + local f_bal=$(echo "$f_diff" | jq -r --arg a "$f_contract_lower" '.[$a].balance // "="') + local f_type=$(get_balance_type "$f_bal") + + # Extract Anvil pre/post balance and determine change type + local a_pre_bal=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" '.result.pre[$a].balance // "0x0"') + local a_post_bal=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" '.result.post[$a].balance // "0x0"') + local a_type="unchanged" + [[ "$a_pre_bal" != "$a_post_bal" ]] && a_type="changed" + + # Semantic assertions - compare intent, not raw values + assert_eq "Forest matches Expected" "$f_type" "$expect" + assert_eq "Forest matches Anvil" "$f_type" "$a_type" + echo "" +} + +echo "==============================================" +echo "Trace Call Comparison: Forest vs Anvil" +echo "==============================================" +echo "Forest: $FOREST_RPC_URL | Contract: $FOREST_CONTRACT" +echo "Anvil: $ANVIL_RPC_URL | Contract: $ANVIL_CONTRACT" +echo "" + +# --- Trace Tests --- +echo -e "${BLUE}=== Trace Tests ===${NC}" +echo "" + +test_trace "setX(123)" \ + "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" + +test_trace "doRevert()" \ + "0xafc874d2" \ + "revert" + +test_trace "callSelf(999)" \ + "0xa1a8859500000000000000000000000000000000000000000000000000000000000003e7" + +test_trace "complexTrace()" \ + "0x6659ab96" + +test_trace "deepTrace(3)" \ + "0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" \ + "deep" + +# --- StateDiff Tests --- +echo -e "${BLUE}=== StateDiff Tests ===${NC}" +echo "" + +test_state_diff "deposit() with 1 ETH" \ + "0xd0e30db0" \ + "0xde0b6b3a7640000" \ + "changed" + +test_state_diff "setX(42) no value" \ + "0x4018d9aa000000000000000000000000000000000000000000000000000000000000002a" \ + "0x0" \ + "unchanged" + +# --- Results --- +echo "==============================================" +echo -e "Results: ${GREEN}Passed: $PASS_COUNT${NC} | ${RED}Failed: $FAIL_COUNT${NC}" +[[ $FAIL_COUNT -gt 0 ]] && exit 1 || exit 0 From f36d8c35f69dd8718bc7f1efd54def2df3feeaaa Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Tue, 3 Feb 2026 19:45:22 +0530 Subject: [PATCH 06/19] add support for the evm storage slot in the state diff --- Cargo.lock | 18 ++ Cargo.toml | 1 + src/rpc/methods/eth/trace.rs | 191 ++++++++++++++++-- src/rpc/methods/eth/types.rs | 4 - src/shim/actors/builtin/evm/mod.rs | 4 + .../api_cmd/contracts/tracer/Tracer.sol | 90 ++++++++- 6 files changed, 290 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f238a777df18..fab9660136ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3298,6 +3298,7 @@ dependencies = [ "fvm_actor_utils", "fvm_ipld_blockstore", "fvm_ipld_encoding", + "fvm_ipld_kamt", "fvm_shared 2.11.3", "fvm_shared 3.13.3", "fvm_shared 4.7.5", @@ -3921,6 +3922,23 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "fvm_ipld_kamt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a7ef4cf7dab3bf09a2c988f7fc88fa0a532be2ca96136470942426ec5f99f0" +dependencies = [ + "anyhow", + "byteorder", + "cid", + "fvm_ipld_blockstore", + "fvm_ipld_encoding", + "multihash-codetable", + "once_cell", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "fvm_sdk" version = "4.7.5" diff --git a/Cargo.toml b/Cargo.toml index 12faeff5653b..7c8a2f45cbff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ fvm4 = { package = "fvm", version = "~4.7", default-features = false, features = fvm_actor_utils = "14" fvm_ipld_blockstore = "0.3.1" fvm_ipld_encoding = "0.5.3" +fvm_ipld_kamt = "0.4.5" fvm_shared2 = { package = "fvm_shared", version = "~2.11" } fvm_shared3 = { package = "fvm_shared", version = "~3.13", features = ["proofs"] } fvm_shared4 = { package = "fvm_shared", version = "~4.7", features = ["proofs"] } diff --git a/src/rpc/methods/eth/trace.rs b/src/rpc/methods/eth/trace.rs index dd563ee2c587..83a0e1525123 100644 --- a/src/rpc/methods/eth/trace.rs +++ b/src/rpc/methods/eth/trace.rs @@ -1,33 +1,67 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use super::types::{EthAddress, EthBytes, EthCallTraceAction, EthTrace, TraceAction, TraceResult}; +use super::types::{ + EthAddress, EthBytes, EthCallTraceAction, EthHash, EthTrace, TraceAction, TraceResult, +}; use super::utils::{decode_params, decode_return}; use super::{ EthCallTraceResult, EthCreateTraceAction, EthCreateTraceResult, decode_payload, encode_filecoin_params_as_abi, encode_filecoin_returns_as_abi, }; use crate::eth::{EAMMethod, EVMMethod}; +use crate::rpc::eth::types::{AccountDiff, Delta, StateDiff}; +use crate::rpc::eth::{EthBigInt, EthUint64, MAX_STATE_DIFF_ADDRESSES}; use crate::rpc::methods::eth::lookup_eth_address; use crate::rpc::methods::state::ExecutionTrace; use crate::rpc::state::ActorTrace; +use crate::shim::actors::{EVMActorStateLoad, evm}; use crate::shim::fvm_shared_latest::METHOD_CONSTRUCTOR; +use crate::shim::state_tree::ActorState; use crate::shim::{actors::is_evm_actor, address::Address, error::ExitCode, state_tree::StateTree}; -use ahash::HashSet; +use ahash::{HashMap, HashSet}; +use anyhow::{Context, bail}; use fil_actor_eam_state::v12 as eam12; +use fil_actor_evm_state::evm_shared::v17::uints::U256; use fil_actor_evm_state::v15 as evm12; use fil_actor_init_state::v12::ExecReturn; use fil_actor_init_state::v15::Method as InitMethod; use fvm_ipld_blockstore::Blockstore; - -use crate::rpc::eth::types::{AccountDiff, Delta, StateDiff}; -use crate::rpc::eth::{EthBigInt, EthUint64, MAX_STATE_DIFF_ADDRESSES}; -use crate::shim::actors::{EVMActorStateLoad, evm}; -use crate::shim::state_tree::ActorState; -use anyhow::{Context, bail}; +use fvm_ipld_kamt::{AsHashedKey, Config as KamtConfig, HashedKey, Kamt}; use num::FromPrimitive; +use std::borrow::Cow; +use std::collections::BTreeMap; use tracing::debug; +/// KAMT configuration matching the EVM actor in builtin-actors. +// Code is taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L47 +fn evm_kamt_config() -> KamtConfig { + KamtConfig { + bit_width: 5, // 32 children per node (2^5) + min_data_depth: 0, // Data can be stored at root level + max_array_width: 1, // Max 1 key-value pair per bucket + } +} + +/// Hash algorithm for EVM storage KAMT. +// Code taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L49. +pub struct EvmStateHashAlgorithm; + +impl AsHashedKey for EvmStateHashAlgorithm { + fn as_hashed_key(key: &U256) -> Cow<'_, HashedKey<32>> { + Cow::Owned(key.to_big_endian()) + } +} + +/// Type alias for EVM storage KAMT with configuration. +type EvmStorageKamt = Kamt; + +fn u256_to_eth_hash(value: &U256) -> EthHash { + EthHash(ethereum_types::H256(value.to_big_endian())) +} + +const ZERO_HASH: EthHash = EthHash(ethereum_types::H256([0u8; 32])); + #[derive(Default)] pub struct Environment { caller: EthAddress, @@ -687,15 +721,13 @@ fn build_account_diff( } }; - // Helper to get bytecode from EVM actor + // Helper to get bytecode from an EVM actor let get_bytecode = |actor: &ActorState| -> Option { if !is_evm_actor(&actor.code) { return None; } - // Load EVM state and get bytecode CID let evm_state = evm::State::load(store, actor.code, actor.state).ok()?; - // Load actual bytecode from blockstore store .get(&evm_state.bytecode()) .ok() @@ -713,11 +745,146 @@ fn build_account_diff( let post_code = post_actor.and_then(get_bytecode); diff.code = Delta::from_comparison(pre_code, post_code); - // TODO: implement EVM storage slot comparison + // Compare storage slots for EVM actors + diff.storage = diff_evm_storage_for_actors(store, pre_actor, post_actor)?; Ok(diff) } +/// Compute storage diff between pre and post actor states. +/// +/// Uses different Delta types based on the scenario: +/// - Account created (None → EVM): storage slots are `Delta::Added` +/// - Account deleted (EVM → None): storage slots are `Delta::Removed` +/// - Account modified (EVM → EVM): storage slots are `Delta::Changed` +/// - Actor type changed (EVM ↔ non-EVM): treated as deletion + creation +fn diff_evm_storage_for_actors( + store: &DB, + pre_actor: Option<&ActorState>, + post_actor: Option<&ActorState>, +) -> anyhow::Result>> { + let pre_is_evm = pre_actor.is_some_and(|a| is_evm_actor(&a.code)); + let post_is_evm = post_actor.is_some_and(|a| is_evm_actor(&a.code)); + + // Extract storage entries from EVM actors (empty map for non-EVM or missing actors) + let pre_entries = extract_evm_storage_entries(store, pre_actor); + let post_entries = extract_evm_storage_entries(store, post_actor); + + // If both are empty, no storage diff + if pre_entries.is_empty() && post_entries.is_empty() { + return Ok(BTreeMap::new()); + } + + let mut diff = BTreeMap::new(); + + match (pre_is_evm, post_is_evm) { + (false, true) => { + for (key_bytes, value) in &post_entries { + let key_hash = EthHash(ethereum_types::H256(*key_bytes)); + diff.insert(key_hash, Delta::Added(u256_to_eth_hash(value))); + } + } + (true, false) => { + for (key_bytes, value) in &pre_entries { + let key_hash = EthHash(ethereum_types::H256(*key_bytes)); + diff.insert(key_hash, Delta::Removed(u256_to_eth_hash(value))); + } + } + (true, true) => { + for (key_bytes, pre_value) in &pre_entries { + let key_hash = EthHash(ethereum_types::H256(*key_bytes)); + let pre_hash = u256_to_eth_hash(pre_value); + + match post_entries.get(key_bytes) { + Some(post_value) if pre_value != post_value => { + // Value changed + diff.insert( + key_hash, + Delta::Changed(super::types::ChangedType { + from: pre_hash, + to: u256_to_eth_hash(post_value), + }), + ); + } + Some(_) => { + // Value unchanged, skip + } + None => { + // Slot cleared (value → zero) + diff.insert( + key_hash, + Delta::Changed(super::types::ChangedType { + from: pre_hash, + to: ZERO_HASH, + }), + ); + } + } + } + + // Check for newly written entries (zero → value) + for (key_bytes, post_value) in &post_entries { + if !pre_entries.contains_key(key_bytes) { + let key_hash = EthHash(ethereum_types::H256(*key_bytes)); + diff.insert( + key_hash, + Delta::Changed(super::types::ChangedType { + from: ZERO_HASH, + to: u256_to_eth_hash(post_value), + }), + ); + } + } + } + // Neither EVM: no storage diff + (false, false) => {} + } + + Ok(diff) +} + +/// Extract all storage entries from an EVM actor's KAMT. +/// Returns empty map if actor is None, not an EVM actor, or state cannot be loaded. +fn extract_evm_storage_entries( + store: &DB, + actor: Option<&ActorState>, +) -> HashMap<[u8; 32], U256> { + let actor = match actor { + Some(a) if is_evm_actor(&a.code) => a, + _ => return HashMap::default(), + }; + + let evm_state = match evm::State::load(store, actor.code, actor.state) { + Ok(state) => state, + Err(e) => { + debug!("failed to load EVM state for storage extraction: {e}"); + return HashMap::default(); + } + }; + + let storage_cid = evm_state.contract_state(); + let config = evm_kamt_config(); + + let kamt: EvmStorageKamt<&DB> = match Kamt::load_with_config(&storage_cid, store, config) { + Ok(k) => k, + Err(e) => { + debug!("failed to load storage KAMT: {e}"); + return HashMap::default(); + } + }; + + let mut entries = HashMap::default(); + if let Err(e) = kamt.for_each(|key, value| { + entries.insert(key.to_big_endian(), *value); + Ok(()) + }) { + debug!("failed to iterate storage KAMT: {e}"); + return HashMap::default(); + } + + entries +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index 015b2afae1af..3a332feed316 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -843,10 +843,6 @@ impl StateDiff { Self(BTreeMap::new()) } - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - pub fn insert_if_changed(&mut self, addr: EthAddress, diff: AccountDiff) { if !diff.is_unchanged() { self.0.insert(addr, diff); diff --git a/src/shim/actors/builtin/evm/mod.rs b/src/shim/actors/builtin/evm/mod.rs index 68b0f50de27a..109daff1e314 100644 --- a/src/shim/actors/builtin/evm/mod.rs +++ b/src/shim/actors/builtin/evm/mod.rs @@ -60,6 +60,10 @@ impl State { pub fn bytecode_hash(&self) -> [u8; 32] { delegate_state!(self.bytecode_hash.into()) } + + pub fn contract_state(&self) -> Cid { + delegate_state!(self.contract_state) + } } #[delegated_enum(impl_conversions)] diff --git a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol index e9aef4dc0c24..b61a7e4c3ffb 100644 --- a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol +++ b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol @@ -2,8 +2,15 @@ pragma solidity ^0.8.30; contract Tracer { - uint256 public x; - mapping(address => uint256) public balances; + uint256 public x; // slot 0 - initialized to 42 + mapping(address => uint256) public balances; // slot 1 - mapping base + + // Storage slots for stateDiff testing (uninitialized = start empty) + uint256 public storageTestA; // slot 2 - for add/change/delete tests + uint256 public storageTestB; // slot 3 - for multiple slot tests + uint256 public storageTestC; // slot 4 - for multiple slot tests + uint256[] public dynamicArray; // slot 5 - for array storage tests + event Transfer(address indexed from, address indexed to, uint256 value); constructor() payable { @@ -226,4 +233,83 @@ contract Tracer { require(ok, "nested call failed"); return abi.decode(result, (uint256)); } + + // ========== STORAGE DIFF TEST FUNCTIONS ========== + // These functions test stateDiff storage tracking: + // - Added (+): Write non-zero to empty slot (slot was 0) + // - Changed (*): Change non-zero to different non-zero + // - Removed (-): Set non-zero slot to 0 + + // 16. Storage Add - Write to empty slot + // First call creates "Added" (+) entry in stateDiff.storage + function storageAdd(uint256 value) external { + require(value != 0, "use non-zero for add test"); + storageTestA = value; + } + + // 17. Storage Change - Modify existing slot + // Creates "Changed" (*) entry in stateDiff.storage + function storageChange(uint256 newValue) external { + require(storageTestA != 0, "slot must have value first"); + require( + newValue != 0 && newValue != storageTestA, + "use different non-zero" + ); + storageTestA = newValue; + } + + // 18. Storage Delete - Set slot to zero + // Creates "Removed" (-) entry in stateDiff.storage + function storageDelete() external { + require(storageTestA != 0, "slot must have value first"); + storageTestA = 0; + } + + // 19. Storage Multiple - Change multiple slots in one call + // Useful for testing multiple storage entries in stateDiff + function storageMultiple(uint256 a, uint256 b, uint256 c) external { + storageTestA = a; + storageTestB = b; + storageTestC = c; + } + + // 20. Storage Mixed - Add, Change, and Delete in one call + // Requires: storageTestA has value, storageTestB is empty + function storageMixed(uint256 newA, uint256 newC) external { + // Change existing (storageTestA should have value) + storageTestA = newA; + // Delete (set to 0) + storageTestB = 0; + // Add new value + storageTestC = newC; + } + + // 21. Array Push - Adds new storage slot + // Dynamic arrays use keccak256(slot) + index for element storage + function arrayPush(uint256 value) external { + dynamicArray.push(value); + } + + // 22. Array Pop - Removes storage slot (sets to 0) + function arrayPop() external { + require(dynamicArray.length > 0, "array is empty"); + dynamicArray.pop(); + } + + // 23. Reset storage test slots to initial state (all zeros) + function storageReset() external { + storageTestA = 0; + storageTestB = 0; + storageTestC = 0; + delete dynamicArray; + } + + // 24. Get storage test values (for verification) + function getStorageTestValues() + external + view + returns (uint256, uint256, uint256, uint256) + { + return (storageTestA, storageTestB, storageTestC, dynamicArray.length); + } } From 47b7aa31fd066784940dd7f9297b7df6ff01bdc8 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Tue, 3 Feb 2026 19:45:50 +0530 Subject: [PATCH 07/19] include storage slot diff in the trace call test script --- scripts/tests/trace_call_integration_test.sh | 175 +++++++++++++------ 1 file changed, 122 insertions(+), 53 deletions(-) diff --git a/scripts/tests/trace_call_integration_test.sh b/scripts/tests/trace_call_integration_test.sh index 63a177415e7f..939d2ce60f97 100755 --- a/scripts/tests/trace_call_integration_test.sh +++ b/scripts/tests/trace_call_integration_test.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash -# Trace Call Comparison Test - Compares Forest's trace_call with Anvil's debug_traceCall -# Usage: ./trace_call_integration_test.sh [--deploy] [--verbose] set -e -# --- Parse Flags --- DEPLOY_CONTRACT=false VERBOSE=false while [[ $# -gt 0 ]]; do @@ -17,21 +14,18 @@ done # --- Configuration --- FOREST_RPC_URL="${FOREST_RPC_URL:-http://localhost:2345/rpc/v1}" ANVIL_RPC_URL="${ANVIL_RPC_URL:-http://localhost:8545}" -FOREST_ACCOUNT="${FOREST_ACCOUNT:- "0xb7aa1e9c847cda5f60f1ae6f65c3eae44848d41f"}" -FOREST_CONTRACT="${FOREST_CONTRACT:- "0x8724d2eb7f86ebaef34e050b02fac6c268e56775"}" -ANVIL_ACCOUNT="${ANVIL_ACCOUNT:-"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}" -ANVIL_CONTRACT="${ANVIL_CONTRACT:-"0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"}" -ANVIL_PRIVATE_KEY="${ANVIL_PRIVATE_KEY:- ""}" +FOREST_ACCOUNT="${FOREST_ACCOUNT:-0xb7aa1e9c847cda5f60f1ae6f65c3eae44848d41f}" +FOREST_CONTRACT="${FOREST_CONTRACT:-0x73a43475aa2ccb14246613708b399f4b2ba546c7}" +ANVIL_ACCOUNT="${ANVIL_ACCOUNT:-0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266}" +ANVIL_CONTRACT="${ANVIL_CONTRACT:-0x5FbDB2315678afecb367f032d93F642f64180aa3}" +ANVIL_PRIVATE_KEY="${ANVIL_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" GREEN='\033[0;32m' RED='\033[0;31m' BLUE='\033[0;34m' YELLOW='\033[0;33m' NC='\033[0m' PASS_COUNT=0 FAIL_COUNT=0 -# --- Dependency Check --- command -v jq &>/dev/null || { echo "Error: jq is required"; exit 1; } command -v curl &>/dev/null || { echo "Error: curl is required"; exit 1; } -# --- Unified RPC Dispatcher --- -# Single entry point for all RPC calls - removes JSON-RPC boilerplate from test logic call_rpc() { local url="$1" method="$2" params="$3" curl -s -X POST "$url" \ @@ -39,7 +33,6 @@ call_rpc() { -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" } -# --- RPC Health Check --- check_rpc() { local name="$1" url="$2" local resp=$(call_rpc "$url" "eth_chainId" "[]") @@ -53,7 +46,6 @@ check_rpc() { check_rpc "Forest" "$FOREST_RPC_URL" || exit 1 check_rpc "Anvil" "$ANVIL_RPC_URL" || exit 1 -# --- Deploy Contract (if requested) --- if [ "$DEPLOY_CONTRACT" = true ]; then command -v forge &>/dev/null || { echo "Error: forge is required for --deploy"; exit 1; } echo -e "${YELLOW}Deploying Tracer contract on Anvil...${NC}" @@ -65,26 +57,16 @@ if [ "$DEPLOY_CONTRACT" = true ]; then echo -e "Deployed to: ${GREEN}$ANVIL_CONTRACT${NC}" fi -# --- Normalization Helpers --- -# Convert different node outputs into a standard format for comparison - -# Normalize empty values: null, "", "0x" -> "0x" normalize_empty() { local val="$1" [[ "$val" == "null" || -z "$val" ]] && echo "0x" || echo "$val" } -# Get balance change type from Forest's Parity Delta format -# Returns: "unchanged", "changed", "added", or "removed" -get_balance_type() { +get_delta_type() { local val="$1" - # Handle unchanged cases if [[ "$val" == "=" || "$val" == "\"=\"" || "$val" == "null" || -z "$val" ]]; then echo "unchanged" - return - fi - # Check for Delta types - if echo "$val" | jq -e 'has("*")' &>/dev/null; then + elif echo "$val" | jq -e 'has("*")' &>/dev/null; then echo "changed" elif echo "$val" | jq -e 'has("+")' &>/dev/null; then echo "added" @@ -97,8 +79,6 @@ get_balance_type() { assert_eq() { local label="$1" f_val="$2" a_val="$3" - - # Normalize: lowercase and treat null/0x/empty as equivalent local f_norm=$(echo "$f_val" | tr '[:upper:]' '[:lower:]') local a_norm=$(echo "$a_val" | tr '[:upper:]' '[:lower:]') [[ "$f_norm" == "null" || -z "$f_norm" ]] && f_norm="0x" @@ -116,36 +96,30 @@ assert_eq() { assert_both_have_error() { local f_err="$1" a_err="$2" if [[ -n "$f_err" && "$f_err" != "null" ]] && [[ -n "$a_err" && "$a_err" != "null" ]]; then - echo -e " ${GREEN}[PASS]${NC} Error: both have error" + echo -e " ${GREEN}[PASS]${NC} Both have error" PASS_COUNT=$((PASS_COUNT + 1)) else - echo -e " ${RED}[FAIL]${NC} Error: (Forest: '$f_err' | Anvil: '$a_err')" + echo -e " ${RED}[FAIL]${NC} Error mismatch (Forest: '$f_err' | Anvil: '$a_err')" FAIL_COUNT=$((FAIL_COUNT + 1)) fi } -# Compares Forest's trace_call [trace] with Anvil's debug_traceCall (callTracer) -# Types: "standard" (default), "revert", "deep" test_trace() { local name="$1" data="$2" type="${3:-standard}" echo -e "${BLUE}--- $name ---${NC}" - # Forest: trace_call with trace local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"trace\"],\"latest\"]" local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") - # Anvil: debug_traceCall with callTracer local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"callTracer\"}]" local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" - # Extract & compare input (common to all types) local f_input=$(echo "$f_resp" | jq -r '.result.trace[0].action.input') local a_input=$(echo "$a_resp" | jq -r '.result.input') assert_eq "Input" "$f_input" "$a_input" - # Type-specific comparisons case $type in revert) local f_err=$(echo "$f_resp" | jq -r '.result.trace[0].error // empty') @@ -169,42 +143,114 @@ test_trace() { echo "" } -# Compares Forest's trace_call [stateDiff] with Anvil's prestateTracer (diffMode) -test_state_diff() { +test_balance_diff() { local name="$1" data="$2" value="${3:-0x0}" expect="${4:-unchanged}" - echo -e "${BLUE}--- $name (stateDiff) ---${NC}" + echo -e "${BLUE}--- $name (balance) ---${NC}" - # Forest: trace_call with stateDiff local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},[\"stateDiff\"],\"latest\"]" local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") - # Anvil: prestateTracer with diffMode local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" - # Extract contract addresses (lowercase for jq lookup) local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') - # Extract Forest stateDiff balance - local f_diff=$(echo "$f_resp" | jq '.result.stateDiff // {}') - local f_bal=$(echo "$f_diff" | jq -r --arg a "$f_contract_lower" '.[$a].balance // "="') - local f_type=$(get_balance_type "$f_bal") + local f_bal=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" '.result.stateDiff[$a].balance // "="') + local f_type=$(get_delta_type "$f_bal") - # Extract Anvil pre/post balance and determine change type local a_pre_bal=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" '.result.pre[$a].balance // "0x0"') local a_post_bal=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" '.result.post[$a].balance // "0x0"') local a_type="unchanged" [[ "$a_pre_bal" != "$a_post_bal" ]] && a_type="changed" - # Semantic assertions - compare intent, not raw values - assert_eq "Forest matches Expected" "$f_type" "$expect" - assert_eq "Forest matches Anvil" "$f_type" "$a_type" + assert_eq "BalanceChange" "$f_type" "$expect" + assert_eq "ForestMatchesAnvil" "$f_type" "$a_type" echo "" } +test_storage_diff() { + local name="$1" data="$2" slot="$3" expect_type="${4:-changed}" + echo -e "${BLUE}--- $name (storage) ---${NC}" + + local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"stateDiff\"],\"latest\"]" + local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") + + local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" + local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") + + [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" + + local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') + local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') + + # Forest: Extract storage slot delta and values + local f_slot_data=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" --arg s "$slot" '.result.stateDiff[$a].storage[$s] // null') + local f_type=$(get_delta_type "$f_slot_data") + local f_to_val="" + if [ "$f_type" = "changed" ]; then + f_to_val=$(echo "$f_slot_data" | jq -r '.["*"].to // empty') + elif [ "$f_type" = "added" ]; then + f_to_val=$(echo "$f_slot_data" | jq -r '.["+"] // empty') + fi + + # Anvil: Extract storage slot pre/post values + local a_pre_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.pre[$a].storage[$s] // "0x0"') + local a_post_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] // "0x0"') + local a_type="unchanged" + if [[ "$a_pre_val" == "0x0" || "$a_pre_val" == "null" ]] && [[ "$a_post_val" != "0x0" && "$a_post_val" != "null" ]]; then + a_type="changed" # Added (was zero, now non-zero) + elif [[ "$a_pre_val" != "0x0" && "$a_pre_val" != "null" ]] && [[ "$a_post_val" != "$a_pre_val" ]]; then + a_type="changed" # Modified + fi + + assert_eq "StorageChangeType" "$f_type" "$expect_type" + assert_eq "ForestMatchesAnvil" "$f_type" "$a_type" + + # Compare actual values if both have the slot + if [[ -n "$f_to_val" && -n "$a_post_val" && "$a_post_val" != "null" ]]; then + assert_eq "StorageValue" "$f_to_val" "$a_post_val" + fi + echo "" +} + +test_storage_multiple() { + local name="$1" data="$2" + shift 2 + local slots=("$@") + echo -e "${BLUE}--- $name (multi-storage) ---${NC}" + + local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"stateDiff\"],\"latest\"]" + local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") + + local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" + local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") + + [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" + + local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') + local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') + + local f_slot_count=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" '.result.stateDiff[$a].storage | length') + local a_slot_count=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" '.result.post[$a].storage | length') + assert_eq "SlotCount" "$f_slot_count" "$a_slot_count" + + for slot in "${slots[@]}"; do + local f_slot_data=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" --arg s "$slot" '.result.stateDiff[$a].storage[$s] // null') + local f_to_val=$(echo "$f_slot_data" | jq -r '.["*"].to // .["+"] // empty') + local a_post_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] // empty') + + local slot_short="${slot: -4}" + assert_eq "Slot$slot_short" "$f_to_val" "$a_post_val" + done + echo "" +} + +# ============================================================================= +# Main Execution +# ============================================================================= echo "==============================================" echo "Trace Call Comparison: Forest vs Anvil" echo "==============================================" @@ -233,20 +279,43 @@ test_trace "deepTrace(3)" \ "0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" \ "deep" -# --- StateDiff Tests --- -echo -e "${BLUE}=== StateDiff Tests ===${NC}" +# --- Balance Diff Tests --- +echo -e "${BLUE}=== Balance Diff Tests ===${NC}" echo "" -test_state_diff "deposit() with 1 ETH" \ +test_balance_diff "deposit() with 1 ETH" \ "0xd0e30db0" \ "0xde0b6b3a7640000" \ "changed" -test_state_diff "setX(42) no value" \ +test_balance_diff "setX(42) no value" \ "0x4018d9aa000000000000000000000000000000000000000000000000000000000000002a" \ "0x0" \ "unchanged" +# --- Storage Diff Tests --- +echo -e "${BLUE}=== Storage Diff Tests ===${NC}" +echo "" + +# Slot 0: x variable (initialized to 42) +test_storage_diff "setX(123) - change slot 0" \ + "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" \ + "0x0000000000000000000000000000000000000000000000000000000000000000" \ + "changed" + +# Slot 2: storageTestA (starts empty) +test_storage_diff "storageAdd(100) - add slot 2" \ + "0x55cb64b40000000000000000000000000000000000000000000000000000000000000064" \ + "0x0000000000000000000000000000000000000000000000000000000000000002" \ + "changed" + +# Multiple slots: storageTestA(10), storageTestB(20), storageTestC(30) +test_storage_multiple "storageMultiple(10,20,30) - slots 2,3,4" \ + "0x310af204000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e" \ + "0x0000000000000000000000000000000000000000000000000000000000000002" \ + "0x0000000000000000000000000000000000000000000000000000000000000003" \ + "0x0000000000000000000000000000000000000000000000000000000000000004" + # --- Results --- echo "==============================================" echo -e "Results: ${GREEN}Passed: $PASS_COUNT${NC} | ${RED}Failed: $FAIL_COUNT${NC}" From c3b0eb58f6dd2e7cf2ec8cba8bbb53be98f74517 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Tue, 3 Feb 2026 20:03:35 +0530 Subject: [PATCH 08/19] fix ci linter issue --- scripts/tests/trace_call_integration_test.sh | 2 +- src/rpc/methods/eth.rs | 44 ++++++++-------- src/rpc/methods/eth/filter/mod.rs | 36 +++++-------- src/rpc/methods/eth/trace.rs | 28 ++++------- src/rpc/methods/eth/types.rs | 15 ++---- .../subcommands/api_cmd/api_compare_tests.rs | 50 +++++++++---------- .../subcommands/api_cmd/stateful_tests.rs | 4 +- 7 files changed, 77 insertions(+), 102 deletions(-) diff --git a/scripts/tests/trace_call_integration_test.sh b/scripts/tests/trace_call_integration_test.sh index 939d2ce60f97..6038673295fb 100755 --- a/scripts/tests/trace_call_integration_test.sh +++ b/scripts/tests/trace_call_integration_test.sh @@ -241,7 +241,7 @@ test_storage_multiple() { local f_slot_data=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" --arg s "$slot" '.result.stateDiff[$a].storage[$s] // null') local f_to_val=$(echo "$f_slot_data" | jq -r '.["*"].to // .["+"] // empty') local a_post_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] // empty') - + local slot_short="${slot: -4}" assert_eq "Slot$slot_short" "$f_to_val" "$a_post_val" done diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 20eacc96b284..a75d1a2b0b7f 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -269,6 +269,7 @@ lotus_json_with_self!(EthInt64); impl EthHash { // Should ONLY be used for blocks and Filecoin messages. Eth transactions expect a different hashing scheme. + #[allow(clippy::wrong_self_convention)] pub fn to_cid(&self) -> cid::Cid { let mh = MultihashCode::Blake2b256 .wrap(self.0.as_bytes()) @@ -636,7 +637,7 @@ impl Block { &state_tree, ctx.chain_config().eth_chain_id, )?; - tx.block_hash = block_hash.clone(); + tx.block_hash = block_hash; tx.block_number = block_number; tx.transaction_index = ti; full_transactions.push(tx); @@ -1505,11 +1506,11 @@ async fn new_eth_tx_receipt( msg_receipt: &Receipt, ) -> anyhow::Result { let mut tx_receipt = EthTxReceipt { - transaction_hash: tx.hash.clone(), - from: tx.from.clone(), - to: tx.to.clone(), + transaction_hash: tx.hash, + from: tx.from, + to: tx.to, transaction_index: tx.transaction_index, - block_hash: tx.block_hash.clone(), + block_hash: tx.block_hash, block_number: tx.block_number, r#type: tx.r#type, status: (msg_receipt.exit_code().is_success() as u64).into(), @@ -1572,7 +1573,7 @@ pub async fn eth_logs_for_block_and_transaction anyhow::Result> { let spec = EthFilterSpec { - block_hash: Some(block_hash.clone()), + block_hash: Some(*block_hash), ..Default::default() }; @@ -3928,9 +3929,9 @@ where result: trace.result, error: trace.error, }, - block_hash: block_hash.clone(), + block_hash, block_number: ts.epoch(), - transaction_hash: tx_hash.clone(), + transaction_hash: tx_hash, transaction_position: msg_idx as i64, }); } @@ -3976,9 +3977,10 @@ impl RpcMethod<3> for EthTraceCall { .map_err(|e| anyhow::anyhow!("failed to apply message: {e}"))?; let post_state = StateTree::new_from_root(ctx.store_owned(), &post_state_root)?; - let mut trace_results = EthTraceResults::default(); - - trace_results.output = get_trace_output(&msg, &invoke_result); + let mut trace_results = EthTraceResults { + output: get_trace_output(&msg, &invoke_result), + ..Default::default() + }; // Extract touched addresses for state diff (do this before consuming exec_trace) let touched_addresses = invoke_result @@ -3988,13 +3990,13 @@ impl RpcMethod<3> for EthTraceCall { .unwrap_or_default(); // Build call traces if requested - if trace_types.contains(&EthTraceType::Trace) { - if let Some(exec_trace) = invoke_result.execution_trace { - let mut env = trace::base_environment(&post_state, &msg.from()) - .map_err(|e| anyhow::anyhow!("failed to create trace environment: {e}"))?; - trace::build_traces(&mut env, &[], exec_trace)?; - trace_results.trace = env.traces; - } + if trace_types.contains(&EthTraceType::Trace) + && let Some(exec_trace) = invoke_result.execution_trace + { + let mut env = trace::base_environment(&post_state, &msg.from()) + .map_err(|e| anyhow::anyhow!("failed to create trace environment: {e}"))?; + trace::build_traces(&mut env, &[], exec_trace)?; + trace_results.trace = env.traces; } // Build state diff if requested @@ -4033,10 +4035,6 @@ fn get_trace_output(msg: &Message, invoke_result: &ApiInvocResult) -> Option = - LazyLock::new(|| env_or_default("FOREST_TRACE_STATE_DIFF_MAX_ADDRESSES", 1000)); - /// Extract all unique Ethereum addresses touched during execution from the trace. fn extract_touched_eth_addresses(trace: &crate::rpc::state::ExecutionTrace) -> HashSet { let mut addresses = HashSet::default(); @@ -4206,7 +4204,7 @@ where output: get_output(), state_diff: None, trace: env.traces.clone(), - transaction_hash: tx_hash.clone(), + transaction_hash: tx_hash, vm_trace: None, }); }; diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index a7a6eaac6be3..961333d0fb45 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -445,7 +445,7 @@ impl EthFilterSpec { if self.from_block.is_some() || self.to_block.is_some() { bail!("must not specify block hash and from/to block"); } - ParsedFilterTipsets::Hash(block_hash.clone()) + ParsedFilterTipsets::Hash(*block_hash) } else { let from_block = self.from_block.as_deref().unwrap_or(""); let to_block = self.to_block.as_deref().unwrap_or(""); @@ -1247,7 +1247,7 @@ mod tests { // Matching the given address 0 let spec0 = EthFilterSpec { - address: Some(vec![eth_addr0.clone()].into()), + address: Some(vec![eth_addr0].into()), ..Default::default() }; @@ -1257,7 +1257,7 @@ mod tests { // Matching the given address 0 or 1 let spec1 = EthFilterSpec { - address: Some(vec![eth_addr0.clone(), eth_addr1.clone()].into()), + address: Some(vec![eth_addr0, eth_addr1].into()), ..Default::default() }; @@ -1335,45 +1335,35 @@ mod tests { assert!(spec2.matches(&addr0, &entries0).unwrap()); let spec2 = EthFilterSpec { - topics: Some(EthTopicSpec(vec![EthHashList::Single(Some( - topic0.clone(), - ))])), + topics: Some(EthTopicSpec(vec![EthHashList::Single(Some(topic0))])), ..Default::default() }; assert!(spec2.matches(&addr0, &entries0).unwrap()); let spec3 = EthFilterSpec { - topics: Some(EthTopicSpec(vec![EthHashList::List(vec![topic0.clone()])])), + topics: Some(EthTopicSpec(vec![EthHashList::List(vec![topic0])])), ..Default::default() }; assert!(spec3.matches(&addr0, &entries0).unwrap()); let spec4 = EthFilterSpec { - topics: Some(EthTopicSpec(vec![EthHashList::List(vec![ - topic1.clone(), - topic0.clone(), - ])])), + topics: Some(EthTopicSpec(vec![EthHashList::List(vec![topic1, topic0])])), ..Default::default() }; assert!(spec4.matches(&addr0, &entries0).unwrap()); let spec5 = EthFilterSpec { - topics: Some(EthTopicSpec(vec![EthHashList::Single(Some( - topic1.clone(), - ))])), + topics: Some(EthTopicSpec(vec![EthHashList::Single(Some(topic1))])), ..Default::default() }; assert!(!spec5.matches(&addr0, &entries0).unwrap()); let spec6 = EthFilterSpec { - topics: Some(EthTopicSpec(vec![EthHashList::List(vec![ - topic2.clone(), - topic3.clone(), - ])])), + topics: Some(EthTopicSpec(vec![EthHashList::List(vec![topic2, topic3])])), ..Default::default() }; @@ -1381,8 +1371,8 @@ mod tests { let spec7 = EthFilterSpec { topics: Some(EthTopicSpec(vec![ - EthHashList::Single(Some(topic1.clone())), - EthHashList::Single(Some(topic1.clone())), + EthHashList::Single(Some(topic1)), + EthHashList::Single(Some(topic1)), ])), ..Default::default() }; @@ -1391,9 +1381,9 @@ mod tests { let spec8 = EthFilterSpec { topics: Some(EthTopicSpec(vec![ - EthHashList::Single(Some(topic0.clone())), - EthHashList::Single(Some(topic1.clone())), - EthHashList::Single(Some(topic3.clone())), + EthHashList::Single(Some(topic0)), + EthHashList::Single(Some(topic1)), + EthHashList::Single(Some(topic3)), ])), ..Default::default() }; diff --git a/src/rpc/methods/eth/trace.rs b/src/rpc/methods/eth/trace.rs index 83a0e1525123..936f4fe3ee70 100644 --- a/src/rpc/methods/eth/trace.rs +++ b/src/rpc/methods/eth/trace.rs @@ -11,7 +11,7 @@ use super::{ }; use crate::eth::{EAMMethod, EVMMethod}; use crate::rpc::eth::types::{AccountDiff, Delta, StateDiff}; -use crate::rpc::eth::{EthBigInt, EthUint64, MAX_STATE_DIFF_ADDRESSES}; +use crate::rpc::eth::{EthBigInt, EthUint64}; use crate::rpc::methods::eth::lookup_eth_address; use crate::rpc::methods::state::ExecutionTrace; use crate::rpc::state::ActorTrace; @@ -302,7 +302,7 @@ fn trace_call( r#type: "call".into(), action: TraceAction::Call(EthCallTraceAction { call_type, - from: env.caller.clone(), + from: env.caller, to: Some(to), gas: trace.msg.gas_limit.unwrap_or_default().into(), value: trace.msg.value.clone().into(), @@ -434,7 +434,7 @@ fn trace_native_create( Some(EthTrace { r#type: "create".into(), action: TraceAction::Create(EthCreateTraceAction { - from: env.caller.clone(), + from: env.caller, gas: trace.msg.gas_limit.unwrap_or_default().into(), value: trace.msg.value.clone().into(), // If we get here, this isn't a native EVM create. Those always go through @@ -544,7 +544,7 @@ fn trace_eth_create( Some(EthTrace { r#type: "create".into(), action: TraceAction::Create(EthCreateTraceAction { - from: env.caller.clone(), + from: env.caller, gas: trace.msg.gas_limit.unwrap_or_default().into(), value: trace.msg.value.clone().into(), init: init_code.into(), @@ -634,8 +634,8 @@ fn trace_evm_private( r#type: "call".into(), action: TraceAction::Call(EthCallTraceAction { call_type: "delegatecall".into(), - from: env.caller.clone(), - to: env.last_byte_code.clone(), + from: env.caller, + to: env.last_byte_code, gas: trace.msg.gas_limit.unwrap_or_default().into(), value: trace.msg.value.clone().into(), input: dp.input.into(), @@ -668,13 +668,7 @@ pub(crate) fn build_state_diff( ) -> anyhow::Result { let mut state_diff = StateDiff::new(); - // Limit the number of addresses for safety - let addresses: Vec<_> = touched_addresses - .iter() - .take(*MAX_STATE_DIFF_ADDRESSES) - .collect(); - - for eth_addr in addresses { + for eth_addr in touched_addresses { let fil_addr = eth_addr.to_filecoin_address()?; // Get actor state before and after @@ -913,10 +907,10 @@ mod tests { fn get_evm_actor_code_cid() -> Option { for bundle in ACTOR_BUNDLES_METADATA.values() { - if bundle.actor_major_version().ok() == Some(17) { - if let Ok(cid) = bundle.manifest.get(BuiltinActor::EVM) { - return Some(cid); - } + if bundle.actor_major_version().ok() == Some(17) + && let Ok(cid) = bundle.manifest.get(BuiltinActor::EVM) + { + return Some(cid); } } None diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index 3a332feed316..6b6781bf4593 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -131,6 +131,7 @@ impl GetSize for EthAddress { } impl EthAddress { + #[allow(clippy::wrong_self_convention)] pub fn to_filecoin_address(&self) -> anyhow::Result { if self.is_masked_id() { const PREFIX_LEN: usize = MASKED_ID_PREFIX.len(); @@ -722,16 +723,15 @@ impl EthTrace { to_decoded_addresses: &Option, ) -> Result { let (trace_to, trace_from) = match &self.action { - TraceAction::Call(action) => (action.to.clone(), action.from.clone()), + TraceAction::Call(action) => (action.to, action.from), TraceAction::Create(action) => { let address = match &self.result { TraceResult::Create(result) => result .address - .clone() .ok_or_else(|| anyhow::anyhow!("address is nil in create trace result"))?, _ => bail!("invalid create trace result"), }; - (Some(address), action.from.clone()) + (Some(address), action.from) } }; @@ -766,10 +766,11 @@ pub struct ChangedType { /// Represents how a value changed during transaction execution. // Taken from https://github.com/alloy-rs/alloy/blob/v1.5.2/crates/rpc-types-trace/src/parity.rs#L84 -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub enum Delta { /// Existing value didn't change. #[serde(rename = "=")] + #[default] Unchanged, /// A new value was added (account/storage created). #[serde(rename = "+")] @@ -782,12 +783,6 @@ pub enum Delta { Changed(ChangedType), } -impl Default for Delta { - fn default() -> Self { - Delta::Unchanged - } -} - impl Delta { pub fn from_comparison(old: Option, new: Option) -> Self { match (old, new) { diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 4741b144ac1c..ab8c359e6faf 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -1443,7 +1443,7 @@ fn eth_tests() -> Vec { for (to, data) in cases { let msg = EthCallMessage { - to: to.clone(), + to, data: data.clone(), ..EthCallMessage::default() }; @@ -1667,14 +1667,14 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset RpcTest::identity( EthGetBalance::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), - BlockNumberOrHash::from_block_hash_object(block_hash.clone(), false), + BlockNumberOrHash::from_block_hash_object(block_hash, false), )) .unwrap(), ), RpcTest::identity( EthGetBalance::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), - BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + BlockNumberOrHash::from_block_hash_object(block_hash, true), )) .unwrap(), ), @@ -1724,14 +1724,14 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset RpcTest::identity( EthGetBalanceV2::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), - ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), false), + ExtBlockNumberOrHash::from_block_hash_object(block_hash, false), )) .unwrap(), ), RpcTest::identity( EthGetBalanceV2::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), - ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + ExtBlockNumberOrHash::from_block_hash_object(block_hash, true), )) .unwrap(), ), @@ -1873,8 +1873,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset ), RpcTest::identity( EthGetBlockReceipts::request((BlockNumberOrHash::from_block_hash_object( - block_hash.clone(), - true, + block_hash, true, ),)) .unwrap(), ), @@ -1887,8 +1886,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset ), RpcTest::identity( EthGetBlockReceiptsV2::request((ExtBlockNumberOrHash::from_block_hash_object( - block_hash.clone(), - true, + block_hash, true, ),)) .unwrap(), ), @@ -1905,11 +1903,11 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset .unwrap(), ), RpcTest::identity( - EthGetBlockTransactionCountByHash::request((block_hash.clone(),)).unwrap(), + EthGetBlockTransactionCountByHash::request((block_hash,)).unwrap(), ), RpcTest::identity( EthGetBlockReceiptsLimited::request(( - BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + BlockNumberOrHash::from_block_hash_object(block_hash, true), 4, )) .unwrap(), @@ -1917,14 +1915,14 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), RpcTest::identity( EthGetBlockReceiptsLimited::request(( - BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + BlockNumberOrHash::from_block_hash_object(block_hash, true), -1, )) .unwrap(), ), RpcTest::identity( EthGetBlockReceiptsLimitedV2::request(( - ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + ExtBlockNumberOrHash::from_block_hash_object(block_hash, true), 4, )) .unwrap(), @@ -1932,7 +1930,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), RpcTest::identity( EthGetBlockReceiptsLimitedV2::request(( - ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + ExtBlockNumberOrHash::from_block_hash_object(block_hash, true), -1, )) .unwrap(), @@ -1962,7 +1960,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset RpcTest::identity( EthGetTransactionCount::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), - BlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + BlockNumberOrHash::from_block_hash_object(block_hash, true), )) .unwrap(), ), @@ -1991,7 +1989,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset RpcTest::identity( EthGetTransactionCountV2::request(( EthAddress::from_str("0xff000000000000000000000000000000000003ec").unwrap(), - ExtBlockNumberOrHash::from_block_hash_object(block_hash.clone(), true), + ExtBlockNumberOrHash::from_block_hash_object(block_hash, true), )) .unwrap(), ), @@ -2323,11 +2321,11 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset ) .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError), RpcTest::identity( - EthGetTransactionByBlockHashAndIndex::request((block_hash.clone(), 0.into())).unwrap(), + EthGetTransactionByBlockHashAndIndex::request((block_hash, 0.into())).unwrap(), ) .policy_on_rejected(PolicyOnRejected::PassWithIdenticalError), - RpcTest::identity(EthGetBlockByHash::request((block_hash.clone(), false)).unwrap()), - RpcTest::identity(EthGetBlockByHash::request((block_hash.clone(), true)).unwrap()), + RpcTest::identity(EthGetBlockByHash::request((block_hash, false)).unwrap()), + RpcTest::identity(EthGetBlockByHash::request((block_hash, true)).unwrap()), RpcTest::identity( EthGetLogs::request((EthFilterSpec { from_block: Some(format!("0x{:x}", shared_tipset.epoch())), @@ -2500,7 +2498,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset tests.extend([RpcTest::identity( EthEstimateGas::request(( EthCallMessage { - to: Some(eth_to_addr.clone()), + to: Some(eth_to_addr), value: Some(msg.value.clone().into()), data: Some(msg.params.clone().into()), ..Default::default() @@ -2611,13 +2609,13 @@ fn eth_state_tests_with_tipset( for smsg in sample_signed_messages(bls_messages.iter(), secp_messages.iter()) { let tx = new_eth_tx_from_signed_message(&smsg, &state, eth_chain_id)?; tests.push(RpcTest::identity( - EthGetMessageCidByTransactionHash::request((tx.hash.clone(),))?, + EthGetMessageCidByTransactionHash::request((tx.hash,))?, )); - tests.push(RpcTest::identity(EthGetTransactionByHash::request((tx - .hash - .clone(),))?)); + tests.push(RpcTest::identity(EthGetTransactionByHash::request(( + tx.hash, + ))?)); tests.push(RpcTest::identity(EthGetTransactionByHashLimited::request( - (tx.hash.clone(), shared_tipset.epoch()), + (tx.hash, shared_tipset.epoch()), )?)); tests.push(RpcTest::identity( EthTraceTransaction::request((tx.hash.to_string(),)).unwrap(), @@ -2626,7 +2624,7 @@ fn eth_state_tests_with_tipset( && smsg.message.to.protocol() == Protocol::Delegated { tests.push( - RpcTest::identity(EthGetTransactionReceipt::request((tx.hash.clone(),))?) + RpcTest::identity(EthGetTransactionReceipt::request((tx.hash,))?) .policy_on_rejected(PolicyOnRejected::PassWithQuasiIdenticalError), ); tests.push( diff --git a/src/tool/subcommands/api_cmd/stateful_tests.rs b/src/tool/subcommands/api_cmd/stateful_tests.rs index 0d32bed136ce..3ea5d93c59ad 100644 --- a/src/tool/subcommands/api_cmd/stateful_tests.rs +++ b/src/tool/subcommands/api_cmd/stateful_tests.rs @@ -439,7 +439,7 @@ fn eth_new_block_filter() -> RpcTestScenario { let verify_hashes = async |hashes: &[EthHash]| -> anyhow::Result<()> { for hash in hashes { let _block = client - .call(EthGetBlockByHash::request((hash.clone(), false))?) + .call(EthGetBlockByHash::request((*hash, false))?) .await?; } Ok(()) @@ -529,7 +529,7 @@ fn eth_new_pending_transaction_filter(tx: TestTransaction) -> RpcTestScenario { let mut cids = vec![]; for hash in &hashes { if let Some(cid) = client - .call(EthGetMessageCidByTransactionHash::request((hash.clone(),))?) + .call(EthGetMessageCidByTransactionHash::request((*hash,))?) .await? { cids.push(cid); From 770f3590961742904d0387424b7716240560368e Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Tue, 3 Feb 2026 20:05:32 +0530 Subject: [PATCH 09/19] remove the unused script and refactor the trace test script --- .../tests/api_compare/gen_trace_call_refs.sh | 115 ------------------ scripts/tests/trace_call_integration_test.sh | 15 +-- 2 files changed, 3 insertions(+), 127 deletions(-) delete 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 deleted file mode 100644 index 8e41b2505731..000000000000 --- a/scripts/tests/api_compare/gen_trace_call_refs.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/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():" -) - -# 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" - - 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, gas: "0x13880" }, - ["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, gas: "0x13880" }, - ["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." diff --git a/scripts/tests/trace_call_integration_test.sh b/scripts/tests/trace_call_integration_test.sh index 6038673295fb..2a15fa671368 100755 --- a/scripts/tests/trace_call_integration_test.sh +++ b/scripts/tests/trace_call_integration_test.sh @@ -2,12 +2,10 @@ set -e DEPLOY_CONTRACT=false -VERBOSE=false while [[ $# -gt 0 ]]; do case $1 in - --deploy) DEPLOY_CONTRACT=true; shift ;; - --verbose) VERBOSE=true; shift ;; - *) echo "Usage: $0 [--deploy] [--verbose]"; exit 1 ;; + --deploy) DEPLOY_CONTRACT=true; shift ;; + *) echo "Usage: $0 [--deploy]"; exit 1 ;; esac done @@ -18,6 +16,7 @@ FOREST_ACCOUNT="${FOREST_ACCOUNT:-0xb7aa1e9c847cda5f60f1ae6f65c3eae44848d41f}" FOREST_CONTRACT="${FOREST_CONTRACT:-0x73a43475aa2ccb14246613708b399f4b2ba546c7}" ANVIL_ACCOUNT="${ANVIL_ACCOUNT:-0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266}" ANVIL_CONTRACT="${ANVIL_CONTRACT:-0x5FbDB2315678afecb367f032d93F642f64180aa3}" +# -- This private key is of anvil dev node -- ANVIL_PRIVATE_KEY="${ANVIL_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" GREEN='\033[0;32m' RED='\033[0;31m' BLUE='\033[0;34m' YELLOW='\033[0;33m' NC='\033[0m' @@ -114,8 +113,6 @@ test_trace() { local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"callTracer\"}]" local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") - [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" - local f_input=$(echo "$f_resp" | jq -r '.result.trace[0].action.input') local a_input=$(echo "$a_resp" | jq -r '.result.input') assert_eq "Input" "$f_input" "$a_input" @@ -153,8 +150,6 @@ test_balance_diff() { local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") - [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" - local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') @@ -181,8 +176,6 @@ test_storage_diff() { local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") - [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" - local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') @@ -228,8 +221,6 @@ test_storage_multiple() { local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") - [[ "$VERBOSE" = true ]] && echo -e "${YELLOW}Forest:${NC} $f_resp\n${YELLOW}Anvil:${NC} $a_resp" - local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') local a_contract_lower=$(echo "$ANVIL_CONTRACT" | tr '[:upper:]' '[:lower:]') From 9f68a523d3ba388bf7276344f4d8f785f5d4cd89 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 5 Feb 2026 20:10:52 +0530 Subject: [PATCH 10/19] add trace call api testing guide --- .../developers/guides/trace_call_guide.md | 523 ++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 docs/docs/developers/guides/trace_call_guide.md diff --git a/docs/docs/developers/guides/trace_call_guide.md b/docs/docs/developers/guides/trace_call_guide.md new file mode 100644 index 000000000000..4588179abcbd --- /dev/null +++ b/docs/docs/developers/guides/trace_call_guide.md @@ -0,0 +1,523 @@ +# trace_call API Guide + +This guide explains the `trace_call` RPC method implemented in Forest, which follows the **[Parity/OpenEthereum](https://openethereum.github.io/JSONRPC-trace-module#trace_call) and [reth](https://reth.rs/jsonrpc/trace#trace-format-specification) trace format**. + +## Overview + +`trace_call` executes an EVM call and returns detailed execution traces without creating a transaction on the blockchain. It's useful for: + +- Debugging smart contract calls +- Analyzing gas usage and call patterns +- Inspecting state changes before execution +- Understanding nested call hierarchies + +## Request Format + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0x...", // Sender address + "to": "0x...", // Contract address + "data": "0x...", // Encoded function call + "value": "0x0", // ETH value (optional) + "gas": "0x...", // Gas limit (optional) + "gasPrice": "0x..." // Gas price (optional) + }, + ["trace", "stateDiff"], // Trace types to return + "latest" // Block number or tag + ] +} +``` + +### Trace Types + +| Type | Description | +| ----------- | ------------------------------------------------------------------- | +| `trace` | Call hierarchy with inputs, outputs, gas used | +| `stateDiff` | State changes (balance, nonce, code, storage) | +| `vmTrace` | Low-level EVM execution (not yet implemented, not supproted by FVM) | + +## Response Format (Parity Style) + +Forest uses the **Parity/OpenEthereum trace format**, which differs from Geth's debug API. + +### Trace Response + +```json +{ + "result": { + "output": "0x...", + "trace": [ + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0x...", + "to": "0x...", + "gas": "0x...", + "value": "0x0", + "input": "0x..." + }, + "result": { + "gasUsed": "0x...", + "output": "0x..." + }, + "error": null + } + ], + "stateDiff": { ... } + } +} +``` + +### StateDiff Response + +State changes use **Delta notation**: + +| Symbol | Meaning | Example | +| ------ | --------- | -------------------------------------------------------- | +| `"="` | Unchanged | `"balance": "="` | +| `"+"` | Added | `"balance": { "+": "0x1000" }` | +| `"-"` | Removed | `"balance": { "-": "0x1000" }` | +| `"*"` | Changed | `"balance": { "*": { "from": "0x100", "to": "0x200" } }` | + +```json +{ + "stateDiff": { + "0xcontract...": { + "balance": "=", + "code": "=", + "nonce": "=", + "storage": { + "0x0000...0000": { + "*": { + "from": "0x000...02a", + "to": "0x000...07b" + } + } + } + }, + "0xsender...": { + "balance": "=", + "code": "=", + "nonce": { + "*": { + "from": "0x5", + "to": "0x6" + } + }, + "storage": {} + } + } +} +``` + +## Parity vs Geth Format Comparison + +| Aspect | Forest (Parity) | Geth | +| -------------------- | ------------------------------------ | --------------------------------------- | +| **API Method** | `trace_call` | `debug_traceCall` | +| **State Format** | Delta notation (`"*"`, `"+"`, `"-"`) | Separate `pre`/`post` objects | +| **Unchanged Values** | Shows `"="` | Included in `pre`, absent in `post` | +| **Storage Changes** | `{ "*": { from, to } }` | Compare `pre.storage` vs `post.storage` | +| **Code Field** | `"="` if unchanged | Full bytecode in `pre` | + +### Example: Same call, different formats + +**Forest (Parity):** + +```json +{ + "storage": { + "0x00...00": { + "*": { + "from": "0x00...2a", + "to": "0x00...7b" + } + } + } +} +``` + +**Geth (prestateTracer with diffMode):** + +```json +{ + "pre": { + "storage": { "0x00...00": "0x00...2a" } + }, + "post": { + "storage": { "0x00...00": "0x00...7b" } + } +} +``` + +## Testing with Tracer Contract + +The `Tracer.sol` contract provides various functions to test different tracing scenarios. + +### Contract Location + +``` +src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol +``` + +### Prerequisites + +- [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (`forge`, `cast` commands) +- A running Forest node or Anvil for local testing or both node for comparison + +--- + +### Option 1: Testing with Forest Node + +#### Starting Forest Node + +**Calibration Network (Testnet - Recommended for testing):** + +```bash +forest --chain calibnet --auto-download-snapshot --encrypt-keystore false +``` + +**Mainnet:** + +```bash +forest --chain mainnet --auto-download-snapshot --encrypt-keystore false +``` + +Forest RPC endpoint: `http://localhost:2345/rpc/v1` + +#### Deploying Contract on Forest + +To deploy the contract on Calibnet or Mainnet, you need: + +1. A funded wallet with FIL tokens for gas +2. Convert your Filecoin address to an Ethereum-style address (f4/0x format) + +```bash +# Deploy using forge (requires funded account) +forge create src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol:Tracer \ + --rpc-url http://localhost:2345/rpc/v1 \ + --boradcast \ + --private-key +``` + +#### Existing Deployed Contracts + +If you don't want to deploy your own contract, you can use these pre-deployed addresses: + +| Network | Contract Address | Notes | +| -------- | -------------------------------------------- | ------------------------------------------- | +| Calibnet | `0x73a43475aa2ccb14246613708b399f4b2ba546c7` | Full Tracer.sol with storage diff functions | + +> **Note:** Contract availability depends on network state. Verify the contract exists before testing: +> +> ```bash +> curl -s -X POST "http://localhost:2345/rpc/v1" \ +> -H "Content-Type: application/json" \ +> -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0x73a43475aa2ccb14246613708b399f4b2ba546c7","latest"]}' \ +> | jq -r '.result | length' +> ``` +> +> If the result is `> 2`, the contract is deployed. + +--- + +### Option 2: Testing with Anvil (Local Development) + +#### What is Anvil? + +[Anvil](https://getfoundry.sh/anvil/reference/) is a local Ethereum development node included with Foundry. It provides: + +- Instant block mining +- Pre-funded test accounts (10 accounts with 10,000 ETH each) +- Support for `debug_traceCall` with various tracers +- No real tokens required + +Anvil uses **Geth-style** tracing (`debug_traceCall` with `prestateTracer`), while Forest uses **Parity-style** tracing (`trace_call` with `stateDiff`). This makes Anvil useful for comparison testing. + +#### Starting Anvil + +```bash +# Start Anvil with tracer to allow debug_traceCall API's +anvil --tracing +``` + +Anvil RPC endpoint: `http://localhost:8545` + +#### Deploying Contract on Anvil + +```bash +# Use the first pre-funded account's private key +forge create src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol:Tracer \ + --rpc-url http://localhost:8545 \ + --broadcast \ + --private-key + +# Output: +# Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +# Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 +# Transaction hash: 0x... +``` + +#### Comparing Forest vs Anvil Responses + +**Forest (Parity-style `trace_call`):** + +```bash +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + {"from": "0x...", "to": "0x...", "data": "0x4018d9aa..."}, + ["stateDiff"], + "latest" + ] + }' +``` + +**Anvil (Geth-style `debug_traceCall`):** + +```bash +curl -s -X POST "http://localhost:8545" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_traceCall", + "params": [ + {"from": "0x...", "to": "0x...", "data": "0x4018d9aa..."}, + "latest", + {"tracer": "prestateTracer", "tracerConfig": {"diffMode": true}} + ] + }' +``` + +--- + +### Storage Layout + +| Slot | Variable | Description | +| ---- | -------------- | ---------------------------- | +| 0 | `x` | Initialized to 42 | +| 1 | `balances` | Mapping base slot | +| 2 | `storageTestA` | Starts empty (for add tests) | +| 3 | `storageTestB` | Starts empty | +| 4 | `storageTestC` | Starts empty | +| 5 | `dynamicArray` | Array length slot | + +### Function Reference + +#### Basic Operations + +| Function | Selector | Description | +| ------------------- | ------------ | --------------------------- | +| `setX(uint256)` | `0x4018d9aa` | Write to slot 0 | +| `deposit()` | `0xd0e30db0` | Receive ETH, update mapping | +| `withdraw(uint256)` | `0x2e1a7d4d` | Send ETH from contract | +| `doRevert()` | `0xafc874d2` | Always reverts | + +#### Call Tracing + +| Function | Selector | Description | +| ----------------------- | ------------ | ---------------------- | +| `callSelf(uint256)` | `0xa1a88595` | Single nested CALL | +| `delegateSelf(uint256)` | `0x8f5e07b8` | DELEGATECALL trace | +| `complexTrace()` | `0x6659ab96` | Multiple nested calls | +| `deepTrace(uint256)` | `0x0f3a17b8` | Recursive N-level deep | + +#### Storage Diff Testing + +| Function | Selector | Description | +| ------------------------------------------ | ------------ | -------------------- | +| `storageAdd(uint256)` | `0x55cb64b4` | Add to empty slot 2 | +| `storageChange(uint256)` | `0x7c8f6e57` | Modify existing slot | +| `storageDelete()` | `0xd92846a3` | Set slot to zero | +| `storageMultiple(uint256,uint256,uint256)` | `0x310af204` | Change slots 2,3,4 | + +## Example curl Requests for Forest node. + +> **Note:**: Anvil has a different params format check above, request data is same as Forest. + +### 1. Basic Trace - setX(123) + +```bash +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0xYOUR_ACCOUNT", + "to": "0xCONTRACT_ADDRESS", + "data": "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" + }, + ["trace"], + "latest" + ] + }' | jq '.' +``` + +### 2. State Diff - Storage Change + +```bash +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0xYOUR_ACCOUNT", + "to": "0xCONTRACT_ADDRESS", + "data": "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" + }, + ["stateDiff"], + "latest" + ] + }' | jq '.result.stateDiff' +``` + +### 3. Balance Change - deposit() with ETH + +```bash +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0xYOUR_ACCOUNT", + "to": "0xCONTRACT_ADDRESS", + "data": "0xd0e30db0", + "value": "0xde0b6b3a7640000" + }, + ["trace", "stateDiff"], + "latest" + ] + }' | jq '.' +``` + +### 4. Deep Trace - deepTrace(3) + +```bash +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0xYOUR_ACCOUNT", + "to": "0xCONTRACT_ADDRESS", + "data": "0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" + }, + ["trace"], + "latest" + ] + }' | jq '.result.trace | length' +``` + +### 5. Multiple Storage Slots - storageMultiple(10,20,30) + +```bash +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0xYOUR_ACCOUNT", + "to": "0xCONTRACT_ADDRESS", + "data": "0x310af204000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e" + }, + ["stateDiff"], + "latest" + ] + }' | jq '.result.stateDiff' +``` + +## Integration Test Script + +An automated test script is available to compare Forest's `trace_call` with Anvil's `debug_traceCall`: + +```bash +# Run the test (requires Forest and Anvil running) +./scripts/tests/trace_call_integration_test.sh + +or + +# Deploy contract on Anvil first, forest and anvil node should already be running +./scripts/tests/trace_call_integration_test.sh --deploy +``` + +### Test Categories + +1. **Trace Tests**: Call hierarchy, subcalls, reverts, deep traces +2. **Balance Diff Tests**: ETH transfers, deposits +3. **Storage Diff Tests**: Single slot, multiple slots, value comparison + +## Generating Function Selectors + +Use `cast` from Foundry to generate function selectors: + +```bash +# Get selector for a function +cast sig "setX(uint256)" +# Output: 0x4018d9aa + +# Encode full calldata +cast calldata "setX(uint256)" 123 +# Output: 0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b +``` + +## Troubleshooting + +### Common Issues + +1. **Empty storage in stateDiff**: Ensure the contract is an EVM actor (has bytecode) +2. **Call reverts**: Check function requirements (e.g., `storageChange` requires slot to have value first) +3. **Missing contract**: Verify contract is deployed at the specified address + +### Debug Tips + +```bash +# Check if address has code +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xCONTRACT","latest"]}' \ + | jq -r '.result | length' + +# Check account balance +curl -s -X POST "http://localhost:2345/rpc/v1" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xACCOUNT","latest"]}' \ + | jq '.result' +``` + +## Offical Resources + +| Resource | Description | +| -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| [OpenEthereum trace module](https://openethereum.github.io/JSONRPC-trace-module) | Official Parity/OpenEthereum documentation for `trace_call` and `stateDiff` format | +| [Geth Built-in Tracers](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers) | Geth documentation for `prestateTracer` and `callTracer` | +| [Alchemy: trace_call vs debug_traceCall](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) | Detailed comparison of both tracing methods | +| [Reth trace Namespace](https://reth.rs/jsonrpc/trace) | Reth's implementation of the trace API (follows Parity format) | +| [Foundry Book - Anvil](https://book.getfoundry.sh/reference/anvil/) | Anvil local development node documentation | From 0460a9e80fc4a2bc25c035fff6f226a95ff04713 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 13 Feb 2026 13:22:53 +0530 Subject: [PATCH 11/19] address comments about docs and scripts --- .../developers/guides/trace_call_guide.md | 477 +++--------------- docs/docs/users/knowledge_base/trace_call.md | 363 +++++++++++++ scripts/tests/trace_call_integration_test.sh | 65 ++- 3 files changed, 481 insertions(+), 424 deletions(-) create mode 100644 docs/docs/users/knowledge_base/trace_call.md diff --git a/docs/docs/developers/guides/trace_call_guide.md b/docs/docs/developers/guides/trace_call_guide.md index 4588179abcbd..25e1bbcf1a78 100644 --- a/docs/docs/developers/guides/trace_call_guide.md +++ b/docs/docs/developers/guides/trace_call_guide.md @@ -1,237 +1,84 @@ -# trace_call API Guide - -This guide explains the `trace_call` RPC method implemented in Forest, which follows the **[Parity/OpenEthereum](https://openethereum.github.io/JSONRPC-trace-module#trace_call) and [reth](https://reth.rs/jsonrpc/trace#trace-format-specification) trace format**. - -## Overview - -`trace_call` executes an EVM call and returns detailed execution traces without creating a transaction on the blockchain. It's useful for: - -- Debugging smart contract calls -- Analyzing gas usage and call patterns -- Inspecting state changes before execution -- Understanding nested call hierarchies - -## Request Format - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "trace_call", - "params": [ - { - "from": "0x...", // Sender address - "to": "0x...", // Contract address - "data": "0x...", // Encoded function call - "value": "0x0", // ETH value (optional) - "gas": "0x...", // Gas limit (optional) - "gasPrice": "0x..." // Gas price (optional) - }, - ["trace", "stateDiff"], // Trace types to return - "latest" // Block number or tag - ] -} -``` +# trace_call Developer Guide -### Trace Types - -| Type | Description | -| ----------- | ------------------------------------------------------------------- | -| `trace` | Call hierarchy with inputs, outputs, gas used | -| `stateDiff` | State changes (balance, nonce, code, storage) | -| `vmTrace` | Low-level EVM execution (not yet implemented, not supproted by FVM) | - -## Response Format (Parity Style) - -Forest uses the **Parity/OpenEthereum trace format**, which differs from Geth's debug API. - -### Trace Response - -```json -{ - "result": { - "output": "0x...", - "trace": [ - { - "type": "call", - "subtraces": 1, - "traceAddress": [], - "action": { - "callType": "call", - "from": "0x...", - "to": "0x...", - "gas": "0x...", - "value": "0x0", - "input": "0x..." - }, - "result": { - "gasUsed": "0x...", - "output": "0x..." - }, - "error": null - } - ], - "stateDiff": { ... } - } -} -``` +This guide covers testing and development workflows for Forest's `trace_call` implementation. For API documentation and user-facing usage, see the [trace_call API guide](/docs/users/knowledge_base/trace_call). -### StateDiff Response - -State changes use **Delta notation**: - -| Symbol | Meaning | Example | -| ------ | --------- | -------------------------------------------------------- | -| `"="` | Unchanged | `"balance": "="` | -| `"+"` | Added | `"balance": { "+": "0x1000" }` | -| `"-"` | Removed | `"balance": { "-": "0x1000" }` | -| `"*"` | Changed | `"balance": { "*": { "from": "0x100", "to": "0x200" } }` | - -```json -{ - "stateDiff": { - "0xcontract...": { - "balance": "=", - "code": "=", - "nonce": "=", - "storage": { - "0x0000...0000": { - "*": { - "from": "0x000...02a", - "to": "0x000...07b" - } - } - } - }, - "0xsender...": { - "balance": "=", - "code": "=", - "nonce": { - "*": { - "from": "0x5", - "to": "0x6" - } - }, - "storage": {} - } - } -} -``` +## Tracer Contract -## Parity vs Geth Format Comparison - -| Aspect | Forest (Parity) | Geth | -| -------------------- | ------------------------------------ | --------------------------------------- | -| **API Method** | `trace_call` | `debug_traceCall` | -| **State Format** | Delta notation (`"*"`, `"+"`, `"-"`) | Separate `pre`/`post` objects | -| **Unchanged Values** | Shows `"="` | Included in `pre`, absent in `post` | -| **Storage Changes** | `{ "*": { from, to } }` | Compare `pre.storage` vs `post.storage` | -| **Code Field** | `"="` if unchanged | Full bytecode in `pre` | - -### Example: Same call, different formats - -**Forest (Parity):** - -```json -{ - "storage": { - "0x00...00": { - "*": { - "from": "0x00...2a", - "to": "0x00...7b" - } - } - } -} -``` +The [`Tracer.sol`](https://github.com/ChainSafe/forest/blob/963237708137e9c7388c57eba39a2f8bf12ace74/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol) contract provides various functions to test different tracing scenarios. -**Geth (prestateTracer with diffMode):** - -```json -{ - "pre": { - "storage": { "0x00...00": "0x00...2a" } - }, - "post": { - "storage": { "0x00...00": "0x00...7b" } - } -} -``` +### Storage Layout -## Testing with Tracer Contract +| Slot | Variable | Description | +| ---- | -------------- | ---------------------------- | +| 0 | `x` | Initialized to 42 | +| 1 | `balances` | Mapping base slot | +| 2 | `storageTestA` | Starts empty (for add tests) | +| 3 | `storageTestB` | Starts empty | +| 4 | `storageTestC` | Starts empty | +| 5 | `dynamicArray` | Array length slot | -The `Tracer.sol` contract provides various functions to test different tracing scenarios. +### Function Reference -### Contract Location +#### Basic Operations -``` -src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol -``` +| Function | Selector | Description | +| ------------------- | ------------ | --------------------------- | +| `setX(uint256)` | `0x4018d9aa` | Write to slot 0 | +| `deposit()` | `0xd0e30db0` | Receive ETH, update mapping | +| `withdraw(uint256)` | `0x2e1a7d4d` | Send ETH from contract | +| `doRevert()` | `0xafc874d2` | Always reverts | -### Prerequisites +#### Call Tracing -- [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (`forge`, `cast` commands) -- A running Forest node or Anvil for local testing or both node for comparison +| Function | Selector | Description | +| ----------------------- | ------------ | ---------------------- | +| `callSelf(uint256)` | `0xa1a88595` | Single nested CALL | +| `delegateSelf(uint256)` | `0x8f5e07b8` | DELEGATECALL trace | +| `complexTrace()` | `0x6659ab96` | Multiple nested calls | +| `deepTrace(uint256)` | `0x0f3a17b8` | Recursive N-level deep | ---- +#### Storage Diff Testing -### Option 1: Testing with Forest Node +| Function | Selector | Description | +| ------------------------------------------ | ------------ | -------------------- | +| `storageAdd(uint256)` | `0x55cb64b4` | Add to empty slot 2 | +| `storageChange(uint256)` | `0x7c8f6e57` | Modify existing slot | +| `storageDelete()` | `0xd92846a3` | Set slot to zero | +| `storageMultiple(uint256,uint256,uint256)` | `0x310af204` | Change slots 2,3,4 | -#### Starting Forest Node +### Generating Function Selectors -**Calibration Network (Testnet - Recommended for testing):** +Use `cast` from Foundry to generate function selectors: ```bash -forest --chain calibnet --auto-download-snapshot --encrypt-keystore false -``` - -**Mainnet:** +# Get selector for a function +cast sig "setX(uint256)" +# Output: 0x4018d9aa -```bash -forest --chain mainnet --auto-download-snapshot --encrypt-keystore false +# Encode full calldata +cast calldata "setX(uint256)" 123 +# Output: 0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b ``` -Forest RPC endpoint: `http://localhost:2345/rpc/v1` - -#### Deploying Contract on Forest - -To deploy the contract on Calibnet or Mainnet, you need: - -1. A funded wallet with FIL tokens for gas -2. Convert your Filecoin address to an Ethereum-style address (f4/0x format) +### Deployed Contracts -```bash -# Deploy using forge (requires funded account) -forge create src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol:Tracer \ - --rpc-url http://localhost:2345/rpc/v1 \ - --boradcast \ - --private-key -``` +Pre-deployed Tracer contracts for quick testing: -#### Existing Deployed Contracts +| Network | Contract Address | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Calibnet | [0x73a43475aa2ccb14246613708b399f4b2ba546c7](https://calibration.filfox.info/en/address/0x73a43475aa2ccb14246613708b399f4b2ba546c7) | +| Mainnet | [0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2](https://filecoin.blockscout.com/address/0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2) | -If you don't want to deploy your own contract, you can use these pre-deployed addresses: +## Comparison Testing with Anvil -| Network | Contract Address | Notes | -| -------- | -------------------------------------------- | ------------------------------------------- | -| Calibnet | `0x73a43475aa2ccb14246613708b399f4b2ba546c7` | Full Tracer.sol with storage diff functions | +Anvil uses **Geth-style** tracing (`debug_traceCall` with `prestateTracer`), while Forest uses **Parity-style** tracing (`trace_call` with `stateDiff`). This makes Anvil useful for comparison testing — verifying that Forest produces semantically equivalent results in a different format. -> **Note:** Contract availability depends on network state. Verify the contract exists before testing: -> -> ```bash -> curl -s -X POST "http://localhost:2345/rpc/v1" \ -> -H "Content-Type: application/json" \ -> -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0x73a43475aa2ccb14246613708b399f4b2ba546c7","latest"]}' \ -> | jq -r '.result | length' -> ``` -> -> If the result is `> 2`, the contract is deployed. - ---- +### Prerequisites -### Option 2: Testing with Anvil (Local Development) +- [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (`forge`, `cast` commands) +- A running [Forest node](https://docs.forest.chainsafe.io/getting_started/syncing) and Anvil instance -#### What is Anvil? +### What is Anvil? [Anvil](https://getfoundry.sh/anvil/reference/) is a local Ethereum development node included with Foundry. It provides: @@ -240,9 +87,7 @@ If you don't want to deploy your own contract, you can use these pre-deployed ad - Support for `debug_traceCall` with various tracers - No real tokens required -Anvil uses **Geth-style** tracing (`debug_traceCall` with `prestateTracer`), while Forest uses **Parity-style** tracing (`trace_call` with `stateDiff`). This makes Anvil useful for comparison testing. - -#### Starting Anvil +### Starting Anvil ```bash # Start Anvil with tracer to allow debug_traceCall API's @@ -251,7 +96,7 @@ anvil --tracing Anvil RPC endpoint: `http://localhost:8545` -#### Deploying Contract on Anvil +### Deploying Contract on Anvil ```bash # Use the first pre-funded account's private key @@ -266,7 +111,9 @@ forge create src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol:Tracer \ # Transaction hash: 0x... ``` -#### Comparing Forest vs Anvil Responses +### Comparing Forest vs Anvil Responses + +The same contract call can be tested against both nodes. The request data payloads are identical; only the method name and parameter ordering differ. **Forest (Parity-style `trace_call`):** @@ -302,158 +149,6 @@ curl -s -X POST "http://localhost:8545" \ }' ``` ---- - -### Storage Layout - -| Slot | Variable | Description | -| ---- | -------------- | ---------------------------- | -| 0 | `x` | Initialized to 42 | -| 1 | `balances` | Mapping base slot | -| 2 | `storageTestA` | Starts empty (for add tests) | -| 3 | `storageTestB` | Starts empty | -| 4 | `storageTestC` | Starts empty | -| 5 | `dynamicArray` | Array length slot | - -### Function Reference - -#### Basic Operations - -| Function | Selector | Description | -| ------------------- | ------------ | --------------------------- | -| `setX(uint256)` | `0x4018d9aa` | Write to slot 0 | -| `deposit()` | `0xd0e30db0` | Receive ETH, update mapping | -| `withdraw(uint256)` | `0x2e1a7d4d` | Send ETH from contract | -| `doRevert()` | `0xafc874d2` | Always reverts | - -#### Call Tracing - -| Function | Selector | Description | -| ----------------------- | ------------ | ---------------------- | -| `callSelf(uint256)` | `0xa1a88595` | Single nested CALL | -| `delegateSelf(uint256)` | `0x8f5e07b8` | DELEGATECALL trace | -| `complexTrace()` | `0x6659ab96` | Multiple nested calls | -| `deepTrace(uint256)` | `0x0f3a17b8` | Recursive N-level deep | - -#### Storage Diff Testing - -| Function | Selector | Description | -| ------------------------------------------ | ------------ | -------------------- | -| `storageAdd(uint256)` | `0x55cb64b4` | Add to empty slot 2 | -| `storageChange(uint256)` | `0x7c8f6e57` | Modify existing slot | -| `storageDelete()` | `0xd92846a3` | Set slot to zero | -| `storageMultiple(uint256,uint256,uint256)` | `0x310af204` | Change slots 2,3,4 | - -## Example curl Requests for Forest node. - -> **Note:**: Anvil has a different params format check above, request data is same as Forest. - -### 1. Basic Trace - setX(123) - -```bash -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "id": 1, - "method": "trace_call", - "params": [ - { - "from": "0xYOUR_ACCOUNT", - "to": "0xCONTRACT_ADDRESS", - "data": "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" - }, - ["trace"], - "latest" - ] - }' | jq '.' -``` - -### 2. State Diff - Storage Change - -```bash -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "id": 1, - "method": "trace_call", - "params": [ - { - "from": "0xYOUR_ACCOUNT", - "to": "0xCONTRACT_ADDRESS", - "data": "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" - }, - ["stateDiff"], - "latest" - ] - }' | jq '.result.stateDiff' -``` - -### 3. Balance Change - deposit() with ETH - -```bash -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "id": 1, - "method": "trace_call", - "params": [ - { - "from": "0xYOUR_ACCOUNT", - "to": "0xCONTRACT_ADDRESS", - "data": "0xd0e30db0", - "value": "0xde0b6b3a7640000" - }, - ["trace", "stateDiff"], - "latest" - ] - }' | jq '.' -``` - -### 4. Deep Trace - deepTrace(3) - -```bash -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "id": 1, - "method": "trace_call", - "params": [ - { - "from": "0xYOUR_ACCOUNT", - "to": "0xCONTRACT_ADDRESS", - "data": "0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" - }, - ["trace"], - "latest" - ] - }' | jq '.result.trace | length' -``` - -### 5. Multiple Storage Slots - storageMultiple(10,20,30) - -```bash -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "id": 1, - "method": "trace_call", - "params": [ - { - "from": "0xYOUR_ACCOUNT", - "to": "0xCONTRACT_ADDRESS", - "data": "0x310af204000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e" - }, - ["stateDiff"], - "latest" - ] - }' | jq '.result.stateDiff' -``` - ## Integration Test Script An automated test script is available to compare Forest's `trace_call` with Anvil's `debug_traceCall`: @@ -474,50 +169,10 @@ or 2. **Balance Diff Tests**: ETH transfers, deposits 3. **Storage Diff Tests**: Single slot, multiple slots, value comparison -## Generating Function Selectors - -Use `cast` from Foundry to generate function selectors: - -```bash -# Get selector for a function -cast sig "setX(uint256)" -# Output: 0x4018d9aa - -# Encode full calldata -cast calldata "setX(uint256)" 123 -# Output: 0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b -``` - -## Troubleshooting - -### Common Issues - -1. **Empty storage in stateDiff**: Ensure the contract is an EVM actor (has bytecode) -2. **Call reverts**: Check function requirements (e.g., `storageChange` requires slot to have value first) -3. **Missing contract**: Verify contract is deployed at the specified address - -### Debug Tips - -```bash -# Check if address has code -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xCONTRACT","latest"]}' \ - | jq -r '.result | length' - -# Check account balance -curl -s -X POST "http://localhost:2345/rpc/v1" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xACCOUNT","latest"]}' \ - | jq '.result' -``` - -## Offical Resources +## Official Resources -| Resource | Description | -| -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| [OpenEthereum trace module](https://openethereum.github.io/JSONRPC-trace-module) | Official Parity/OpenEthereum documentation for `trace_call` and `stateDiff` format | -| [Geth Built-in Tracers](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers) | Geth documentation for `prestateTracer` and `callTracer` | -| [Alchemy: trace_call vs debug_traceCall](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) | Detailed comparison of both tracing methods | -| [Reth trace Namespace](https://reth.rs/jsonrpc/trace) | Reth's implementation of the trace API (follows Parity format) | -| [Foundry Book - Anvil](https://book.getfoundry.sh/reference/anvil/) | Anvil local development node documentation | +- [OpenEthereum trace module](https://openethereum.github.io/JSONRPC-trace-module) +- [Geth Built-in Tracers](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers) +- [Alchemy: trace_call vs debug_traceCall](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) +- [Reth trace Namespace](https://reth.rs/jsonrpc/trace) +- [Foundry Book - Anvil](https://book.getfoundry.sh/reference/anvil/) diff --git a/docs/docs/users/knowledge_base/trace_call.md b/docs/docs/users/knowledge_base/trace_call.md new file mode 100644 index 000000000000..eaabe6635e38 --- /dev/null +++ b/docs/docs/users/knowledge_base/trace_call.md @@ -0,0 +1,363 @@ +# trace_call API Guide + +This guide explains the `trace_call` RPC method implemented in Forest, which follows the **[Parity/OpenEthereum](https://openethereum.github.io/JSONRPC-trace-module#trace_call) and [reth](https://reth.rs/jsonrpc/trace#trace-format-specification) trace format**. + +## Overview + +`trace_call` executes an EVM call and returns detailed execution traces without creating a transaction on the blockchain. It's useful for: + +- Debugging smart contract calls +- Analyzing gas usage and call patterns +- Inspecting state changes before execution +- Understanding nested call hierarchies + +## Request Format + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "0x...", // Sender address + "to": "0x...", // Contract address + "data": "0x...", // Encoded function call + "value": "0x0", // ETH value (optional) + "gas": "0x...", // Gas limit (optional) + "gasPrice": "0x..." // Gas price (optional) + }, + ["trace", "stateDiff"], // Trace types to return + "latest" // Block number or tag + ] +} +``` + +### Trace Types + +| Type | Description | +| ----------- | ------------------------------------------------------------------- | +| `trace` | Call hierarchy with inputs, outputs, gas used | +| `stateDiff` | State changes (balance, nonce, code, storage) | +| `vmTrace` | Low-level EVM execution (not yet implemented, not supported by FVM) | + +## Response Format (Parity Style) + +Forest uses the **Parity/OpenEthereum trace format**, which differs from [Geth's debug API's](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracecall). + +### Trace Response + +```json +{ + "result": { + "output": "0x...", + "trace": [ + { + "type": "call", + "subtraces": 1, + "traceAddress": [], + "action": { + "callType": "call", + "from": "0x...", + "to": "0x...", + "gas": "0x...", + "value": "0x0", + "input": "0x..." + }, + "result": { + "gasUsed": "0x...", + "output": "0x..." + }, + "error": null + } + ], + "stateDiff": { ... } + } +} +``` + +### StateDiff Response + +State changes use **Delta notation**: + +| Symbol | Meaning | Example | +| ------ | --------- | -------------------------------------------------------- | +| `"="` | Unchanged | `"balance": "="` | +| `"+"` | Added | `"balance": { "+": "0x1000" }` | +| `"-"` | Removed | `"balance": { "-": "0x1000" }` | +| `"*"` | Changed | `"balance": { "*": { "from": "0x100", "to": "0x200" } }` | + +```json +{ + "stateDiff": { + "0xcontract...": { + "balance": "=", + "code": "=", + "nonce": "=", + "storage": { + "0x0000...0000": { + "*": { + "from": "0x000...02a", + "to": "0x000...07b" + } + } + } + }, + "0xsender...": { + "balance": "=", + "code": "=", + "nonce": { + "*": { + "from": "0x5", + "to": "0x6" + } + }, + "storage": {} + } + } +} +``` + +## Parity vs Geth Format Comparison + +| Aspect | Forest (Parity) | Geth | +| -------------------- | ------------------------------------ | --------------------------------------- | +| **API Method** | `trace_call` | `debug_traceCall` | +| **State Format** | Delta notation (`"*"`, `"+"`, `"-"`) | Separate `pre`/`post` objects | +| **Unchanged Values** | Shows `"="` | Included in `pre`, absent in `post` | +| **Storage Changes** | `{ "*": { from, to } }` | Compare `pre.storage` vs `post.storage` | +| **Code Field** | `"="` if unchanged | Full bytecode in `pre` | + +### Example: Same call, different formats + +**Forest (Parity):** + +```json +{ + "storage": { + "0x00...00": { + "*": { + "from": "0x00...2a", + "to": "0x00...7b" + } + } + } +} +``` + +**Geth (prestateTracer with diffMode):** + +```json +{ + "pre": { + "storage": { "0x00...00": "0x00...2a" } + }, + "post": { + "storage": { "0x00...00": "0x00...7b" } + } +} +``` + +## Using trace_call with Forest + +### Prerequisites + +- A running Forest node — follow the [Getting Started](https://docs.forest.chainsafe.io/getting_started/syncing) guide to start and sync. +- A deployed EVM contract to trace against (see [Deployed Contracts](#deployed-contracts) below, or deploy your own). + +Forest RPC endpoint: `http://localhost:2345/rpc/v1` + +### Deployed Contracts + +A [Tracer](https://github.com/ChainSafe/forest/blob/963237708137e9c7388c57eba39a2f8bf12ace74/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol) contract is pre-deployed on Calibnet and Mainnet for testing `trace_call`. It provides functions for storage writes, ETH transfers, nested calls, and reverts. + +| Network | Contract Address | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Calibnet | [0x73a43475aa2ccb14246613708b399f4b2ba546c7](https://calibration.filfox.info/en/address/0x73a43475aa2ccb14246613708b399f4b2ba546c7) | +| Mainnet | [0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2](https://filecoin.blockscout.com/address/0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2) | + +> **Note:** Contract availability depends on network state. Verify the contract exists before testing: +> +> ```bash +> curl -s -X POST "http://localhost:2345/rpc/v1" \ +> -H "Content-Type: application/json" \ +> -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0x73a43475aa2ccb14246613708b399f4b2ba546c7","latest"]}' \ +> | jq -r '.result | length' +> ``` +> +> If the result is `> 2`, the contract is deployed. + +### Deploying Your Own Contract + +To deploy a contract on Calibnet or Mainnet, you need: + +1. A funded wallet with FIL tokens for gas (use the [Forest faucet](https://forest-explorer.chainsafe.dev/faucet) for testnet funds) +2. [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (`forge` command) + +```bash +forge create YourContract.sol:YourContract \ + --rpc-url http://localhost:2345/rpc/v1 \ + --broadcast \ + --private-key + +# Output: +# Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +# Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 +# Transaction hash: 0x... +``` + +## Example curl Requests + +The examples below use the pre-deployed [Tracer](https://github.com/ChainSafe/forest/blob/963237708137e9c7388c57eba39a2f8bf12ace74/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol) contract. Here are the functions used: + +| Function | Selector | Description | +| ------------------------------------------ | ------------ | ----------------------------------- | +| `setX(uint256)` | `0x4018d9aa` | Write a value to storage slot 0 | +| `deposit()` | `0xd0e30db0` | Receive ETH, update balance mapping | +| `deepTrace(uint256)` | `0x0f3a17b8` | Recursive N-level nested calls | +| `storageMultiple(uint256,uint256,uint256)` | `0x310af204` | Write to multiple storage slots | + +Before running the examples, set the following environment variables: + +```bash +export FOREST_RPC_URL="http://localhost:2345/rpc/v1" +export SENDER="0xYOUR_ACCOUNT" # your sender address +export CONTRACT="0xCONTRACT_ADDRESS" # deployed Tracer contract +``` + +### 1. Basic Trace - setX(123) + +```bash +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "'$SENDER'", + "to": "'$CONTRACT'", + "data": "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" + }, + ["trace"], + "latest" + ] + }' | jq '.' +``` + +### 2. State Diff - Storage Change + +```bash +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "'$SENDER'", + "to": "'$CONTRACT'", + "data": "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" + }, + ["stateDiff"], + "latest" + ] + }' | jq '.result.stateDiff' +``` + +### 3. Balance Change - deposit() with ETH + +```bash +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "'$SENDER'", + "to": "'$CONTRACT'", + "data": "0xd0e30db0", + "value": "0xde0b6b3a7640000" + }, + ["trace", "stateDiff"], + "latest" + ] + }' | jq '.' +``` + +### 4. Deep Trace - deepTrace(3) + +```bash +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "'$SENDER'", + "to": "'$CONTRACT'", + "data": "0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" + }, + ["trace"], + "latest" + ] + }' | jq '.result.trace | length' +``` + +### 5. Multiple Storage Slots - storageMultiple(10,20,30) + +```bash +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "trace_call", + "params": [ + { + "from": "'$SENDER'", + "to": "'$CONTRACT'", + "data": "0x310af204000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e" + }, + ["stateDiff"], + "latest" + ] + }' | jq '.result.stateDiff' +``` + +## Troubleshooting + +### Common Issues + +1. **Empty storage in stateDiff**: Ensure the contract is an EVM actor (has bytecode) +2. **Call reverts**: Check function requirements (e.g., `storageChange` requires slot to have value first) +3. **Missing contract**: Verify contract is deployed at the specified address + +### Debug Tips + +```bash +# Check if address has code +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["'$CONTRACT'","latest"]}' \ + | jq -r '.result | length' + +# Check account balance +curl -s -X POST "$FOREST_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["'$SENDER'","latest"]}' \ + | jq '.result' +``` + +## Official Resources + +- [OpenEthereum trace module](https://openethereum.github.io/JSONRPC-trace-module) +- [Geth Built-in Tracers](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers) +- [Alchemy: trace_call vs debug_traceCall](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) +- [Reth trace Namespace](https://reth.rs/jsonrpc/trace) diff --git a/scripts/tests/trace_call_integration_test.sh b/scripts/tests/trace_call_integration_test.sh index 2a15fa671368..474776619bee 100755 --- a/scripts/tests/trace_call_integration_test.sh +++ b/scripts/tests/trace_call_integration_test.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash -set -e +# Compare Forest `trace_call` (Parity-style traces and stateDiff) against Anvil +# `debug_traceCall` (Geth-style prestateTracer) using the Tracer.sol contract. +# Verifies call traces, balance diffs, and EVM storage diffs stay in sync. + +set -euo pipefail DEPLOY_CONTRACT=false while [[ $# -gt 0 ]]; do @@ -25,11 +29,12 @@ PASS_COUNT=0 FAIL_COUNT=0 command -v jq &>/dev/null || { echo "Error: jq is required"; exit 1; } command -v curl &>/dev/null || { echo "Error: curl is required"; exit 1; } +# Call JSON-RPC method using jq for body construction (type-safe, no escaping needed) call_rpc() { local url="$1" method="$2" params="$3" - curl -s -X POST "$url" \ - -H "Content-Type: application/json" \ - -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$method\",\"params\":$params}" + local body=$(jq -n --arg method "$method" --argjson params "$params" \ + '{"jsonrpc": "2.0", "id": 1, "method": $method, "params": $params}') + curl -s -X POST "$url" -H "Content-Type: application/json" -d "$body" } check_rpc() { @@ -82,7 +87,7 @@ assert_eq() { local a_norm=$(echo "$a_val" | tr '[:upper:]' '[:lower:]') [[ "$f_norm" == "null" || -z "$f_norm" ]] && f_norm="0x" [[ "$a_norm" == "null" || -z "$a_norm" ]] && a_norm="0x" - + if [ "$f_norm" = "$a_norm" ]; then echo -e " ${GREEN}[PASS]${NC} $label: $f_val" PASS_COUNT=$((PASS_COUNT + 1)) @@ -107,10 +112,18 @@ test_trace() { local name="$1" data="$2" type="${3:-standard}" echo -e "${BLUE}--- $name ---${NC}" - local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"trace\"],\"latest\"]" + local f_params=$(jq -n \ + --arg from "$FOREST_ACCOUNT" \ + --arg to "$FOREST_CONTRACT" \ + --arg data "$data" \ + '[{"from": $from, "to": $to, "data": $data}, ["trace"], "latest"]') local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") - local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"callTracer\"}]" + local a_params=$(jq -n \ + --arg from "$ANVIL_ACCOUNT" \ + --arg to "$ANVIL_CONTRACT" \ + --arg data "$data" \ + '[{"from": $from, "to": $to, "data": $data}, "latest", {"tracer": "callTracer"}]') local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") local f_input=$(echo "$f_resp" | jq -r '.result.trace[0].action.input') @@ -144,10 +157,20 @@ test_balance_diff() { local name="$1" data="$2" value="${3:-0x0}" expect="${4:-unchanged}" echo -e "${BLUE}--- $name (balance) ---${NC}" - local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},[\"stateDiff\"],\"latest\"]" + local f_params=$(jq -n \ + --arg from "$FOREST_ACCOUNT" \ + --arg to "$FOREST_CONTRACT" \ + --arg data "$data" \ + --arg value "$value" \ + '[{"from": $from, "to": $to, "data": $data, "value": $value}, ["stateDiff"], "latest"]') local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") - local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\",\"value\":\"$value\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" + local a_params=$(jq -n \ + --arg from "$ANVIL_ACCOUNT" \ + --arg to "$ANVIL_CONTRACT" \ + --arg data "$data" \ + --arg value "$value" \ + '[{"from": $from, "to": $to, "data": $data, "value": $value}, "latest", {"tracer": "prestateTracer", "tracerConfig": {"diffMode": true}}]') local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') @@ -170,10 +193,18 @@ test_storage_diff() { local name="$1" data="$2" slot="$3" expect_type="${4:-changed}" echo -e "${BLUE}--- $name (storage) ---${NC}" - local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"stateDiff\"],\"latest\"]" + local f_params=$(jq -n \ + --arg from "$FOREST_ACCOUNT" \ + --arg to "$FOREST_CONTRACT" \ + --arg data "$data" \ + '[{"from": $from, "to": $to, "data": $data}, ["stateDiff"], "latest"]') local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") - local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" + local a_params=$(jq -n \ + --arg from "$ANVIL_ACCOUNT" \ + --arg to "$ANVIL_CONTRACT" \ + --arg data "$data" \ + '[{"from": $from, "to": $to, "data": $data}, "latest", {"tracer": "prestateTracer", "tracerConfig": {"diffMode": true}}]') local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') @@ -215,10 +246,18 @@ test_storage_multiple() { local slots=("$@") echo -e "${BLUE}--- $name (multi-storage) ---${NC}" - local f_params="[{\"from\":\"$FOREST_ACCOUNT\",\"to\":\"$FOREST_CONTRACT\",\"data\":\"$data\"},[\"stateDiff\"],\"latest\"]" + local f_params=$(jq -n \ + --arg from "$FOREST_ACCOUNT" \ + --arg to "$FOREST_CONTRACT" \ + --arg data "$data" \ + '[{"from": $from, "to": $to, "data": $data}, ["stateDiff"], "latest"]') local f_resp=$(call_rpc "$FOREST_RPC_URL" "trace_call" "$f_params") - local a_params="[{\"from\":\"$ANVIL_ACCOUNT\",\"to\":\"$ANVIL_CONTRACT\",\"data\":\"$data\"},\"latest\",{\"tracer\":\"prestateTracer\",\"tracerConfig\":{\"diffMode\":true}}]" + local a_params=$(jq -n \ + --arg from "$ANVIL_ACCOUNT" \ + --arg to "$ANVIL_CONTRACT" \ + --arg data "$data" \ + '[{"from": $from, "to": $to, "data": $data}, "latest", {"tracer": "prestateTracer", "tracerConfig": {"diffMode": true}}]') local a_resp=$(call_rpc "$ANVIL_RPC_URL" "debug_traceCall" "$a_params") local f_contract_lower=$(echo "$FOREST_CONTRACT" | tr '[:upper:]' '[:lower:]') From 2371d26405059e1a2395df140e073ee05cb4f21c Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 13 Feb 2026 13:23:20 +0530 Subject: [PATCH 12/19] address comments about adding unit tests and small cleanup --- src/rpc/methods/eth.rs | 149 ++++++++++++++++++ src/rpc/methods/eth/types.rs | 75 +++++++++ .../subcommands/api_cmd/api_compare_tests.rs | 66 +------- .../api_cmd/contracts/tracer/Tracer.sol | 13 +- 4 files changed, 239 insertions(+), 64 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index a75d1a2b0b7f..b9e45537499a 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -4330,6 +4330,8 @@ async fn trace_filter( mod test { use super::*; use crate::rpc::eth::EventEntry; + use crate::rpc::state::{ExecutionTrace, MessageTrace, ReturnTrace}; + use crate::shim::{econ::TokenAmount, error::ExitCode}; use crate::{ db::MemoryDB, test_utils::{construct_bls_messages, construct_eth_messages, construct_messages}, @@ -4852,4 +4854,151 @@ mod test { "overflow with all ones" ); } + + fn create_execution_trace(from: FilecoinAddress, to: FilecoinAddress) -> ExecutionTrace { + ExecutionTrace { + msg: MessageTrace { + from, + to, + value: TokenAmount::default(), + method: 0, + params: Default::default(), + params_codec: 0, + gas_limit: None, + read_only: None, + }, + msg_rct: ReturnTrace { + exit_code: ExitCode::from(0u32), + r#return: Default::default(), + return_codec: 0, + }, + invoked_actor: None, + gas_charges: vec![], + subcalls: vec![], + } + } + + fn create_execution_trace_with_subcalls( + from: FilecoinAddress, + to: FilecoinAddress, + subcalls: Vec, + ) -> ExecutionTrace { + let mut trace = create_execution_trace(from, to); + trace.subcalls = subcalls; + trace + } + + #[test] + fn test_extract_touched_addresses_with_id_addresses() { + // ID addresses (e.g., f0100) can be converted to EthAddress + let from = FilecoinAddress::new_id(100); + let to = FilecoinAddress::new_id(200); + let trace = create_execution_trace(from, to); + + let addresses = extract_touched_eth_addresses(&trace); + + assert_eq!(addresses.len(), 2); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&from).unwrap())); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&to).unwrap())); + } + + #[test] + fn test_extract_touched_addresses_same_from_and_to() { + let addr = FilecoinAddress::new_id(100); + let trace = create_execution_trace(addr, addr); + + let addresses = extract_touched_eth_addresses(&trace); + + // Should deduplicate + assert_eq!(addresses.len(), 1); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr).unwrap())); + } + + #[test] + fn test_extract_touched_addresses_with_subcalls() { + let addr1 = FilecoinAddress::new_id(100); + let addr2 = FilecoinAddress::new_id(200); + let addr3 = FilecoinAddress::new_id(300); + let addr4 = FilecoinAddress::new_id(400); + + let subcall = create_execution_trace(addr3, addr4); + let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]); + + let addresses = extract_touched_eth_addresses(&trace); + + assert_eq!(addresses.len(), 4); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr1).unwrap())); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr2).unwrap())); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr3).unwrap())); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr4).unwrap())); + } + + #[test] + fn test_extract_touched_addresses_with_nested_subcalls() { + let addr1 = FilecoinAddress::new_id(100); + let addr2 = FilecoinAddress::new_id(200); + let addr3 = FilecoinAddress::new_id(300); + let addr4 = FilecoinAddress::new_id(400); + let addr5 = FilecoinAddress::new_id(500); + let addr6 = FilecoinAddress::new_id(600); + + // Create nested structure: trace -> subcall1 -> nested_subcall + let nested_subcall = create_execution_trace(addr5, addr6); + let subcall = create_execution_trace_with_subcalls(addr3, addr4, vec![nested_subcall]); + let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]); + + let addresses = extract_touched_eth_addresses(&trace); + + assert_eq!(addresses.len(), 6); + for addr in [addr1, addr2, addr3, addr4, addr5, addr6] { + assert!(addresses.contains(&EthAddress::from_filecoin_address(&addr).unwrap())); + } + } + + #[test] + fn test_extract_touched_addresses_with_multiple_subcalls() { + let addr1 = FilecoinAddress::new_id(100); + let addr2 = FilecoinAddress::new_id(200); + let addr3 = FilecoinAddress::new_id(300); + let addr4 = FilecoinAddress::new_id(400); + let addr5 = FilecoinAddress::new_id(500); + let addr6 = FilecoinAddress::new_id(600); + + let subcall1 = create_execution_trace(addr3, addr4); + let subcall2 = create_execution_trace(addr5, addr6); + let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall1, subcall2]); + + let addresses = extract_touched_eth_addresses(&trace); + + assert_eq!(addresses.len(), 6); + } + + #[test] + fn test_extract_touched_addresses_deduplicates_across_subcalls() { + // Same address appears in parent and subcall + let addr1 = FilecoinAddress::new_id(100); + let addr2 = FilecoinAddress::new_id(200); + + let subcall = create_execution_trace(addr1, addr2); // addr1 repeated + let trace = create_execution_trace_with_subcalls(addr1, addr2, vec![subcall]); + + let addresses = extract_touched_eth_addresses(&trace); + + // Should deduplicate + assert_eq!(addresses.len(), 2); + } + + #[test] + fn test_extract_touched_addresses_with_non_convertible_addresses() { + // BLS addresses cannot be converted to EthAddress + let bls_addr = FilecoinAddress::new_bls(&[0u8; 48]).unwrap(); + let id_addr = FilecoinAddress::new_id(100); + + let trace = create_execution_trace(bls_addr, id_addr); + let addresses = extract_touched_eth_addresses(&trace); + + // Only the ID address should be in the set + assert_eq!(addresses.len(), 1); + assert!(addresses.contains(&EthAddress::from_filecoin_address(&id_addr).unwrap())); + } } diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index 6b6781bf4593..04102dc2724c 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -903,4 +903,79 @@ mod tests { let result = EthAddress::eth_address_from_pub_key(&pubkey).unwrap(); assert_eq!(result, expected_eth_address); } + + #[test] + fn test_changed_type_serialization() { + let changed = ChangedType { + from: 10u64, + to: 20u64, + }; + let json = serde_json::to_string(&changed).unwrap(); + assert_eq!(json, r#"{"from":10,"to":20}"#); + + let deserialized: ChangedType = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, changed); + } + + #[test] + fn test_delta_unchanged() { + let delta: Delta = Delta::from_comparison(Some(42), Some(42)); + assert!(delta.is_unchanged()); + assert_eq!(delta, Delta::Unchanged); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#""=""#); + } + + #[test] + fn test_delta_added() { + let delta: Delta = Delta::from_comparison(None, Some(100)); + assert!(!delta.is_unchanged()); + assert_eq!(delta, Delta::Added(100)); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#"{"+":100}"#); + } + + #[test] + fn test_delta_removed() { + let delta: Delta = Delta::from_comparison(Some(50), None); + assert!(!delta.is_unchanged()); + assert_eq!(delta, Delta::Removed(50)); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#"{"-":50}"#); + } + + #[test] + fn test_delta_changed() { + let delta: Delta = Delta::from_comparison(Some(10), Some(20)); + assert!(!delta.is_unchanged()); + assert_eq!(delta, Delta::Changed(ChangedType { from: 10, to: 20 })); + + let json = serde_json::to_string(&delta).unwrap(); + assert_eq!(json, r#"{"*":{"from":10,"to":20}}"#); + } + + #[test] + fn test_delta_none_none() { + let delta: Delta = Delta::from_comparison(None, None); + assert!(delta.is_unchanged()); + assert_eq!(delta, Delta::Unchanged); + } + + #[test] + fn test_delta_deserialization() { + let unchanged: Delta = serde_json::from_str(r#""=""#).unwrap(); + assert_eq!(unchanged, Delta::Unchanged); + + let added: Delta = serde_json::from_str(r#"{"+":42}"#).unwrap(); + assert_eq!(added, Delta::Added(42)); + + let removed: Delta = serde_json::from_str(r#"{"-":42}"#).unwrap(); + assert_eq!(removed, Delta::Removed(42)); + + let changed: Delta = serde_json::from_str(r#"{"*":{"from":10,"to":20}}"#).unwrap(); + assert_eq!(changed, Delta::Changed(ChangedType { from: 10, to: 20 })); + } } diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index ab8c359e6faf..426ba66f69bf 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, EthTraceType, EthUint64, ExtBlockNumberOrHash, ExtPredefined, - Predefined, new_eth_tx_from_signed_message, types::*, + BlockNumberOrHash, EthInt64, ExtBlockNumberOrHash, ExtPredefined, Predefined, + new_eth_tx_from_signed_message, types::*, }; use crate::rpc::gas::{GasEstimateGasLimit, GasEstimateMessageGas}; use crate::rpc::miner::BlockTemplate; @@ -1529,42 +1529,6 @@ fn eth_tests() -> Vec { FilecoinAddressToEthAddress::request((*KNOWN_CALIBNET_F4_ADDRESS, None)).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 } @@ -1621,28 +1585,6 @@ 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(), @@ -1902,9 +1844,7 @@ fn eth_tests_with_tipset(store: &Arc, shared_tipset: &Tipset ),)) .unwrap(), ), - RpcTest::identity( - EthGetBlockTransactionCountByHash::request((block_hash,)).unwrap(), - ), + RpcTest::identity(EthGetBlockTransactionCountByHash::request((block_hash,)).unwrap()), RpcTest::identity( EthGetBlockReceiptsLimited::request(( BlockNumberOrHash::from_block_hash_object(block_hash, true), diff --git a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol index b61a7e4c3ffb..66813cabd258 100644 --- a/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol +++ b/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol @@ -1,6 +1,17 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.30; +/// @title Tracer Contract +/// @notice Test contract for validating Forest's trace_call RPC implementation. +/// @dev This contract is used internally for: +/// - Integration tests comparing Forest's trace_call output with other implementations (e.g., Anvil) +/// - Manual testing of trace, stateDiff +/// - Generating test vectors for EVM execution tracing +/// +/// NOT intended for production use. Functions are designed to exercise specific +/// EVM behaviors (storage writes, subcalls, reverts, events) for trace validation. +/// +/// See: docs/docs/developers/guides/trace_call_guide.md contract Tracer { uint256 public x; // slot 0 - initialized to 42 mapping(address => uint256) public balances; // slot 1 - mapping base From cbd672ad729b4eef27b01f23786870c75d9ffe95 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 13 Feb 2026 16:10:05 +0530 Subject: [PATCH 13/19] fix linter issue --- docs/dictionary.txt | 19 +++++++++++++++++++ .../developers/guides/trace_call_guide.md | 15 +++++++-------- docs/docs/users/knowledge_base/trace_call.md | 8 ++++---- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/dictionary.txt b/docs/dictionary.txt index 3fec05068dac..821d05e53987 100644 --- a/docs/dictionary.txt +++ b/docs/dictionary.txt @@ -8,6 +8,7 @@ blockstore BLS BuildKit Butterflynet +bytecode Calibnet calibnet calibnet-related @@ -28,15 +29,20 @@ config Datacap datacap DDL +debug_traceCall +deepTrace +DELEGATECALL Devnet devnet Devops Devs DHT +diffMode DigitalOcean Drand EAM enums +ETH Eth Ethereum EVM @@ -56,6 +62,8 @@ FIP FIPs FVM GC +Geth +Geth's GH GiB Github @@ -96,6 +104,7 @@ migrator migrators mise-en-place multiaddress +Namespace namespace Neo4j NetworkEvents @@ -109,20 +118,26 @@ NVMe NVXX onwards OOMs +OpenEthereum orchestrator Organisation P2P p2p performant pre-compiled +Pre-deployed +pre-deployed +Pre-funded pre-migrations preload preloaded +prestateTracer pubsub Q4 README retag Reth +reth Reth-like RNG Roadmap @@ -135,10 +150,14 @@ sccache SecP256k1 semver serverless +setX Sqlx +StateDiff +stateDiff Stateful stateful stateroots +storageMultiple struct subcommands swappiness diff --git a/docs/docs/developers/guides/trace_call_guide.md b/docs/docs/developers/guides/trace_call_guide.md index 25e1bbcf1a78..23051fefccac 100644 --- a/docs/docs/developers/guides/trace_call_guide.md +++ b/docs/docs/developers/guides/trace_call_guide.md @@ -1,6 +1,6 @@ # trace_call Developer Guide -This guide covers testing and development workflows for Forest's `trace_call` implementation. For API documentation and user-facing usage, see the [trace_call API guide](/docs/users/knowledge_base/trace_call). +This guide covers testing and development workflows for Forest's `trace_call` implementation. For API documentation and user-facing usage, see the [trace_call API guide](/knowledge_base/trace_call). ## Tracer Contract @@ -64,14 +64,14 @@ cast calldata "setX(uint256)" 123 Pre-deployed Tracer contracts for quick testing: -| Network | Contract Address | -| -------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| Calibnet | [0x73a43475aa2ccb14246613708b399f4b2ba546c7](https://calibration.filfox.info/en/address/0x73a43475aa2ccb14246613708b399f4b2ba546c7) | -| Mainnet | [0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2](https://filecoin.blockscout.com/address/0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2) | +| Network | Contract Address | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Calibnet | [`0x73a43475aa2ccb14246613708b399f4b2ba546c7`](https://calibration.filfox.info/en/address/0x73a43475aa2ccb14246613708b399f4b2ba546c7) | +| Mainnet | [`0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2`](https://filecoin.blockscout.com/address/0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2) | ## Comparison Testing with Anvil -Anvil uses **Geth-style** tracing (`debug_traceCall` with `prestateTracer`), while Forest uses **Parity-style** tracing (`trace_call` with `stateDiff`). This makes Anvil useful for comparison testing — verifying that Forest produces semantically equivalent results in a different format. +Anvil uses **Geth style** tracing (`debug_traceCall` with `prestateTracer`), while Forest uses **Parity style** tracing (`trace_call` with `stateDiff`). This makes Anvil useful for comparison testing — verifying that Forest produces semantically equivalent results in a different format. ### Prerequisites @@ -99,7 +99,6 @@ Anvil RPC endpoint: `http://localhost:8545` ### Deploying Contract on Anvil ```bash -# Use the first pre-funded account's private key forge create src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol:Tracer \ --rpc-url http://localhost:8545 \ --broadcast \ @@ -132,7 +131,7 @@ curl -s -X POST "http://localhost:2345/rpc/v1" \ }' ``` -**Anvil (Geth-style `debug_traceCall`):** +**Anvil (Geth style `debug_traceCall`):** ```bash curl -s -X POST "http://localhost:8545" \ diff --git a/docs/docs/users/knowledge_base/trace_call.md b/docs/docs/users/knowledge_base/trace_call.md index eaabe6635e38..3c633b722642 100644 --- a/docs/docs/users/knowledge_base/trace_call.md +++ b/docs/docs/users/knowledge_base/trace_call.md @@ -171,10 +171,10 @@ Forest RPC endpoint: `http://localhost:2345/rpc/v1` A [Tracer](https://github.com/ChainSafe/forest/blob/963237708137e9c7388c57eba39a2f8bf12ace74/src/tool/subcommands/api_cmd/contracts/tracer/Tracer.sol) contract is pre-deployed on Calibnet and Mainnet for testing `trace_call`. It provides functions for storage writes, ETH transfers, nested calls, and reverts. -| Network | Contract Address | -| -------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| Calibnet | [0x73a43475aa2ccb14246613708b399f4b2ba546c7](https://calibration.filfox.info/en/address/0x73a43475aa2ccb14246613708b399f4b2ba546c7) | -| Mainnet | [0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2](https://filecoin.blockscout.com/address/0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2) | +| Network | Contract Address | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Calibnet | [`0x73a43475aa2ccb14246613708b399f4b2ba546c7`](https://calibration.filfox.info/en/address/0x73a43475aa2ccb14246613708b399f4b2ba546c7) | +| Mainnet | [`0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2`](https://filecoin.blockscout.com/address/0x9BB686Ba6a50D1CF670a98f522a59555d4977fb2) | > **Note:** Contract availability depends on network state. Verify the contract exists before testing: > From 641aad6b1fd8e6d03c805140456600736c4b9de1 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 19 Feb 2026 15:26:38 +0530 Subject: [PATCH 14/19] allow the state tree flush in call_with_gas based on bool --- src/rpc/methods/eth.rs | 12 ++++++++++-- src/rpc/methods/gas.rs | 1 + src/state_manager/mod.rs | 17 +++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index b9e45537499a..ad3d73f4d4d8 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -2152,7 +2152,7 @@ where { let (invoc_res, _) = ctx .state_manager - .apply_on_state_with_gas(tipset, msg, StateLookupPolicy::Enabled) + .apply_on_state_with_gas(tipset, msg, StateLookupPolicy::Enabled, false) .await .map_err(|e| anyhow::anyhow!("failed to apply on state with gas: {e}"))?; @@ -2249,6 +2249,7 @@ where Some(ts), VMTrace::NotTraced, StateLookupPolicy::Enabled, + false, ) .await?; Ok(apply_ret.msg_receipt().exit_code().is_success()) @@ -3972,9 +3973,16 @@ impl RpcMethod<3> for EthTraceCall { let (invoke_result, post_state_root) = ctx .state_manager - .apply_on_state_with_gas(Some(ts.clone()), msg.clone(), StateLookupPolicy::Enabled) + .apply_on_state_with_gas( + Some(ts.clone()), + msg.clone(), + StateLookupPolicy::Enabled, + true, + ) .await .map_err(|e| anyhow::anyhow!("failed to apply message: {e}"))?; + let post_state_root = + post_state_root.context("post-execution state root required for trace call")?; let post_state = StateTree::new_from_root(ctx.store_owned(), &post_state_root)?; let mut trace_results = EthTraceResults { diff --git a/src/rpc/methods/gas.rs b/src/rpc/methods/gas.rs index 2edd8ea71b5d..a91aed8ef1d7 100644 --- a/src/rpc/methods/gas.rs +++ b/src/rpc/methods/gas.rs @@ -262,6 +262,7 @@ impl GasEstimateGasLimit { Some(ts.clone()), trace_config, StateLookupPolicy::Enabled, + false, ) .await?; Ok((invoc_res, apply_ret, prior_messages, ts)) diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 2cf27e0b6f52..38d976991a64 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -683,7 +683,8 @@ where tipset: Option, msg: Message, state_lookup: StateLookupPolicy, - ) -> anyhow::Result<(ApiInvocResult, Cid)> { + flush: bool, + ) -> anyhow::Result<(ApiInvocResult, Option)> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); let from_a = self.resolve_to_key_addr(&msg.from, &ts).await?; @@ -706,7 +707,14 @@ where }; let (_invoc_res, apply_ret, duration, state_root) = self - .call_with_gas(&mut chain_msg, &[], Some(ts), VMTrace::Traced, state_lookup) + .call_with_gas( + &mut chain_msg, + &[], + Some(ts), + VMTrace::Traced, + state_lookup, + flush, + ) .await?; Ok(( @@ -733,7 +741,8 @@ where tipset: Option, trace_config: VMTrace, state_lookup: StateLookupPolicy, - ) -> Result<(InvocResult, ApplyRet, Duration, Cid), Error> { + flush: bool, + ) -> Result<(InvocResult, ApplyRet, Duration, Option), Error> { let ts = tipset.unwrap_or_else(|| self.heaviest_tipset()); let (st, _) = self .tipset_state(&ts, state_lookup) @@ -778,7 +787,7 @@ where message.set_sequence(from_actor.sequence); let (ret, duration) = vm.apply_message(message)?; - let state_root = vm.flush()?; + let state_root = if flush { Some(vm.flush()?) } else { None }; Ok((ret, duration, state_root)) })?; From 1a3f0d8bd6766e4c3bcbb9a43ed71097e72297f8 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 19 Feb 2026 15:39:06 +0530 Subject: [PATCH 15/19] refactor integration test to assert value checks and add more tests --- scripts/tests/trace_call_integration_test.sh | 95 +++++++++++++++----- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/scripts/tests/trace_call_integration_test.sh b/scripts/tests/trace_call_integration_test.sh index 474776619bee..89272eac29c0 100755 --- a/scripts/tests/trace_call_integration_test.sh +++ b/scripts/tests/trace_call_integration_test.sh @@ -186,6 +186,13 @@ test_balance_diff() { assert_eq "BalanceChange" "$f_type" "$expect" assert_eq "ForestMatchesAnvil" "$f_type" "$a_type" + + # Compare actual balance values when changed + if [ "$f_type" = "changed" ]; then + local f_bal_to=$(echo "$f_bal" | jq -r '.["*"].to // empty') + [[ -n "$f_bal_to" && -n "$a_post_bal" && "$a_post_bal" != "0x0" ]] && \ + assert_eq "BalanceTo" "$f_bal_to" "$a_post_bal" + fi echo "" } @@ -213,36 +220,43 @@ test_storage_diff() { # Forest: Extract storage slot delta and values local f_slot_data=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" --arg s "$slot" '.result.stateDiff[$a].storage[$s] // null') local f_type=$(get_delta_type "$f_slot_data") - local f_to_val="" - if [ "$f_type" = "changed" ]; then - f_to_val=$(echo "$f_slot_data" | jq -r '.["*"].to // empty') - elif [ "$f_type" = "added" ]; then - f_to_val=$(echo "$f_slot_data" | jq -r '.["+"] // empty') - fi - # Anvil: Extract storage slot pre/post values - local a_pre_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.pre[$a].storage[$s] // "0x0"') - local a_post_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] // "0x0"') + # Anvil: Determine if the storage slot changed. + # Per reth/Parity behavior, all storage slot transitions on existing accounts + # are Delta::Changed ("*"), including zero→nonzero and nonzero→zero. + local a_pre_has=$(echo "$a_resp" | jq --arg a "$a_contract_lower" --arg s "$slot" '.result.pre[$a].storage[$s] != null') + local a_post_has=$(echo "$a_resp" | jq --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] != null') local a_type="unchanged" - if [[ "$a_pre_val" == "0x0" || "$a_pre_val" == "null" ]] && [[ "$a_post_val" != "0x0" && "$a_post_val" != "null" ]]; then - a_type="changed" # Added (was zero, now non-zero) - elif [[ "$a_pre_val" != "0x0" && "$a_pre_val" != "null" ]] && [[ "$a_post_val" != "$a_pre_val" ]]; then - a_type="changed" # Modified + if [[ "$a_pre_has" == "true" || "$a_post_has" == "true" ]]; then + a_type="changed" fi assert_eq "StorageChangeType" "$f_type" "$expect_type" assert_eq "ForestMatchesAnvil" "$f_type" "$a_type" - # Compare actual values if both have the slot - if [[ -n "$f_to_val" && -n "$a_post_val" && "$a_post_val" != "null" ]]; then - assert_eq "StorageValue" "$f_to_val" "$a_post_val" + # Compare values: Forest uses "*": { "from": ..., "to": ... } for all changes. + # Anvil may omit the slot from pre (zero→nonzero) or post (nonzero→zero). + if [ "$f_type" = "changed" ]; then + local f_from=$(echo "$f_slot_data" | jq -r '.["*"].from // empty') + local f_to=$(echo "$f_slot_data" | jq -r '.["*"].to // empty') + + # Anvil pre value: present means nonzero existed, absent means was zero + local a_from=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.pre[$a].storage[$s] // empty') + # Anvil post value: present means nonzero now, absent means cleared to zero + local a_to=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] // empty') + + if [[ -n "$f_to" && -n "$a_to" ]]; then + assert_eq "StorageTo" "$f_to" "$a_to" + elif [[ -n "$f_from" && -n "$a_from" ]]; then + assert_eq "StorageFrom" "$f_from" "$a_from" + fi fi echo "" } test_storage_multiple() { - local name="$1" data="$2" - shift 2 + local name="$1" data="$2" expect_type="$3" + shift 3 local slots=("$@") echo -e "${BLUE}--- $name (multi-storage) ---${NC}" @@ -269,11 +283,13 @@ test_storage_multiple() { for slot in "${slots[@]}"; do local f_slot_data=$(echo "$f_resp" | jq -r --arg a "$f_contract_lower" --arg s "$slot" '.result.stateDiff[$a].storage[$s] // null') - local f_to_val=$(echo "$f_slot_data" | jq -r '.["*"].to // .["+"] // empty') + local f_type=$(get_delta_type "$f_slot_data") + local f_to_val=$(echo "$f_slot_data" | jq -r '.["*"].to // empty') local a_post_val=$(echo "$a_resp" | jq -r --arg a "$a_contract_lower" --arg s "$slot" '.result.post[$a].storage[$s] // empty') local slot_short="${slot: -4}" - assert_eq "Slot$slot_short" "$f_to_val" "$a_post_val" + assert_eq "Slot${slot_short}Type" "$f_type" "$expect_type" + assert_eq "Slot${slot_short}Value" "$f_to_val" "$a_post_val" done echo "" } @@ -309,6 +325,21 @@ test_trace "deepTrace(3)" \ "0x0f3a17b80000000000000000000000000000000000000000000000000000000000000003" \ "deep" +# delegateSelf(999) - selector: 0x8f5e07b8 +test_trace "delegateSelf(999)" \ + "0x8f5e07b800000000000000000000000000000000000000000000000000000000000003e7" + +# wideTrace(3, 1) - selector: 0x56d15f7c, 3 siblings each 1 deep +test_trace "wideTrace(3,1)" \ + "0x56d15f7c00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001" \ + "deep" + +# failAtDepth(3, 2) - revert at depth 2 inside depth-3 recursion +# selector: 0x68bcf9e2 +test_trace "failAtDepth(3,2)" \ + "0x68bcf9e200000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002" \ + "revert" + # --- Balance Diff Tests --- echo -e "${BLUE}=== Balance Diff Tests ===${NC}" echo "" @@ -327,21 +358,37 @@ test_balance_diff "setX(42) no value" \ echo -e "${BLUE}=== Storage Diff Tests ===${NC}" echo "" -# Slot 0: x variable (initialized to 42) +# Note: Each trace_call is stateless — simulated against on-chain state. +# Tests do NOT affect each other. Slot 0 (x) is always 42 on-chain. + +# Slot 0: setX(123) modifies existing value (42 → 123 = changed) test_storage_diff "setX(123) - change slot 0" \ "0x4018d9aa000000000000000000000000000000000000000000000000000000000000007b" \ "0x0000000000000000000000000000000000000000000000000000000000000000" \ "changed" -# Slot 2: storageTestA (starts empty) -test_storage_diff "storageAdd(100) - add slot 2" \ +# Slot 0: setX(42) writes same value as on-chain (42 → 42 = unchanged, no storage entry) +test_storage_diff "setX(42) - no-op same value" \ + "0x4018d9aa000000000000000000000000000000000000000000000000000000000000002a" \ + "0x0000000000000000000000000000000000000000000000000000000000000000" \ + "unchanged" + +# Slot 0: setX(0) clears existing value (42 → 0 = changed per reth/Parity) +test_storage_diff "setX(0) - clear slot 0" \ + "0x4018d9aa0000000000000000000000000000000000000000000000000000000000000000" \ + "0x0000000000000000000000000000000000000000000000000000000000000000" \ + "changed" + +# Slot 2: storageTestA (starts empty on-chain, 0 → 100 = changed per reth/Parity) +test_storage_diff "storageAdd(100) - write slot 2" \ "0x55cb64b40000000000000000000000000000000000000000000000000000000000000064" \ "0x0000000000000000000000000000000000000000000000000000000000000002" \ "changed" -# Multiple slots: storageTestA(10), storageTestB(20), storageTestC(30) +# Multiple slots: storageTestA(10), storageTestB(20), storageTestC(30) - all start empty test_storage_multiple "storageMultiple(10,20,30) - slots 2,3,4" \ "0x310af204000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001e" \ + "changed" \ "0x0000000000000000000000000000000000000000000000000000000000000002" \ "0x0000000000000000000000000000000000000000000000000000000000000003" \ "0x0000000000000000000000000000000000000000000000000000000000000004" From 57ad705ba83809f27d04748298439e8199b689c4 Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 19 Feb 2026 17:06:13 +0530 Subject: [PATCH 16/19] fix linter issue and make blockparam optional in the API --- src/rpc/methods/eth.rs | 7 ++++++- src/tool/subcommands/api_cmd/test_snapshots_ignored.txt | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index ad3d73f4d4d8..c43d48b1da40 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3951,13 +3951,18 @@ impl RpcMethod<3> for EthTraceCall { const PERMISSION: Permission = Permission::Read; const DESCRIPTION: Option<&'static str> = Some("Returns traces created by the transaction."); - type Params = (EthCallMessage, NonEmpty, BlockNumberOrHash); + type Params = ( + EthCallMessage, + NonEmpty, + Option, + ); type Ok = EthTraceResults; async fn handle( ctx: Ctx, (tx, trace_types, block_param): Self::Params, ) -> Result { let msg = Message::try_from(tx)?; + let block_param = block_param.unwrap_or(BlockNumberOrHash::from_str("latest")?); let ts = tipset_by_block_number_or_hash( ctx.chain_store(), block_param, diff --git a/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt b/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt index e56c80c338f1..33841dd53eb4 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt @@ -82,6 +82,7 @@ Forest.ChainExportDiff Forest.ChainExportStatus Forest.ChainGetMinBaseFee Forest.ChainGetTipsetByParentState +Forest.EthTraceCall Forest.NetInfo Forest.SnapshotGC Forest.StateActorInfo From 812e5e2c151c73db3db465ed87f89b5a55444e5a Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 19 Feb 2026 19:05:02 +0530 Subject: [PATCH 17/19] update open rpc doc to add trace call and only support trace_call in V1 and V2 --- src/rpc/methods/eth.rs | 2 +- .../forest__rpc__tests__rpc__v1.snap | 276 +++++++++++++++++ .../forest__rpc__tests__rpc__v2.snap | 283 ++++++++++++++++++ 3 files changed, 560 insertions(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index e54d2b8abdd3..30ffef5ef60b 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3991,7 +3991,7 @@ impl RpcMethod<3> for 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"]; - const API_PATHS: BitFlags = ApiPaths::all(); + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V1 | V2 }); const PERMISSION: Permission = Permission::Read; const DESCRIPTION: Option<&'static str> = Some("Returns traces created by the transaction."); diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap index 2cb6025bcf52..d195d2191e81 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v1.snap @@ -1555,6 +1555,52 @@ methods: items: $ref: "#/components/schemas/EthBlockTrace" paramStructure: by-position + - name: Forest.EthTraceCall + description: Returns traces created by the transaction. + params: + - name: tx + required: true + schema: + $ref: "#/components/schemas/EthCallMessage" + - name: traceTypes + required: true + schema: + $ref: "#/components/schemas/NonEmpty_Array_of_EthTraceType" + - name: blockParam + required: false + schema: + anyOf: + - $ref: "#/components/schemas/BlockNumberOrHash" + - type: "null" + result: + name: Forest.EthTraceCall.Result + required: true + schema: + $ref: "#/components/schemas/EthTraceResults" + paramStructure: by-position + - name: trace_call + description: Returns traces created by the transaction. + params: + - name: tx + required: true + schema: + $ref: "#/components/schemas/EthCallMessage" + - name: traceTypes + required: true + schema: + $ref: "#/components/schemas/NonEmpty_Array_of_EthTraceType" + - name: blockParam + required: false + schema: + anyOf: + - $ref: "#/components/schemas/BlockNumberOrHash" + - type: "null" + result: + name: trace_call.Result + required: true + schema: + $ref: "#/components/schemas/EthTraceResults" + paramStructure: by-position - name: Filecoin.EthTraceFilter description: Returns the traces for transactions matching the filter criteria. params: @@ -4308,6 +4354,26 @@ methods: paramStructure: by-position components: schemas: + AccountDiff: + description: "Account state diff after transaction execution.\nTracks changes to balance, nonce, code, and storage." + type: object + properties: + balance: + $ref: "#/components/schemas/Delta" + code: + $ref: "#/components/schemas/Delta2" + nonce: + $ref: "#/components/schemas/Delta3" + storage: + description: All touched/changed storage values (key -> delta) + type: object + additionalProperties: + $ref: "#/components/schemas/Delta4" + required: + - balance + - code + - nonce + - storage ActorEvent: type: object properties: @@ -5084,6 +5150,58 @@ components: - tipset_keys - skip_checksum - dry_run + ChangedType: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthBigInt" + to: + description: Value after the change + $ref: "#/components/schemas/EthBigInt" + required: + - from + - to + ChangedType2: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthBytes" + to: + description: Value after the change + $ref: "#/components/schemas/EthBytes" + required: + - from + - to + ChangedType3: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthUint64" + to: + description: Value after the change + $ref: "#/components/schemas/EthUint64" + required: + - from + - to + ChangedType4: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthHash" + to: + description: Value after the change + $ref: "#/components/schemas/EthHash" + required: + - from + - to Cid: type: object properties: @@ -5184,6 +5302,126 @@ components: required: - Min - Max + Delta: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthBigInt" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthBigInt" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType" + additionalProperties: false + required: + - "*" + Delta2: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthBytes" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthBytes" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType2" + additionalProperties: false + required: + - "*" + Delta3: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthUint64" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthUint64" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType3" + additionalProperties: false + required: + - "*" + Delta4: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthHash" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthHash" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType4" + additionalProperties: false + required: + - "*" ECTipSet: type: object properties: @@ -5606,6 +5844,34 @@ components: type: - string - "null" + EthTraceResults: + type: object + properties: + output: + description: Output bytes from the transaction execution + anyOf: + - $ref: "#/components/schemas/EthBytes" + - type: "null" + stateDiff: + description: State diff showing all account changes (only when StateDiff trace type requested) + anyOf: + - $ref: "#/components/schemas/StateDiff" + - type: "null" + trace: + description: Call trace hierarchy (only when Trace trace type requested) + type: array + items: + $ref: "#/components/schemas/EthTrace" + required: + - trace + EthTraceType: + oneOf: + - description: "Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`)\nwith details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`." + type: string + const: trace + - description: "Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`)\ncaused by the simulated transaction.\n\nIt shows `\"from\"` and `\"to\"` values for modified fields, using `\"+\"`, `\"-\"`, or `\"=\"` for code changes." + type: string + const: stateDiff EthTxReceipt: type: object properties: @@ -6779,6 +7045,11 @@ components: items: $ref: "#/components/schemas/ECTipSet" minItems: 1 + NonEmpty_Array_of_EthTraceType: + type: array + items: + $ref: "#/components/schemas/EthTraceType" + minItems: 1 Nonce: type: string Nullable_Address: @@ -7302,6 +7573,11 @@ components: - Manifest - Bundle - ActorCids + StateDiff: + description: State diff containing all account changes from a transaction. + type: object + additionalProperties: + $ref: "#/components/schemas/AccountDiff" SupplementalData: type: object properties: diff --git a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap index 2f602208d0d8..4c9122d572be 100644 --- a/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap +++ b/src/rpc/snapshots/forest__rpc__tests__rpc__v2.snap @@ -1112,6 +1112,52 @@ methods: items: $ref: "#/components/schemas/EthBlockTrace" paramStructure: by-position + - name: Forest.EthTraceCall + description: Returns traces created by the transaction. + params: + - name: tx + required: true + schema: + $ref: "#/components/schemas/EthCallMessage" + - name: traceTypes + required: true + schema: + $ref: "#/components/schemas/NonEmpty_Array_of_EthTraceType" + - name: blockParam + required: false + schema: + anyOf: + - $ref: "#/components/schemas/BlockNumberOrHash" + - type: "null" + result: + name: Forest.EthTraceCall.Result + required: true + schema: + $ref: "#/components/schemas/EthTraceResults" + paramStructure: by-position + - name: trace_call + description: Returns traces created by the transaction. + params: + - name: tx + required: true + schema: + $ref: "#/components/schemas/EthCallMessage" + - name: traceTypes + required: true + schema: + $ref: "#/components/schemas/NonEmpty_Array_of_EthTraceType" + - name: blockParam + required: false + schema: + anyOf: + - $ref: "#/components/schemas/BlockNumberOrHash" + - type: "null" + result: + name: trace_call.Result + required: true + schema: + $ref: "#/components/schemas/EthTraceResults" + paramStructure: by-position - name: Filecoin.EthTraceTransaction description: Returns the traces for a specific transaction. params: @@ -1330,6 +1376,26 @@ methods: paramStructure: by-position components: schemas: + AccountDiff: + description: "Account state diff after transaction execution.\nTracks changes to balance, nonce, code, and storage." + type: object + properties: + balance: + $ref: "#/components/schemas/Delta" + code: + $ref: "#/components/schemas/Delta2" + nonce: + $ref: "#/components/schemas/Delta3" + storage: + description: All touched/changed storage values (key -> delta) + type: object + additionalProperties: + $ref: "#/components/schemas/Delta4" + required: + - balance + - code + - nonce + - storage ActorState: type: object properties: @@ -1577,12 +1643,71 @@ components: $ref: "#/components/schemas/EthInt64" required: - blockNumber + BlockNumberOrHash: + anyOf: + - type: string + - $ref: "#/components/schemas/EthInt64" + - $ref: "#/components/schemas/EthHash" + - $ref: "#/components/schemas/BlockNumber" + - $ref: "#/components/schemas/BlockHash" BlockNumberOrPredefined: anyOf: - type: string - $ref: "#/components/schemas/EthInt64" Bloom: type: string + ChangedType: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthBigInt" + to: + description: Value after the change + $ref: "#/components/schemas/EthBigInt" + required: + - from + - to + ChangedType2: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthBytes" + to: + description: Value after the change + $ref: "#/components/schemas/EthBytes" + required: + - from + - to + ChangedType3: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthUint64" + to: + description: Value after the change + $ref: "#/components/schemas/EthUint64" + required: + - from + - to + ChangedType4: + description: Represents a changed value with before and after states. + type: object + properties: + from: + description: Value before the change + $ref: "#/components/schemas/EthHash" + to: + description: Value after the change + $ref: "#/components/schemas/EthHash" + required: + - from + - to Cid: type: object properties: @@ -1590,6 +1715,126 @@ components: type: string required: - / + Delta: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthBigInt" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthBigInt" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType" + additionalProperties: false + required: + - "*" + Delta2: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthBytes" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthBytes" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType2" + additionalProperties: false + required: + - "*" + Delta3: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthUint64" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthUint64" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType3" + additionalProperties: false + required: + - "*" + Delta4: + description: Represents how a value changed during transaction execution. + oneOf: + - description: "Existing value didn't change." + type: string + const: "=" + - description: A new value was added (account/storage created). + type: object + properties: + +: + $ref: "#/components/schemas/EthHash" + additionalProperties: false + required: + - + + - description: The existing value was removed (account/storage deleted). + type: object + properties: + "-": + $ref: "#/components/schemas/EthHash" + additionalProperties: false + required: + - "-" + - description: The existing value changed from one value to another. + type: object + properties: + "*": + $ref: "#/components/schemas/ChangedType4" + additionalProperties: false + required: + - "*" ElectionProof: type: object properties: @@ -1932,6 +2177,34 @@ components: - traceAddress - action - result + EthTraceResults: + type: object + properties: + output: + description: Output bytes from the transaction execution + anyOf: + - $ref: "#/components/schemas/EthBytes" + - type: "null" + stateDiff: + description: State diff showing all account changes (only when StateDiff trace type requested) + anyOf: + - $ref: "#/components/schemas/StateDiff" + - type: "null" + trace: + description: Call trace hierarchy (only when Trace trace type requested) + type: array + items: + $ref: "#/components/schemas/EthTrace" + required: + - trace + EthTraceType: + oneOf: + - description: "Requests a structured call graph, showing the hierarchy of calls (e.g., `call`, `create`, `reward`)\nwith details like `from`, `to`, `gas`, `input`, `output`, and `subtraces`." + type: string + const: trace + - description: "Requests a state difference object, detailing changes to account states (e.g., `balance`, `nonce`, `storage`, `code`)\ncaused by the simulated transaction.\n\nIt shows `\"from\"` and `\"to\"` values for modified fields, using `\"+\"`, `\"-\"`, or `\"=\"` for code changes." + type: string + const: stateDiff EthTxReceipt: type: object properties: @@ -2006,6 +2279,11 @@ components: items: $ref: "#/components/schemas/Cid" minItems: 1 + NonEmpty_Array_of_EthTraceType: + type: array + items: + $ref: "#/components/schemas/EthTraceType" + minItems: 1 Nonce: type: string Nullable_Address: @@ -2070,6 +2348,11 @@ components: maximum: 255 minimum: 0 - $ref: "#/components/schemas/SignatureType" + StateDiff: + description: State diff containing all account changes from a transaction. + type: object + additionalProperties: + $ref: "#/components/schemas/AccountDiff" Ticket: type: object properties: From 324f78f3be2514babcaeffe1e49683ea5dc73b6f Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Thu, 19 Feb 2026 19:34:14 +0530 Subject: [PATCH 18/19] fix linter issue --- src/rpc/methods/eth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 30ffef5ef60b..1a261f9ea6cc 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3991,7 +3991,7 @@ impl RpcMethod<3> for 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"]; - const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V1 | V2 }); + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V1 | V2 }); const PERMISSION: Permission = Permission::Read; const DESCRIPTION: Option<&'static str> = Some("Returns traces created by the transaction."); From ee300c589b0c7f49f240c6c39a9dd8e1d3e3d0af Mon Sep 17 00:00:00 2001 From: Aryan Tikarya Date: Fri, 20 Feb 2026 23:12:02 +0530 Subject: [PATCH 19/19] address comments --- docs/dictionary.txt | 9 --------- docs/docs/developers/guides/trace_call_guide.md | 6 +++--- .../users/knowledge_base/{ => RPC}/trace_call.md | 14 +++++++------- src/rpc/methods/eth.rs | 3 +-- src/rpc/methods/eth/types.rs | 3 +-- 5 files changed, 12 insertions(+), 23 deletions(-) rename docs/docs/users/knowledge_base/{ => RPC}/trace_call.md (96%) diff --git a/docs/dictionary.txt b/docs/dictionary.txt index 9b64b1224340..4a4190eb96ca 100644 --- a/docs/dictionary.txt +++ b/docs/dictionary.txt @@ -32,15 +32,11 @@ config Datacap datacap DDL -debug_traceCall -deepTrace -DELEGATECALL Devnet devnet Devops Devs DHT -diffMode DigitalOcean Drand EAM @@ -135,7 +131,6 @@ Pre-funded pre-migrations preload preloaded -prestateTracer pubsub Q4 README @@ -154,14 +149,10 @@ sccache SecP256k1 semver serverless -setX Sqlx -StateDiff -stateDiff Stateful stateful stateroots -storageMultiple struct subcommands swappiness diff --git a/docs/docs/developers/guides/trace_call_guide.md b/docs/docs/developers/guides/trace_call_guide.md index 23051fefccac..92ae832c2ee1 100644 --- a/docs/docs/developers/guides/trace_call_guide.md +++ b/docs/docs/developers/guides/trace_call_guide.md @@ -1,6 +1,6 @@ # trace_call Developer Guide -This guide covers testing and development workflows for Forest's `trace_call` implementation. For API documentation and user-facing usage, see the [trace_call API guide](/knowledge_base/trace_call). +This guide covers testing and development workflows for Forest's `trace_call` implementation. For API documentation and user-facing usage, see the [trace_call API guide](/knowledge_base/rpc/trace_call). ## Tracer Contract @@ -33,7 +33,7 @@ The [`Tracer.sol`](https://github.com/ChainSafe/forest/blob/963237708137e9c7388c | Function | Selector | Description | | ----------------------- | ------------ | ---------------------- | | `callSelf(uint256)` | `0xa1a88595` | Single nested CALL | -| `delegateSelf(uint256)` | `0x8f5e07b8` | DELEGATECALL trace | +| `delegateSelf(uint256)` | `0x8f5e07b8` | `DELEGATECALL` trace | | `complexTrace()` | `0x6659ab96` | Multiple nested calls | | `deepTrace(uint256)` | `0x0f3a17b8` | Recursive N-level deep | @@ -172,6 +172,6 @@ or - [OpenEthereum trace module](https://openethereum.github.io/JSONRPC-trace-module) - [Geth Built-in Tracers](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers) -- [Alchemy: trace_call vs debug_traceCall](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) +- [Alchemy: `trace_call` vs `debug_traceCall`](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) - [Reth trace Namespace](https://reth.rs/jsonrpc/trace) - [Foundry Book - Anvil](https://book.getfoundry.sh/reference/anvil/) diff --git a/docs/docs/users/knowledge_base/trace_call.md b/docs/docs/users/knowledge_base/RPC/trace_call.md similarity index 96% rename from docs/docs/users/knowledge_base/trace_call.md rename to docs/docs/users/knowledge_base/RPC/trace_call.md index 3c633b722642..5df1a153a1cf 100644 --- a/docs/docs/users/knowledge_base/trace_call.md +++ b/docs/docs/users/knowledge_base/RPC/trace_call.md @@ -76,7 +76,7 @@ Forest uses the **Parity/OpenEthereum trace format**, which differs from [Geth's } ``` -### StateDiff Response +### `stateDiff` Response State changes use **Delta notation**: @@ -145,7 +145,7 @@ State changes use **Delta notation**: } ``` -**Geth (prestateTracer with diffMode):** +**Geth (`prestateTracer` with `diffMode`):** ```json { @@ -225,7 +225,7 @@ export SENDER="0xYOUR_ACCOUNT" # your sender address export CONTRACT="0xCONTRACT_ADDRESS" # deployed Tracer contract ``` -### 1. Basic Trace - setX(123) +### 1. Basic Trace - `setX(123)` ```bash curl -s -X POST "$FOREST_RPC_URL" \ @@ -289,7 +289,7 @@ curl -s -X POST "$FOREST_RPC_URL" \ }' | jq '.' ``` -### 4. Deep Trace - deepTrace(3) +### 4. Deep Trace - `deepTrace(3)` ```bash curl -s -X POST "$FOREST_RPC_URL" \ @@ -310,7 +310,7 @@ curl -s -X POST "$FOREST_RPC_URL" \ }' | jq '.result.trace | length' ``` -### 5. Multiple Storage Slots - storageMultiple(10,20,30) +### 5. Multiple Storage Slots - `storageMultiple(10,20,30)` ```bash curl -s -X POST "$FOREST_RPC_URL" \ @@ -335,7 +335,7 @@ curl -s -X POST "$FOREST_RPC_URL" \ ### Common Issues -1. **Empty storage in stateDiff**: Ensure the contract is an EVM actor (has bytecode) +1. **Empty storage in `stateDiff`**: Ensure the contract is an EVM actor (has bytecode) 2. **Call reverts**: Check function requirements (e.g., `storageChange` requires slot to have value first) 3. **Missing contract**: Verify contract is deployed at the specified address @@ -359,5 +359,5 @@ curl -s -X POST "$FOREST_RPC_URL" \ - [OpenEthereum trace module](https://openethereum.github.io/JSONRPC-trace-module) - [Geth Built-in Tracers](https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers) -- [Alchemy: trace_call vs debug_traceCall](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) +- [Alchemy: `trace_call` vs `debug_traceCall`](https://www.alchemy.com/docs/reference/trace_call-vs-debug_tracecall) - [Reth trace Namespace](https://reth.rs/jsonrpc/trace) diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 1a261f9ea6cc..d6d5de14a6c3 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -269,8 +269,7 @@ lotus_json_with_self!(EthInt64); impl EthHash { // Should ONLY be used for blocks and Filecoin messages. Eth transactions expect a different hashing scheme. - #[allow(clippy::wrong_self_convention)] - pub fn to_cid(&self) -> cid::Cid { + pub fn to_cid(self) -> cid::Cid { let mh = MultihashCode::Blake2b256 .wrap(self.0.as_bytes()) .expect("should not fail"); diff --git a/src/rpc/methods/eth/types.rs b/src/rpc/methods/eth/types.rs index 04102dc2724c..5359c5d57e58 100644 --- a/src/rpc/methods/eth/types.rs +++ b/src/rpc/methods/eth/types.rs @@ -131,8 +131,7 @@ impl GetSize for EthAddress { } impl EthAddress { - #[allow(clippy::wrong_self_convention)] - pub fn to_filecoin_address(&self) -> anyhow::Result { + pub fn to_filecoin_address(self) -> anyhow::Result { if self.is_masked_id() { const PREFIX_LEN: usize = MASKED_ID_PREFIX.len(); // This is a masked ID address.