Skip to main content

forest/rpc/methods/eth/
trace.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::types::{EthAddress, EthBytes, EthCallTraceAction, EthTrace, TraceAction, TraceResult};
5use super::utils::{decode_params, decode_return};
6use super::{
7    EthCallTraceResult, EthCreateTraceAction, EthCreateTraceResult, decode_payload,
8    encode_filecoin_params_as_abi, encode_filecoin_returns_as_abi,
9};
10use crate::eth::{EAMMethod, EVMMethod};
11use crate::rpc::methods::eth::lookup_eth_address;
12use crate::rpc::methods::state::ExecutionTrace;
13use crate::rpc::state::ActorTrace;
14use crate::shim::fvm_shared_latest::METHOD_CONSTRUCTOR;
15use crate::shim::{actors::is_evm_actor, address::Address, error::ExitCode, state_tree::StateTree};
16use fil_actor_eam_state::v12 as eam12;
17use fil_actor_evm_state::v15 as evm12;
18use fil_actor_init_state::v12::ExecReturn;
19use fil_actor_init_state::v15::Method as InitMethod;
20use fvm_ipld_blockstore::Blockstore;
21
22use anyhow::{Context, bail};
23use num::FromPrimitive;
24use tracing::debug;
25
26#[derive(Default)]
27pub struct Environment {
28    caller: EthAddress,
29    is_evm: bool,
30    subtrace_count: i64,
31    pub traces: Vec<EthTrace>,
32    last_byte_code: Option<EthAddress>,
33}
34
35pub fn base_environment<BS: Blockstore + Send + Sync>(
36    state: &StateTree<BS>,
37    from: &Address,
38) -> anyhow::Result<Environment> {
39    let sender = lookup_eth_address(from, state)?
40        .with_context(|| format!("top-level message sender {from} s could not be found"))?;
41    Ok(Environment {
42        caller: sender,
43        ..Environment::default()
44    })
45}
46
47fn trace_to_address(trace: &ActorTrace) -> EthAddress {
48    if let Some(addr) = trace.state.delegated_address
49        && let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr.into())
50    {
51        return eth_addr;
52    }
53    EthAddress::from_actor_id(trace.id)
54}
55
56/// Returns true if the trace is a call to an EVM or EAM actor.
57fn trace_is_evm_or_eam(trace: &ExecutionTrace) -> bool {
58    if let Some(invoked_actor) = &trace.invoked_actor {
59        is_evm_actor(&invoked_actor.state.code)
60            || invoked_actor.id != Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap()
61    } else {
62        false
63    }
64}
65
66/// Returns true if the trace is a call to an EVM or EAM actor.
67fn trace_err_msg(trace: &ExecutionTrace) -> Option<String> {
68    let code = trace.msg_rct.exit_code;
69
70    if code.is_success() {
71        return None;
72    }
73
74    // EVM tools often expect this literal string.
75    if code == ExitCode::SYS_OUT_OF_GAS {
76        return Some("out of gas".into());
77    }
78
79    // indicate when we have a "system" error.
80    if code < ExitCode::FIRST_ACTOR_ERROR_CODE.into() {
81        return Some(format!("vm error: {code}"));
82    }
83
84    // handle special exit codes from the EVM/EAM.
85    if trace_is_evm_or_eam(trace) {
86        match code.into() {
87            evm12::EVM_CONTRACT_REVERTED => return Some("Reverted".into()), // capitalized for compatibility
88            evm12::EVM_CONTRACT_INVALID_INSTRUCTION => return Some("invalid instruction".into()),
89            evm12::EVM_CONTRACT_UNDEFINED_INSTRUCTION => {
90                return Some("undefined instruction".into());
91            }
92            evm12::EVM_CONTRACT_STACK_UNDERFLOW => return Some("stack underflow".into()),
93            evm12::EVM_CONTRACT_STACK_OVERFLOW => return Some("stack overflow".into()),
94            evm12::EVM_CONTRACT_ILLEGAL_MEMORY_ACCESS => {
95                return Some("illegal memory access".into());
96            }
97            evm12::EVM_CONTRACT_BAD_JUMPDEST => return Some("invalid jump destination".into()),
98            evm12::EVM_CONTRACT_SELFDESTRUCT_FAILED => return Some("self destruct failed".into()),
99            _ => (),
100        }
101    }
102    // everything else...
103    Some(format!("actor error: {code}"))
104}
105
106/// Recursively builds the traces for a given ExecutionTrace by walking the subcalls
107pub fn build_traces(
108    env: &mut Environment,
109    address: &[i64],
110    trace: ExecutionTrace,
111) -> anyhow::Result<()> {
112    let (trace, recurse_into) = build_trace(env, address, trace)?;
113
114    let last_trace_idx = if let Some(trace) = trace {
115        let len = env.traces.len();
116        env.traces.push(trace);
117        env.subtrace_count += 1;
118        Some(len)
119    } else {
120        None
121    };
122
123    // Skip if there's nothing more to do and/or `build_trace` told us to skip this one.
124    let (recurse_into, invoked_actor) = if let Some(trace) = recurse_into {
125        if let Some(invoked_actor) = &trace.invoked_actor {
126            let invoked_actor = invoked_actor.clone();
127            (trace, invoked_actor)
128        } else {
129            return Ok(());
130        }
131    } else {
132        return Ok(());
133    };
134
135    let mut sub_env = Environment {
136        caller: trace_to_address(&invoked_actor),
137        is_evm: is_evm_actor(&invoked_actor.state.code),
138        traces: env.traces.clone(),
139        ..Environment::default()
140    };
141    for subcall in recurse_into.subcalls.into_iter() {
142        let mut new_address = address.to_vec();
143        new_address.push(sub_env.subtrace_count);
144        build_traces(&mut sub_env, &new_address, subcall)?;
145    }
146    env.traces = sub_env.traces;
147    if let Some(idx) = last_trace_idx {
148        env.traces.get_mut(idx).expect("Infallible").subtraces = sub_env.subtrace_count;
149    }
150
151    Ok(())
152}
153
154// `build_trace` processes the passed execution trace and updates the environment, if necessary.
155//
156// On success, it returns a trace to add (or `None` to skip) and the trace to recurse into (or `None` to skip).
157fn build_trace(
158    env: &mut Environment,
159    address: &[i64],
160    trace: ExecutionTrace,
161) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
162    // This function first assumes that the call is a "native" call, then handles all the "not
163    // native" cases. If we get any unexpected results in any of these special cases, we just
164    // keep the "native" interpretation and move on.
165    //
166    // 1. If we're invoking a contract (even if the caller is a native account/actor), we
167    //    attempt to decode the params/return value as a contract invocation.
168    // 2. If we're calling the EAM and/or init actor, we try to treat the call as a CREATE.
169    // 3. Finally, if the caller is an EVM smart contract and it's calling a "private" (1-1023)
170    //    method, we know something special is going on. We look for calls related to
171    //    DELEGATECALL and drop everything else (everything else includes calls triggered by,
172    //    e.g., EXTCODEHASH).
173
174    // If we don't have sufficient funds, or we have a fatal error, or we have some
175    // other syscall error: skip the entire trace to mimic Ethereum (Ethereum records
176    // traces _after_ checking things like this).
177    //
178    // NOTE: The FFI currently folds all unknown syscall errors into "sys assertion
179    // failed" which is turned into SysErrFatal.
180    if !address.is_empty()
181        && Into::<ExitCode>::into(trace.msg_rct.exit_code) == ExitCode::SYS_INSUFFICIENT_FUNDS
182    {
183        return Ok((None, None));
184    }
185
186    // We may fail before we can even invoke the actor. In that case, we have no 100% reliable
187    // way of getting its address (e.g., due to reverts) so we're just going to drop the entire
188    // trace. This is OK (ish) because the call never really "happened".
189    if trace.invoked_actor.is_none() {
190        return Ok((None, None));
191    }
192
193    // Step 2: Decode as a contract invocation
194    //
195    // Normal EVM calls. We don't care if the caller/receiver are actually EVM actors, we only
196    // care if the call _looks_ like an EVM call. If we fail to decode it as an EVM call, we
197    // fallback on interpreting it as a native call.
198    let method = EVMMethod::from_u64(trace.msg.method);
199    if let Some(EVMMethod::InvokeContract) = method {
200        let (trace, exec_trace) = trace_evm_call(env, address, trace)?;
201        return Ok((Some(trace), Some(exec_trace)));
202    }
203
204    // Step 3: Decode as a contract deployment
205    match trace.msg.to {
206        Address::INIT_ACTOR => {
207            let method = InitMethod::from_u64(trace.msg.method);
208            match method {
209                Some(InitMethod::Exec) | Some(InitMethod::Exec4) => {
210                    return trace_native_create(env, address, &trace);
211                }
212                _ => (),
213            }
214        }
215        Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR => {
216            let method = EAMMethod::from_u64(trace.msg.method);
217            match method {
218                Some(EAMMethod::Create)
219                | Some(EAMMethod::Create2)
220                | Some(EAMMethod::CreateExternal) => {
221                    return trace_eth_create(env, address, &trace);
222                }
223                _ => (),
224            }
225        }
226        _ => (),
227    }
228
229    // Step 4: Handle DELEGATECALL
230    //
231    // EVM contracts cannot call methods in the range 1-1023, only the EVM itself can. So, if we
232    // see a call in this range, we know it's an implementation detail of the EVM and not an
233    // explicit call by the user.
234    //
235    // While the EVM calls several methods in this range (some we've already handled above with
236    // respect to the EAM), we only care about the ones relevant DELEGATECALL and can _ignore_
237    // all the others.
238    if env.is_evm && trace.msg.method > 0 && trace.msg.method < 1024 {
239        return trace_evm_private(env, address, &trace);
240    }
241
242    Ok((Some(trace_native_call(env, address, &trace)?), Some(trace)))
243}
244
245// Build an EthTrace for a "call" with the given input & output.
246fn trace_call(
247    env: &mut Environment,
248    address: &[i64],
249    trace: &ExecutionTrace,
250    input: EthBytes,
251    output: EthBytes,
252) -> anyhow::Result<EthTrace> {
253    if let Some(invoked_actor) = &trace.invoked_actor {
254        let to = trace_to_address(invoked_actor);
255        let call_type: String = if trace.msg.read_only.unwrap_or_default() {
256            "staticcall"
257        } else {
258            "call"
259        }
260        .into();
261
262        Ok(EthTrace {
263            r#type: "call".into(),
264            action: TraceAction::Call(EthCallTraceAction {
265                call_type,
266                from: env.caller.clone(),
267                to: Some(to),
268                gas: trace.msg.gas_limit.unwrap_or_default().into(),
269                value: trace.msg.value.clone().into(),
270                input,
271            }),
272            result: TraceResult::Call(EthCallTraceResult {
273                gas_used: trace.sum_gas().total_gas.into(),
274                output,
275            }),
276            trace_address: Vec::from(address),
277            error: trace_err_msg(trace),
278            ..EthTrace::default()
279        })
280    } else {
281        bail!("no invoked actor")
282    }
283}
284
285// Build an EthTrace for a "call", parsing the inputs & outputs as a "native" FVM call.
286fn trace_native_call(
287    env: &mut Environment,
288    address: &[i64],
289    trace: &ExecutionTrace,
290) -> anyhow::Result<EthTrace> {
291    trace_call(
292        env,
293        address,
294        trace,
295        encode_filecoin_params_as_abi(trace.msg.method, trace.msg.params_codec, &trace.msg.params)?,
296        EthBytes(encode_filecoin_returns_as_abi(
297            trace.msg_rct.exit_code.value().into(),
298            trace.msg_rct.return_codec,
299            &trace.msg_rct.r#return,
300        )),
301    )
302}
303
304// Build an EthTrace for a "call", parsing the inputs & outputs as an EVM call (falling back on
305// treating it as a native call).
306fn trace_evm_call(
307    env: &mut Environment,
308    address: &[i64],
309    trace: ExecutionTrace,
310) -> anyhow::Result<(EthTrace, ExecutionTrace)> {
311    let input = match decode_payload(&trace.msg.params, trace.msg.params_codec) {
312        Ok(value) => value,
313        Err(err) => {
314            debug!("failed to decode contract invocation payload: {err}");
315            return Ok((trace_native_call(env, address, &trace)?, trace));
316        }
317    };
318    let output = match decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec) {
319        Ok(value) => value,
320        Err(err) => {
321            debug!("failed to decode contract invocation return: {err}");
322            return Ok((trace_native_call(env, address, &trace)?, trace));
323        }
324    };
325    Ok((trace_call(env, address, &trace, input, output)?, trace))
326}
327
328// Build an EthTrace for a native "create" operation. This should only be called with an
329// ExecutionTrace is an Exec or Exec4 method invocation on the Init actor.
330
331fn trace_native_create(
332    env: &mut Environment,
333    address: &[i64],
334    trace: &ExecutionTrace,
335) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
336    if trace.msg.read_only.unwrap_or_default() {
337        // "create" isn't valid in a staticcall, so we just skip this trace
338        // (couldn't have created an actor anyways).
339        // This mimic's the EVM: it doesn't trace CREATE calls when in
340        // read-only mode.
341        return Ok((None, None));
342    }
343
344    let sub_trace = trace
345        .subcalls
346        .iter()
347        .find(|c| c.msg.method == METHOD_CONSTRUCTOR);
348
349    let sub_trace = if let Some(sub_trace) = sub_trace {
350        sub_trace
351    } else {
352        // If we succeed in calling Exec/Exec4 but don't even try to construct
353        // something, we have a bug in our tracing logic or a mismatch between our
354        // tracing logic and the actors.
355        if trace.msg_rct.exit_code.is_success() {
356            bail!("successful Exec/Exec4 call failed to call a constructor");
357        }
358        // Otherwise, this can happen if creation fails early (bad params,
359        // out of gas, contract already exists, etc.). The EVM wouldn't
360        // trace such cases, so we don't either.
361        //
362        // NOTE: It's actually impossible to run out of gas before calling
363        // initcode in the EVM (without running out of gas in the calling
364        // contract), but this is an equivalent edge-case to InvokedActor
365        // being nil, so we treat it the same way and skip the entire
366        // operation.
367        return Ok((None, None));
368    };
369
370    // Native actors that aren't the EAM can attempt to call Exec4, but such
371    // call should fail immediately without ever attempting to construct an
372    // actor. I'm catching this here because it likely means that there's a bug
373    // in our trace-conversion logic.
374    if trace.msg.method == (InitMethod::Exec4 as u64) {
375        bail!("direct call to Exec4 successfully called a constructor!");
376    }
377
378    let mut output = EthBytes::default();
379    let mut create_addr = EthAddress::default();
380    if trace.msg_rct.exit_code.is_success() {
381        // We're supposed to put the "installed bytecode" here. But this
382        // isn't an EVM actor, so we just put some invalid bytecode (this is
383        // the answer you'd get if you called EXTCODECOPY on a native
384        // non-account actor, anyways).
385        output = EthBytes(vec![0xFE]);
386
387        // Extract the address of the created actor from the return value.
388        let init_return: ExecReturn = decode_return(&trace.msg_rct)?;
389        let actor_id = init_return.id_address.id()?;
390        let eth_addr = EthAddress::from_actor_id(actor_id);
391        create_addr = eth_addr;
392    }
393
394    Ok((
395        Some(EthTrace {
396            r#type: "create".into(),
397            action: TraceAction::Create(EthCreateTraceAction {
398                from: env.caller.clone(),
399                gas: trace.msg.gas_limit.unwrap_or_default().into(),
400                value: trace.msg.value.clone().into(),
401                // If we get here, this isn't a native EVM create. Those always go through
402                // the EAM. So we have no "real" initcode and must use the sentinel value
403                // for "invalid" initcode.
404                init: EthBytes(vec![0xFE]),
405            }),
406            result: TraceResult::Create(EthCreateTraceResult {
407                gas_used: trace.sum_gas().total_gas.into(),
408                address: Some(create_addr),
409                code: output,
410            }),
411            trace_address: Vec::from(address),
412            error: trace_err_msg(trace),
413            ..EthTrace::default()
414        }),
415        Some(sub_trace.clone()),
416    ))
417}
418
419// Decode the parameters and return value of an EVM smart contract creation through the EAM. This
420// should only be called with an ExecutionTrace for a Create, Create2, or CreateExternal method
421// invocation on the EAM.
422fn decode_create_via_eam(trace: &ExecutionTrace) -> anyhow::Result<(Vec<u8>, EthAddress)> {
423    let init_code = match EAMMethod::from_u64(trace.msg.method) {
424        Some(EAMMethod::Create) => {
425            let params = decode_params::<eam12::CreateParams>(&trace.msg)?;
426            params.initcode
427        }
428        Some(EAMMethod::Create2) => {
429            let params = decode_params::<eam12::Create2Params>(&trace.msg)?;
430            params.initcode
431        }
432        Some(EAMMethod::CreateExternal) => {
433            decode_payload(&trace.msg.params, trace.msg.params_codec)?.into()
434        }
435        _ => bail!("unexpected CREATE method {}", trace.msg.method),
436    };
437    let ret = decode_return::<eam12::CreateReturn>(&trace.msg_rct)?;
438
439    Ok((init_code, ret.eth_address.0.into()))
440}
441
442// Build an EthTrace for an EVM "create" operation. This should only be called with an
443// ExecutionTrace for a Create, Create2, or CreateExternal method invocation on the EAM.
444fn trace_eth_create(
445    env: &mut Environment,
446    address: &[i64],
447    trace: &ExecutionTrace,
448) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
449    // Same as the Init actor case above, see the comment there.
450    if trace.msg.read_only.unwrap_or_default() {
451        return Ok((None, None));
452    }
453
454    // Look for a call to either a constructor or the EVM's resurrect method.
455    let sub_trace = trace
456        .subcalls
457        .iter()
458        .filter_map(|et| {
459            if et.msg.to == Address::INIT_ACTOR {
460                et.subcalls
461                    .iter()
462                    .find(|et| et.msg.method == METHOD_CONSTRUCTOR)
463            } else {
464                match EVMMethod::from_u64(et.msg.method) {
465                    Some(EVMMethod::Resurrect) => Some(et),
466                    _ => None,
467                }
468            }
469        })
470        .next();
471
472    // Same as the Init actor case above, see the comment there.
473    let sub_trace = if let Some(sub_trace) = sub_trace {
474        sub_trace
475    } else {
476        if trace.msg_rct.exit_code.is_success() {
477            bail!("successful Create/Create2 call failed to call a constructor");
478        }
479        return Ok((None, None));
480    };
481
482    // Decode inputs & determine create type.
483    let (init_code, create_addr) = decode_create_via_eam(trace)?;
484
485    // Handle the output.
486    let output = match trace.msg_rct.exit_code.value() {
487        0 => {
488            // success
489            // We're _supposed_ to include the contracts bytecode here, but we
490            // can't do that reliably (e.g., if some part of the trace reverts).
491            // So we don't try and include a sentinel "impossible bytecode"
492            // value (the value specified by EIP-3541).
493            EthBytes(vec![0xFE])
494        }
495        33 => {
496            // Reverted, parse the revert message.
497            // If we managed to call the constructor, parse/return its revert message. If we
498            // fail, we just return no output.
499            decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec)?
500        }
501        _ => EthBytes::default(),
502    };
503
504    Ok((
505        Some(EthTrace {
506            r#type: "create".into(),
507            action: TraceAction::Create(EthCreateTraceAction {
508                from: env.caller.clone(),
509                gas: trace.msg.gas_limit.unwrap_or_default().into(),
510                value: trace.msg.value.clone().into(),
511                init: init_code.into(),
512            }),
513            result: TraceResult::Create(EthCreateTraceResult {
514                gas_used: trace.sum_gas().total_gas.into(),
515                address: Some(create_addr),
516                code: output,
517            }),
518            trace_address: Vec::from(address),
519            error: trace_err_msg(trace),
520            ..EthTrace::default()
521        }),
522        Some(sub_trace.clone()),
523    ))
524}
525
526// Build an EthTrace for a "private" method invocation from the EVM. This should only be called with
527// an ExecutionTrace from an EVM instance and on a method between 1 and 1023 inclusive.
528fn trace_evm_private(
529    env: &mut Environment,
530    address: &[i64],
531    trace: &ExecutionTrace,
532) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
533    // The EVM actor implements DELEGATECALL by:
534    //
535    // 1. Asking the callee for its bytecode by calling it on the GetBytecode method.
536    // 2. Recursively invoking the currently executing contract on the
537    //    InvokeContractDelegate method.
538    //
539    // The code below "reconstructs" that delegate call by:
540    //
541    // 1. Remembering the last contract on which we called GetBytecode.
542    // 2. Treating the contract invoked in step 1 as the DELEGATECALL receiver.
543    //
544    // Note, however: GetBytecode will be called, e.g., if the user invokes the
545    // EXTCODECOPY instruction. It's not an error to see multiple GetBytecode calls
546    // before we see an InvokeContractDelegate.
547    match EVMMethod::from_u64(trace.msg.method) {
548        Some(EVMMethod::GetBytecode) => {
549            // NOTE: I'm not checking anything about the receiver here. The EVM won't
550            // DELEGATECALL any non-EVM actor, but there's no need to encode that fact
551            // here in case we decide to loosen this up in the future.
552            env.last_byte_code = None;
553            if trace.msg_rct.exit_code.is_success()
554                && let Option::Some(actor_trace) = &trace.invoked_actor
555            {
556                let to = trace_to_address(actor_trace);
557                env.last_byte_code = Some(to);
558            }
559            Ok((None, None))
560        }
561        Some(EVMMethod::InvokeContractDelegate) => {
562            // NOTE: We return errors in all the failure cases below instead of trying
563            // to continue because the caller is an EVM actor. If something goes wrong
564            // here, there's a bug in our EVM implementation.
565
566            // Handle delegate calls
567            //
568            // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate,
569            //    method 6.
570            // 2) Check that the previous trace calls another actor on method 3
571            //    (GetByteCode) and they are at the same level (same parent)
572            // 3) Treat this as a delegate call to actor A.
573            if env.last_byte_code.is_none() {
574                bail!("unknown bytecode for delegate call");
575            }
576
577            if let Option::Some(actor_trace) = &trace.invoked_actor {
578                let to = trace_to_address(actor_trace);
579                if env.caller != to {
580                    bail!(
581                        "delegate-call not from address to self: {:?} != {:?}",
582                        env.caller,
583                        to
584                    );
585                }
586            }
587
588            let dp = decode_params::<evm12::DelegateCallParams>(&trace.msg)?;
589
590            let output = decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec)
591                .map_err(|e| anyhow::anyhow!("failed to decode delegate-call return: {}", e))?;
592
593            Ok((
594                Some(EthTrace {
595                    r#type: "call".into(),
596                    action: TraceAction::Call(EthCallTraceAction {
597                        call_type: "delegatecall".into(),
598                        from: env.caller.clone(),
599                        to: env.last_byte_code.clone(),
600                        gas: trace.msg.gas_limit.unwrap_or_default().into(),
601                        value: trace.msg.value.clone().into(),
602                        input: dp.input.into(),
603                    }),
604                    result: TraceResult::Call(EthCallTraceResult {
605                        gas_used: trace.sum_gas().total_gas.into(),
606                        output,
607                    }),
608                    trace_address: Vec::from(address),
609                    error: trace_err_msg(trace),
610                    ..EthTrace::default()
611                }),
612                Some(trace.clone()),
613            ))
614        }
615        _ => {
616            // We drop all other "private" calls from FEVM. We _forbid_ explicit calls between 0 and
617            // 1024 (exclusive), so any calls in this range must be implementation details.
618            Ok((None, None))
619        }
620    }
621}