Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions scripts/tests/api_compare/gen_trace_call_refs.sh
Original file line number Diff line number Diff line change
@@ -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."
146 changes: 145 additions & 1 deletion src/rpc/methods/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::rpc::eth::filter::{
};
use crate::rpc::eth::types::{EthBlockTrace, EthTrace};
use crate::rpc::eth::utils::decode_revert_reason;
use crate::rpc::state::ApiInvocResult;
use crate::rpc::state::{Action, ApiInvocResult, ExecutionTrace, ResultData, TraceEntry};
use crate::rpc::types::{ApiTipsetKey, EventEntry, MessageLookup};
use crate::rpc::{ApiPaths, Ctx, Permission, RpcMethod};
use crate::rpc::{EthEventHandler, LOOKBACK_NO_LIMIT};
Expand Down Expand Up @@ -75,6 +75,8 @@ use std::sync::{Arc, LazyLock};
use tracing::log;
use utils::{decode_payload, lookup_eth_address};

use nunny::Vec as NonEmpty;

static FOREST_TRACE_FILTER_MAX_RESULT: LazyLock<u64> =
LazyLock::new(|| env_or_default("FOREST_TRACE_FILTER_MAX_RESULT", 500));

Expand Down Expand Up @@ -449,6 +451,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<Instruction>,
}

lotus_json_with_self!(EthVmTrace);

#[derive(PartialEq, Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EthTraceResults {
output: Option<EthBytes>,
state_diff: Option<String>,
trace: Vec<TraceEntry>,
// This should always be empty since we don't support `vmTrace` atm (this
// would likely need changes in the FEVM)
vm_trace: Option<EthVmTrace>,
}

lotus_json_with_self!(EthTraceResults);

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)] // try a Vec<String>, then a Vec<Tx>
pub enum Transactions {
Expand Down Expand Up @@ -3314,6 +3353,111 @@ async fn trace_block<B: Blockstore + Send + Sync + 'static>(
Ok(all_traces)
}

fn get_output(msg: &Message, invoke_result: ApiInvocResult) -> Result<EthBytes> {
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<Vec<TraceEntry>> {
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> = ApiPaths::all();
const PERMISSION: Permission = Permission::Read;
type Params = (EthCallMessage, NonEmpty<EthTraceType>, BlockNumberOrHash);
type Ok = EthTraceResults;
async fn handle(
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
(tx, trace_types, block_param): Self::Params,
) -> Result<Self::Ok, ServerError> {
// 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";
Expand Down
38 changes: 38 additions & 0 deletions src/rpc/methods/state/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<ResultData>,
/// 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<usize>,
/// 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 {
Expand Down
1 change: 1 addition & 0 deletions src/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ macro_rules! for_each_rpc_method {
$callback!($crate::rpc::eth::EthSubscribe);
$callback!($crate::rpc::eth::EthSyncing);
$callback!($crate::rpc::eth::EthTraceBlock);
$callback!($crate::rpc::eth::EthTraceCall);
$callback!($crate::rpc::eth::EthTraceFilter);
$callback!($crate::rpc::eth::EthTraceTransaction);
$callback!($crate::rpc::eth::EthTraceReplayBlockTransactions);
Expand Down
Loading
Loading