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::{
5    EthAddress, EthBytes, EthCallTraceAction, EthHash, EthTrace, TraceAction, TraceResult,
6};
7use super::utils::{decode_params, decode_return};
8use super::{
9    EthCallTraceResult, EthCreateTraceAction, EthCreateTraceResult, decode_payload,
10    encode_filecoin_params_as_abi, encode_filecoin_returns_as_abi,
11};
12use crate::eth::{EAMMethod, EVMMethod};
13use crate::rpc::eth::types::{AccountDiff, Delta, StateDiff};
14use crate::rpc::eth::{EthBigInt, EthUint64};
15use crate::rpc::methods::eth::lookup_eth_address;
16use crate::rpc::methods::state::ExecutionTrace;
17use crate::rpc::state::ActorTrace;
18use crate::shim::actors::{EVMActorStateLoad, evm};
19use crate::shim::fvm_shared_latest::METHOD_CONSTRUCTOR;
20use crate::shim::state_tree::ActorState;
21use crate::shim::{actors::is_evm_actor, address::Address, error::ExitCode, state_tree::StateTree};
22use ahash::{HashMap, HashSet};
23use anyhow::{Context, bail};
24use fil_actor_eam_state::v12 as eam12;
25use fil_actor_evm_state::evm_shared::v17::uints::U256;
26use fil_actor_evm_state::v15 as evm12;
27use fil_actor_init_state::v12::ExecReturn;
28use fil_actor_init_state::v15::Method as InitMethod;
29use fvm_ipld_blockstore::Blockstore;
30use fvm_ipld_kamt::{AsHashedKey, Config as KamtConfig, HashedKey, Kamt};
31use num::FromPrimitive;
32use std::borrow::Cow;
33use std::collections::BTreeMap;
34use tracing::debug;
35
36/// KAMT configuration matching the EVM actor in builtin-actors.
37// Code is taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L47
38fn evm_kamt_config() -> KamtConfig {
39    KamtConfig {
40        bit_width: 5,       // 32 children per node (2^5)
41        min_data_depth: 0,  // Data can be stored at root level
42        max_array_width: 1, // Max 1 key-value pair per bucket
43    }
44}
45
46/// Hash algorithm for EVM storage KAMT.
47// Code taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L49.
48pub struct EvmStateHashAlgorithm;
49
50impl AsHashedKey<U256, 32> for EvmStateHashAlgorithm {
51    fn as_hashed_key(key: &U256) -> Cow<'_, HashedKey<32>> {
52        Cow::Owned(key.to_big_endian())
53    }
54}
55
56/// Type alias for EVM storage KAMT with configuration.
57type EvmStorageKamt<BS> = Kamt<BS, U256, U256, EvmStateHashAlgorithm>;
58
59fn u256_to_eth_hash(value: &U256) -> EthHash {
60    EthHash(ethereum_types::H256(value.to_big_endian()))
61}
62
63const ZERO_HASH: EthHash = EthHash(ethereum_types::H256([0u8; 32]));
64
65#[derive(Default)]
66pub struct Environment {
67    caller: EthAddress,
68    is_evm: bool,
69    subtrace_count: i64,
70    pub traces: Vec<EthTrace>,
71    last_byte_code: Option<EthAddress>,
72}
73
74pub fn base_environment<BS: Blockstore + Send + Sync>(
75    state: &StateTree<BS>,
76    from: &Address,
77) -> anyhow::Result<Environment> {
78    let sender = lookup_eth_address(from, state)?
79        .with_context(|| format!("top-level message sender {from} s could not be found"))?;
80    Ok(Environment {
81        caller: sender,
82        ..Environment::default()
83    })
84}
85
86fn trace_to_address(trace: &ActorTrace) -> EthAddress {
87    if let Some(addr) = trace.state.delegated_address
88        && let Ok(eth_addr) = EthAddress::from_filecoin_address(&addr.into())
89    {
90        return eth_addr;
91    }
92    EthAddress::from_actor_id(trace.id)
93}
94
95/// Returns true if the trace is a call to an EVM or EAM actor.
96fn trace_is_evm_or_eam(trace: &ExecutionTrace) -> bool {
97    if let Some(invoked_actor) = &trace.invoked_actor {
98        is_evm_actor(&invoked_actor.state.code)
99            || invoked_actor.id != Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap()
100    } else {
101        false
102    }
103}
104
105/// Returns true if the trace is a call to an EVM or EAM actor.
106fn trace_err_msg(trace: &ExecutionTrace) -> Option<String> {
107    let code = trace.msg_rct.exit_code;
108
109    if code.is_success() {
110        return None;
111    }
112
113    // EVM tools often expect this literal string.
114    if code == ExitCode::SYS_OUT_OF_GAS {
115        return Some("out of gas".into());
116    }
117
118    // indicate when we have a "system" error.
119    if code < ExitCode::FIRST_ACTOR_ERROR_CODE.into() {
120        return Some(format!("vm error: {code}"));
121    }
122
123    // handle special exit codes from the EVM/EAM.
124    if trace_is_evm_or_eam(trace) {
125        match code.into() {
126            evm12::EVM_CONTRACT_REVERTED => return Some("Reverted".into()), // capitalized for compatibility
127            evm12::EVM_CONTRACT_INVALID_INSTRUCTION => return Some("invalid instruction".into()),
128            evm12::EVM_CONTRACT_UNDEFINED_INSTRUCTION => {
129                return Some("undefined instruction".into());
130            }
131            evm12::EVM_CONTRACT_STACK_UNDERFLOW => return Some("stack underflow".into()),
132            evm12::EVM_CONTRACT_STACK_OVERFLOW => return Some("stack overflow".into()),
133            evm12::EVM_CONTRACT_ILLEGAL_MEMORY_ACCESS => {
134                return Some("illegal memory access".into());
135            }
136            evm12::EVM_CONTRACT_BAD_JUMPDEST => return Some("invalid jump destination".into()),
137            evm12::EVM_CONTRACT_SELFDESTRUCT_FAILED => return Some("self destruct failed".into()),
138            _ => (),
139        }
140    }
141    // everything else...
142    Some(format!("actor error: {code}"))
143}
144
145/// Recursively builds the traces for a given ExecutionTrace by walking the subcalls
146pub fn build_traces(
147    env: &mut Environment,
148    address: &[i64],
149    trace: ExecutionTrace,
150) -> anyhow::Result<()> {
151    let (trace, recurse_into) = build_trace(env, address, trace)?;
152
153    let last_trace_idx = if let Some(trace) = trace {
154        let len = env.traces.len();
155        env.traces.push(trace);
156        env.subtrace_count += 1;
157        Some(len)
158    } else {
159        None
160    };
161
162    // Skip if there's nothing more to do and/or `build_trace` told us to skip this one.
163    let (recurse_into, invoked_actor) = if let Some(trace) = recurse_into {
164        if let Some(invoked_actor) = &trace.invoked_actor {
165            let invoked_actor = invoked_actor.clone();
166            (trace, invoked_actor)
167        } else {
168            return Ok(());
169        }
170    } else {
171        return Ok(());
172    };
173
174    let mut sub_env = Environment {
175        caller: trace_to_address(&invoked_actor),
176        is_evm: is_evm_actor(&invoked_actor.state.code),
177        traces: env.traces.clone(),
178        ..Environment::default()
179    };
180    for subcall in recurse_into.subcalls.into_iter() {
181        let mut new_address = address.to_vec();
182        new_address.push(sub_env.subtrace_count);
183        build_traces(&mut sub_env, &new_address, subcall)?;
184    }
185    env.traces = sub_env.traces;
186    if let Some(idx) = last_trace_idx {
187        env.traces.get_mut(idx).expect("Infallible").subtraces = sub_env.subtrace_count;
188    }
189
190    Ok(())
191}
192
193// `build_trace` processes the passed execution trace and updates the environment, if necessary.
194//
195// On success, it returns a trace to add (or `None` to skip) and the trace to recurse into (or `None` to skip).
196fn build_trace(
197    env: &mut Environment,
198    address: &[i64],
199    trace: ExecutionTrace,
200) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
201    // This function first assumes that the call is a "native" call, then handles all the "not
202    // native" cases. If we get any unexpected results in any of these special cases, we just
203    // keep the "native" interpretation and move on.
204    //
205    // 1. If we're invoking a contract (even if the caller is a native account/actor), we
206    //    attempt to decode the params/return value as a contract invocation.
207    // 2. If we're calling the EAM and/or init actor, we try to treat the call as a CREATE.
208    // 3. Finally, if the caller is an EVM smart contract and it's calling a "private" (1-1023)
209    //    method, we know something special is going on. We look for calls related to
210    //    DELEGATECALL and drop everything else (everything else includes calls triggered by,
211    //    e.g., EXTCODEHASH).
212
213    // If we don't have sufficient funds, or we have a fatal error, or we have some
214    // other syscall error: skip the entire trace to mimic Ethereum (Ethereum records
215    // traces _after_ checking things like this).
216    //
217    // NOTE: The FFI currently folds all unknown syscall errors into "sys assertion
218    // failed" which is turned into SysErrFatal.
219    if !address.is_empty()
220        && Into::<ExitCode>::into(trace.msg_rct.exit_code) == ExitCode::SYS_INSUFFICIENT_FUNDS
221    {
222        return Ok((None, None));
223    }
224
225    // We may fail before we can even invoke the actor. In that case, we have no 100% reliable
226    // way of getting its address (e.g., due to reverts) so we're just going to drop the entire
227    // trace. This is OK (ish) because the call never really "happened".
228    if trace.invoked_actor.is_none() {
229        return Ok((None, None));
230    }
231
232    // Step 2: Decode as a contract invocation
233    //
234    // Normal EVM calls. We don't care if the caller/receiver are actually EVM actors, we only
235    // care if the call _looks_ like an EVM call. If we fail to decode it as an EVM call, we
236    // fallback on interpreting it as a native call.
237    let method = EVMMethod::from_u64(trace.msg.method);
238    if let Some(EVMMethod::InvokeContract) = method {
239        let (trace, exec_trace) = trace_evm_call(env, address, trace)?;
240        return Ok((Some(trace), Some(exec_trace)));
241    }
242
243    // Step 3: Decode as a contract deployment
244    match trace.msg.to {
245        Address::INIT_ACTOR => {
246            let method = InitMethod::from_u64(trace.msg.method);
247            match method {
248                Some(InitMethod::Exec) | Some(InitMethod::Exec4) => {
249                    return trace_native_create(env, address, &trace);
250                }
251                _ => (),
252            }
253        }
254        Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR => {
255            let method = EAMMethod::from_u64(trace.msg.method);
256            match method {
257                Some(EAMMethod::Create)
258                | Some(EAMMethod::Create2)
259                | Some(EAMMethod::CreateExternal) => {
260                    return trace_eth_create(env, address, &trace);
261                }
262                _ => (),
263            }
264        }
265        _ => (),
266    }
267
268    // Step 4: Handle DELEGATECALL
269    //
270    // EVM contracts cannot call methods in the range 1-1023, only the EVM itself can. So, if we
271    // see a call in this range, we know it's an implementation detail of the EVM and not an
272    // explicit call by the user.
273    //
274    // While the EVM calls several methods in this range (some we've already handled above with
275    // respect to the EAM), we only care about the ones relevant DELEGATECALL and can _ignore_
276    // all the others.
277    if env.is_evm && trace.msg.method > 0 && trace.msg.method < 1024 {
278        return trace_evm_private(env, address, &trace);
279    }
280
281    Ok((Some(trace_native_call(env, address, &trace)?), Some(trace)))
282}
283
284// Build an EthTrace for a "call" with the given input & output.
285fn trace_call(
286    env: &mut Environment,
287    address: &[i64],
288    trace: &ExecutionTrace,
289    input: EthBytes,
290    output: EthBytes,
291) -> anyhow::Result<EthTrace> {
292    if let Some(invoked_actor) = &trace.invoked_actor {
293        let to = trace_to_address(invoked_actor);
294        let call_type: String = if trace.msg.read_only.unwrap_or_default() {
295            "staticcall"
296        } else {
297            "call"
298        }
299        .into();
300
301        Ok(EthTrace {
302            r#type: "call".into(),
303            action: TraceAction::Call(EthCallTraceAction {
304                call_type,
305                from: env.caller,
306                to: Some(to),
307                gas: trace.msg.gas_limit.unwrap_or_default().into(),
308                value: trace.msg.value.clone().into(),
309                input,
310            }),
311            result: TraceResult::Call(EthCallTraceResult {
312                gas_used: trace.sum_gas().total_gas.into(),
313                output,
314            }),
315            trace_address: Vec::from(address),
316            error: trace_err_msg(trace),
317            ..EthTrace::default()
318        })
319    } else {
320        bail!("no invoked actor")
321    }
322}
323
324// Build an EthTrace for a "call", parsing the inputs & outputs as a "native" FVM call.
325fn trace_native_call(
326    env: &mut Environment,
327    address: &[i64],
328    trace: &ExecutionTrace,
329) -> anyhow::Result<EthTrace> {
330    trace_call(
331        env,
332        address,
333        trace,
334        encode_filecoin_params_as_abi(trace.msg.method, trace.msg.params_codec, &trace.msg.params)?,
335        EthBytes(encode_filecoin_returns_as_abi(
336            trace.msg_rct.exit_code.value().into(),
337            trace.msg_rct.return_codec,
338            &trace.msg_rct.r#return,
339        )),
340    )
341}
342
343// Build an EthTrace for a "call", parsing the inputs & outputs as an EVM call (falling back on
344// treating it as a native call).
345fn trace_evm_call(
346    env: &mut Environment,
347    address: &[i64],
348    trace: ExecutionTrace,
349) -> anyhow::Result<(EthTrace, ExecutionTrace)> {
350    let input = match decode_payload(&trace.msg.params, trace.msg.params_codec) {
351        Ok(value) => value,
352        Err(err) => {
353            debug!("failed to decode contract invocation payload: {err}");
354            return Ok((trace_native_call(env, address, &trace)?, trace));
355        }
356    };
357    let output = match decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec) {
358        Ok(value) => value,
359        Err(err) => {
360            debug!("failed to decode contract invocation return: {err}");
361            return Ok((trace_native_call(env, address, &trace)?, trace));
362        }
363    };
364    Ok((trace_call(env, address, &trace, input, output)?, trace))
365}
366
367// Build an EthTrace for a native "create" operation. This should only be called with an
368// ExecutionTrace is an Exec or Exec4 method invocation on the Init actor.
369
370fn trace_native_create(
371    env: &mut Environment,
372    address: &[i64],
373    trace: &ExecutionTrace,
374) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
375    if trace.msg.read_only.unwrap_or_default() {
376        // "create" isn't valid in a staticcall, so we just skip this trace
377        // (couldn't have created an actor anyways).
378        // This mimic's the EVM: it doesn't trace CREATE calls when in
379        // read-only mode.
380        return Ok((None, None));
381    }
382
383    let sub_trace = trace
384        .subcalls
385        .iter()
386        .find(|c| c.msg.method == METHOD_CONSTRUCTOR);
387
388    let sub_trace = if let Some(sub_trace) = sub_trace {
389        sub_trace
390    } else {
391        // If we succeed in calling Exec/Exec4 but don't even try to construct
392        // something, we have a bug in our tracing logic or a mismatch between our
393        // tracing logic and the actors.
394        if trace.msg_rct.exit_code.is_success() {
395            bail!("successful Exec/Exec4 call failed to call a constructor");
396        }
397        // Otherwise, this can happen if creation fails early (bad params,
398        // out of gas, contract already exists, etc.). The EVM wouldn't
399        // trace such cases, so we don't either.
400        //
401        // NOTE: It's actually impossible to run out of gas before calling
402        // initcode in the EVM (without running out of gas in the calling
403        // contract), but this is an equivalent edge-case to InvokedActor
404        // being nil, so we treat it the same way and skip the entire
405        // operation.
406        return Ok((None, None));
407    };
408
409    // Native actors that aren't the EAM can attempt to call Exec4, but such
410    // call should fail immediately without ever attempting to construct an
411    // actor. I'm catching this here because it likely means that there's a bug
412    // in our trace-conversion logic.
413    if trace.msg.method == (InitMethod::Exec4 as u64) {
414        bail!("direct call to Exec4 successfully called a constructor!");
415    }
416
417    let mut output = EthBytes::default();
418    let mut create_addr = EthAddress::default();
419    if trace.msg_rct.exit_code.is_success() {
420        // We're supposed to put the "installed bytecode" here. But this
421        // isn't an EVM actor, so we just put some invalid bytecode (this is
422        // the answer you'd get if you called EXTCODECOPY on a native
423        // non-account actor, anyways).
424        output = EthBytes(vec![0xFE]);
425
426        // Extract the address of the created actor from the return value.
427        let init_return: ExecReturn = decode_return(&trace.msg_rct)?;
428        let actor_id = init_return.id_address.id()?;
429        let eth_addr = EthAddress::from_actor_id(actor_id);
430        create_addr = eth_addr;
431    }
432
433    Ok((
434        Some(EthTrace {
435            r#type: "create".into(),
436            action: TraceAction::Create(EthCreateTraceAction {
437                from: env.caller,
438                gas: trace.msg.gas_limit.unwrap_or_default().into(),
439                value: trace.msg.value.clone().into(),
440                // If we get here, this isn't a native EVM create. Those always go through
441                // the EAM. So we have no "real" initcode and must use the sentinel value
442                // for "invalid" initcode.
443                init: EthBytes(vec![0xFE]),
444            }),
445            result: TraceResult::Create(EthCreateTraceResult {
446                gas_used: trace.sum_gas().total_gas.into(),
447                address: Some(create_addr),
448                code: output,
449            }),
450            trace_address: Vec::from(address),
451            error: trace_err_msg(trace),
452            ..EthTrace::default()
453        }),
454        Some(sub_trace.clone()),
455    ))
456}
457
458// Decode the parameters and return value of an EVM smart contract creation through the EAM. This
459// should only be called with an ExecutionTrace for a Create, Create2, or CreateExternal method
460// invocation on the EAM.
461fn decode_create_via_eam(trace: &ExecutionTrace) -> anyhow::Result<(Vec<u8>, EthAddress)> {
462    let init_code = match EAMMethod::from_u64(trace.msg.method) {
463        Some(EAMMethod::Create) => {
464            let params = decode_params::<eam12::CreateParams>(&trace.msg)?;
465            params.initcode
466        }
467        Some(EAMMethod::Create2) => {
468            let params = decode_params::<eam12::Create2Params>(&trace.msg)?;
469            params.initcode
470        }
471        Some(EAMMethod::CreateExternal) => {
472            decode_payload(&trace.msg.params, trace.msg.params_codec)?.into()
473        }
474        _ => bail!("unexpected CREATE method {}", trace.msg.method),
475    };
476    let ret = decode_return::<eam12::CreateReturn>(&trace.msg_rct)?;
477
478    Ok((init_code, ret.eth_address.0.into()))
479}
480
481// Build an EthTrace for an EVM "create" operation. This should only be called with an
482// ExecutionTrace for a Create, Create2, or CreateExternal method invocation on the EAM.
483fn trace_eth_create(
484    env: &mut Environment,
485    address: &[i64],
486    trace: &ExecutionTrace,
487) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
488    // Same as the Init actor case above, see the comment there.
489    if trace.msg.read_only.unwrap_or_default() {
490        return Ok((None, None));
491    }
492
493    // Look for a call to either a constructor or the EVM's resurrect method.
494    let sub_trace = trace
495        .subcalls
496        .iter()
497        .filter_map(|et| {
498            if et.msg.to == Address::INIT_ACTOR {
499                et.subcalls
500                    .iter()
501                    .find(|et| et.msg.method == METHOD_CONSTRUCTOR)
502            } else {
503                match EVMMethod::from_u64(et.msg.method) {
504                    Some(EVMMethod::Resurrect) => Some(et),
505                    _ => None,
506                }
507            }
508        })
509        .next();
510
511    // Same as the Init actor case above, see the comment there.
512    let sub_trace = if let Some(sub_trace) = sub_trace {
513        sub_trace
514    } else {
515        if trace.msg_rct.exit_code.is_success() {
516            bail!("successful Create/Create2 call failed to call a constructor");
517        }
518        return Ok((None, None));
519    };
520
521    // Decode inputs & determine create type.
522    let (init_code, create_addr) = decode_create_via_eam(trace)?;
523
524    // Handle the output.
525    let output = match trace.msg_rct.exit_code.value() {
526        0 => {
527            // success
528            // We're _supposed_ to include the contracts bytecode here, but we
529            // can't do that reliably (e.g., if some part of the trace reverts).
530            // So we don't try and include a sentinel "impossible bytecode"
531            // value (the value specified by EIP-3541).
532            EthBytes(vec![0xFE])
533        }
534        33 => {
535            // Reverted, parse the revert message.
536            // If we managed to call the constructor, parse/return its revert message. If we
537            // fail, we just return no output.
538            decode_payload(&sub_trace.msg_rct.r#return, sub_trace.msg_rct.return_codec)?
539        }
540        _ => EthBytes::default(),
541    };
542
543    Ok((
544        Some(EthTrace {
545            r#type: "create".into(),
546            action: TraceAction::Create(EthCreateTraceAction {
547                from: env.caller,
548                gas: trace.msg.gas_limit.unwrap_or_default().into(),
549                value: trace.msg.value.clone().into(),
550                init: init_code.into(),
551            }),
552            result: TraceResult::Create(EthCreateTraceResult {
553                gas_used: trace.sum_gas().total_gas.into(),
554                address: Some(create_addr),
555                code: output,
556            }),
557            trace_address: Vec::from(address),
558            error: trace_err_msg(trace),
559            ..EthTrace::default()
560        }),
561        Some(sub_trace.clone()),
562    ))
563}
564
565// Build an EthTrace for a "private" method invocation from the EVM. This should only be called with
566// an ExecutionTrace from an EVM instance and on a method between 1 and 1023 inclusive.
567fn trace_evm_private(
568    env: &mut Environment,
569    address: &[i64],
570    trace: &ExecutionTrace,
571) -> anyhow::Result<(Option<EthTrace>, Option<ExecutionTrace>)> {
572    // The EVM actor implements DELEGATECALL by:
573    //
574    // 1. Asking the callee for its bytecode by calling it on the GetBytecode method.
575    // 2. Recursively invoking the currently executing contract on the
576    //    InvokeContractDelegate method.
577    //
578    // The code below "reconstructs" that delegate call by:
579    //
580    // 1. Remembering the last contract on which we called GetBytecode.
581    // 2. Treating the contract invoked in step 1 as the DELEGATECALL receiver.
582    //
583    // Note, however: GetBytecode will be called, e.g., if the user invokes the
584    // EXTCODECOPY instruction. It's not an error to see multiple GetBytecode calls
585    // before we see an InvokeContractDelegate.
586    match EVMMethod::from_u64(trace.msg.method) {
587        Some(EVMMethod::GetBytecode) => {
588            // NOTE: I'm not checking anything about the receiver here. The EVM won't
589            // DELEGATECALL any non-EVM actor, but there's no need to encode that fact
590            // here in case we decide to loosen this up in the future.
591            env.last_byte_code = None;
592            if trace.msg_rct.exit_code.is_success()
593                && let Option::Some(actor_trace) = &trace.invoked_actor
594            {
595                let to = trace_to_address(actor_trace);
596                env.last_byte_code = Some(to);
597            }
598            Ok((None, None))
599        }
600        Some(EVMMethod::InvokeContractDelegate) => {
601            // NOTE: We return errors in all the failure cases below instead of trying
602            // to continue because the caller is an EVM actor. If something goes wrong
603            // here, there's a bug in our EVM implementation.
604
605            // Handle delegate calls
606            //
607            // 1) Look for trace from an EVM actor to itself on InvokeContractDelegate,
608            //    method 6.
609            // 2) Check that the previous trace calls another actor on method 3
610            //    (GetByteCode) and they are at the same level (same parent)
611            // 3) Treat this as a delegate call to actor A.
612            if env.last_byte_code.is_none() {
613                bail!("unknown bytecode for delegate call");
614            }
615
616            if let Option::Some(actor_trace) = &trace.invoked_actor {
617                let to = trace_to_address(actor_trace);
618                if env.caller != to {
619                    bail!(
620                        "delegate-call not from address to self: {:?} != {:?}",
621                        env.caller,
622                        to
623                    );
624                }
625            }
626
627            let dp = decode_params::<evm12::DelegateCallParams>(&trace.msg)?;
628
629            let output = decode_payload(&trace.msg_rct.r#return, trace.msg_rct.return_codec)
630                .map_err(|e| anyhow::anyhow!("failed to decode delegate-call return: {}", e))?;
631
632            Ok((
633                Some(EthTrace {
634                    r#type: "call".into(),
635                    action: TraceAction::Call(EthCallTraceAction {
636                        call_type: "delegatecall".into(),
637                        from: env.caller,
638                        to: env.last_byte_code,
639                        gas: trace.msg.gas_limit.unwrap_or_default().into(),
640                        value: trace.msg.value.clone().into(),
641                        input: dp.input.into(),
642                    }),
643                    result: TraceResult::Call(EthCallTraceResult {
644                        gas_used: trace.sum_gas().total_gas.into(),
645                        output,
646                    }),
647                    trace_address: Vec::from(address),
648                    error: trace_err_msg(trace),
649                    ..EthTrace::default()
650                }),
651                Some(trace.clone()),
652            ))
653        }
654        _ => {
655            // We drop all other "private" calls from FEVM. We _forbid_ explicit calls between 0 and
656            // 1024 (exclusive), so any calls in this range must be implementation details.
657            Ok((None, None))
658        }
659    }
660}
661
662/// Build state diff by comparing pre and post-execution states for touched addresses.
663pub(crate) fn build_state_diff<S: Blockstore, T: Blockstore>(
664    store: &S,
665    pre_state: &StateTree<T>,
666    post_state: &StateTree<T>,
667    touched_addresses: &HashSet<EthAddress>,
668) -> anyhow::Result<StateDiff> {
669    let mut state_diff = StateDiff::new();
670
671    for eth_addr in touched_addresses {
672        let fil_addr = eth_addr.to_filecoin_address()?;
673
674        // Get actor state before and after
675        let pre_actor = pre_state
676            .get_actor(&fil_addr)
677            .map_err(|e| anyhow::anyhow!("failed to get actor state: {e}"))?;
678
679        let post_actor = post_state
680            .get_actor(&fil_addr)
681            .map_err(|e| anyhow::anyhow!("failed to get actor state: {e}"))?;
682
683        let account_diff = build_account_diff(store, pre_actor.as_ref(), post_actor.as_ref())?;
684
685        // Only include it if there were actual changes
686        state_diff.insert_if_changed(*eth_addr, account_diff);
687    }
688
689    Ok(state_diff)
690}
691
692/// Build account diff by comparing pre and post actor states.
693fn build_account_diff<DB: Blockstore>(
694    store: &DB,
695    pre_actor: Option<&ActorState>,
696    post_actor: Option<&ActorState>,
697) -> anyhow::Result<AccountDiff> {
698    let mut diff = AccountDiff::default();
699
700    // Compare balance
701    let pre_balance = pre_actor.map(|a| EthBigInt(a.balance.atto().clone()));
702    let post_balance = post_actor.map(|a| EthBigInt(a.balance.atto().clone()));
703    diff.balance = Delta::from_comparison(pre_balance, post_balance);
704
705    // Helper to get nonce from actor (uses EVM nonce for EVM actors)
706    let get_nonce = |actor: &ActorState| -> EthUint64 {
707        if is_evm_actor(&actor.code) {
708            EthUint64::from(
709                evm::State::load(store, actor.code, actor.state)
710                    .map(|s| s.nonce())
711                    .unwrap_or(actor.sequence),
712            )
713        } else {
714            EthUint64::from(actor.sequence)
715        }
716    };
717
718    // Helper to get bytecode from an EVM actor
719    let get_bytecode = |actor: &ActorState| -> Option<EthBytes> {
720        if !is_evm_actor(&actor.code) {
721            return None;
722        }
723
724        let evm_state = evm::State::load(store, actor.code, actor.state).ok()?;
725        store
726            .get(&evm_state.bytecode())
727            .ok()
728            .flatten()
729            .map(EthBytes)
730    };
731
732    // Compare nonce
733    let pre_nonce = pre_actor.map(get_nonce);
734    let post_nonce = post_actor.map(get_nonce);
735    diff.nonce = Delta::from_comparison(pre_nonce, post_nonce);
736
737    // Compare code (bytecode for EVM actors)
738    let pre_code = pre_actor.and_then(get_bytecode);
739    let post_code = post_actor.and_then(get_bytecode);
740    diff.code = Delta::from_comparison(pre_code, post_code);
741
742    // Compare storage slots for EVM actors
743    diff.storage = diff_evm_storage_for_actors(store, pre_actor, post_actor)?;
744
745    Ok(diff)
746}
747
748/// Compute storage diff between pre and post actor states.
749///
750/// Uses different Delta types based on the scenario:
751/// - Account created (None → EVM): storage slots are `Delta::Added`
752/// - Account deleted (EVM → None): storage slots are `Delta::Removed`
753/// - Account modified (EVM → EVM): storage slots are `Delta::Changed`
754/// - Actor type changed (EVM ↔ non-EVM): treated as deletion + creation
755fn diff_evm_storage_for_actors<DB: Blockstore>(
756    store: &DB,
757    pre_actor: Option<&ActorState>,
758    post_actor: Option<&ActorState>,
759) -> anyhow::Result<BTreeMap<EthHash, Delta<EthHash>>> {
760    let pre_is_evm = pre_actor.is_some_and(|a| is_evm_actor(&a.code));
761    let post_is_evm = post_actor.is_some_and(|a| is_evm_actor(&a.code));
762
763    // Extract storage entries from EVM actors (empty map for non-EVM or missing actors)
764    let pre_entries = extract_evm_storage_entries(store, pre_actor);
765    let post_entries = extract_evm_storage_entries(store, post_actor);
766
767    // If both are empty, no storage diff
768    if pre_entries.is_empty() && post_entries.is_empty() {
769        return Ok(BTreeMap::new());
770    }
771
772    let mut diff = BTreeMap::new();
773
774    match (pre_is_evm, post_is_evm) {
775        (false, true) => {
776            for (key_bytes, value) in &post_entries {
777                let key_hash = EthHash(ethereum_types::H256(*key_bytes));
778                diff.insert(key_hash, Delta::Added(u256_to_eth_hash(value)));
779            }
780        }
781        (true, false) => {
782            for (key_bytes, value) in &pre_entries {
783                let key_hash = EthHash(ethereum_types::H256(*key_bytes));
784                diff.insert(key_hash, Delta::Removed(u256_to_eth_hash(value)));
785            }
786        }
787        (true, true) => {
788            for (key_bytes, pre_value) in &pre_entries {
789                let key_hash = EthHash(ethereum_types::H256(*key_bytes));
790                let pre_hash = u256_to_eth_hash(pre_value);
791
792                match post_entries.get(key_bytes) {
793                    Some(post_value) if pre_value != post_value => {
794                        // Value changed
795                        diff.insert(
796                            key_hash,
797                            Delta::Changed(super::types::ChangedType {
798                                from: pre_hash,
799                                to: u256_to_eth_hash(post_value),
800                            }),
801                        );
802                    }
803                    Some(_) => {
804                        // Value unchanged, skip
805                    }
806                    None => {
807                        // Slot cleared (value → zero)
808                        diff.insert(
809                            key_hash,
810                            Delta::Changed(super::types::ChangedType {
811                                from: pre_hash,
812                                to: ZERO_HASH,
813                            }),
814                        );
815                    }
816                }
817            }
818
819            // Check for newly written entries (zero → value)
820            for (key_bytes, post_value) in &post_entries {
821                if !pre_entries.contains_key(key_bytes) {
822                    let key_hash = EthHash(ethereum_types::H256(*key_bytes));
823                    diff.insert(
824                        key_hash,
825                        Delta::Changed(super::types::ChangedType {
826                            from: ZERO_HASH,
827                            to: u256_to_eth_hash(post_value),
828                        }),
829                    );
830                }
831            }
832        }
833        // Neither EVM: no storage diff
834        (false, false) => {}
835    }
836
837    Ok(diff)
838}
839
840/// Extract all storage entries from an EVM actor's KAMT.
841/// Returns empty map if actor is None, not an EVM actor, or state cannot be loaded.
842fn extract_evm_storage_entries<DB: Blockstore>(
843    store: &DB,
844    actor: Option<&ActorState>,
845) -> HashMap<[u8; 32], U256> {
846    let actor = match actor {
847        Some(a) if is_evm_actor(&a.code) => a,
848        _ => return HashMap::default(),
849    };
850
851    let evm_state = match evm::State::load(store, actor.code, actor.state) {
852        Ok(state) => state,
853        Err(e) => {
854            debug!("failed to load EVM state for storage extraction: {e}");
855            return HashMap::default();
856        }
857    };
858
859    let storage_cid = evm_state.contract_state();
860    let config = evm_kamt_config();
861
862    let kamt: EvmStorageKamt<&DB> = match Kamt::load_with_config(&storage_cid, store, config) {
863        Ok(k) => k,
864        Err(e) => {
865            debug!("failed to load storage KAMT: {e}");
866            return HashMap::default();
867        }
868    };
869
870    let mut entries = HashMap::default();
871    if let Err(e) = kamt.for_each(|key, value| {
872        entries.insert(key.to_big_endian(), *value);
873        Ok(())
874    }) {
875        debug!("failed to iterate storage KAMT: {e}");
876        return HashMap::default();
877    }
878
879    entries
880}
881
882#[cfg(test)]
883mod tests {
884    use super::*;
885    use crate::db::MemoryDB;
886    use crate::networks::ACTOR_BUNDLES_METADATA;
887    use crate::rpc::eth::types::ChangedType;
888    use crate::shim::address::Address as FilecoinAddress;
889    use crate::shim::econ::TokenAmount;
890    use crate::shim::machine::BuiltinActor;
891    use crate::shim::state_tree::{StateTree, StateTreeVersion};
892    use crate::utils::db::CborStoreExt as _;
893    use ahash::HashSetExt as _;
894    use cid::Cid;
895    use num::BigInt;
896    use std::sync::Arc;
897
898    fn create_test_actor(balance_atto: u64, sequence: u64) -> ActorState {
899        ActorState::new(
900            Cid::default(), // Non-EVM actor code CID
901            Cid::default(), // State CID (not used for non-EVM)
902            TokenAmount::from_atto(balance_atto),
903            sequence,
904            None, // No delegated address
905        )
906    }
907
908    fn get_evm_actor_code_cid() -> Option<Cid> {
909        for bundle in ACTOR_BUNDLES_METADATA.values() {
910            if bundle.actor_major_version().ok() == Some(17)
911                && let Ok(cid) = bundle.manifest.get(BuiltinActor::EVM)
912            {
913                return Some(cid);
914            }
915        }
916        None
917    }
918
919    fn create_evm_actor_with_bytecode(
920        store: &MemoryDB,
921        balance_atto: u64,
922        actor_sequence: u64,
923        evm_nonce: u64,
924        bytecode: Option<&[u8]>,
925    ) -> Option<ActorState> {
926        use fvm_ipld_blockstore::Blockstore as _;
927
928        let evm_code_cid = get_evm_actor_code_cid()?;
929
930        // Store bytecode as raw bytes (not CBOR-encoded)
931        let bytecode_cid = if let Some(code) = bytecode {
932            use multihash_codetable::MultihashDigest;
933            let mh = multihash_codetable::Code::Blake2b256.digest(code);
934            let cid = Cid::new_v1(fvm_ipld_encoding::IPLD_RAW, mh);
935            store.put_keyed(&cid, code).ok()?;
936            cid
937        } else {
938            Cid::default()
939        };
940
941        let bytecode_hash = if let Some(code) = bytecode {
942            use keccak_hash::keccak;
943            let hash = keccak(code);
944            fil_actor_evm_state::v17::BytecodeHash::from(hash.0)
945        } else {
946            fil_actor_evm_state::v17::BytecodeHash::EMPTY
947        };
948
949        let evm_state = fil_actor_evm_state::v17::State {
950            bytecode: bytecode_cid,
951            bytecode_hash,
952            contract_state: Cid::default(),
953            transient_data: None,
954            nonce: evm_nonce,
955            tombstone: None,
956        };
957
958        let state_cid = store.put_cbor_default(&evm_state).ok()?;
959
960        Some(ActorState::new(
961            evm_code_cid,
962            state_cid,
963            TokenAmount::from_atto(balance_atto),
964            actor_sequence,
965            None,
966        ))
967    }
968
969    fn create_masked_id_eth_address(actor_id: u64) -> EthAddress {
970        EthAddress::from_actor_id(actor_id)
971    }
972
973    struct TestStateTrees {
974        store: Arc<MemoryDB>,
975        pre_state: StateTree<MemoryDB>,
976        post_state: StateTree<MemoryDB>,
977    }
978
979    impl TestStateTrees {
980        fn new() -> anyhow::Result<Self> {
981            let store = Arc::new(MemoryDB::default());
982            // Use V4 which creates FvmV2 state trees that allow direct set_actor
983            let pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
984            let post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
985            Ok(Self {
986                store,
987                pre_state,
988                post_state,
989            })
990        }
991
992        /// Create state trees with different actors in pre and post.
993        fn with_changed_actor(
994            actor_id: u64,
995            pre_actor: ActorState,
996            post_actor: ActorState,
997        ) -> anyhow::Result<Self> {
998            let store = Arc::new(MemoryDB::default());
999            let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
1000            let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
1001            let addr = FilecoinAddress::new_id(actor_id);
1002            pre_state.set_actor(&addr, pre_actor)?;
1003            post_state.set_actor(&addr, post_actor)?;
1004            Ok(Self {
1005                store,
1006                pre_state,
1007                post_state,
1008            })
1009        }
1010
1011        /// Create state trees with actor only in post (creation scenario).
1012        fn with_created_actor(actor_id: u64, post_actor: ActorState) -> anyhow::Result<Self> {
1013            let store = Arc::new(MemoryDB::default());
1014            let pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
1015            let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
1016            let addr = FilecoinAddress::new_id(actor_id);
1017            post_state.set_actor(&addr, post_actor)?;
1018            Ok(Self {
1019                store,
1020                pre_state,
1021                post_state,
1022            })
1023        }
1024
1025        /// Create state trees with actor only in pre (deletion scenario).
1026        fn with_deleted_actor(actor_id: u64, pre_actor: ActorState) -> anyhow::Result<Self> {
1027            let store = Arc::new(MemoryDB::default());
1028            let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
1029            let post_state = StateTree::new(store.clone(), StateTreeVersion::V5)?;
1030            let addr = FilecoinAddress::new_id(actor_id);
1031            pre_state.set_actor(&addr, pre_actor)?;
1032            Ok(Self {
1033                store,
1034                pre_state,
1035                post_state,
1036            })
1037        }
1038
1039        /// Build state diff for given touched addresses.
1040        fn build_diff(&self, touched_addresses: &HashSet<EthAddress>) -> anyhow::Result<StateDiff> {
1041            build_state_diff(
1042                self.store.as_ref(),
1043                &self.pre_state,
1044                &self.post_state,
1045                touched_addresses,
1046            )
1047        }
1048    }
1049
1050    #[test]
1051    fn test_build_state_diff_empty_touched_addresses() {
1052        let trees = TestStateTrees::new().unwrap();
1053        let touched_addresses = HashSet::new();
1054
1055        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1056
1057        // No addresses touched = empty state diff
1058        assert!(state_diff.0.is_empty());
1059    }
1060
1061    #[test]
1062    fn test_build_state_diff_nonexistent_address() {
1063        let trees = TestStateTrees::new().unwrap();
1064        let mut touched_addresses = HashSet::new();
1065        touched_addresses.insert(create_masked_id_eth_address(9999));
1066
1067        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1068
1069        // Address doesn't exist in either state, so no diff (both None = unchanged)
1070        assert!(state_diff.0.is_empty());
1071    }
1072
1073    #[test]
1074    fn test_build_state_diff_balance_increase() {
1075        let actor_id = 1001u64;
1076        let pre_actor = create_test_actor(1000, 5);
1077        let post_actor = create_test_actor(2000, 5);
1078        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
1079
1080        let mut touched_addresses = HashSet::new();
1081        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1082
1083        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1084
1085        assert_eq!(state_diff.0.len(), 1);
1086        let eth_addr = create_masked_id_eth_address(actor_id);
1087        let diff = state_diff.0.get(&eth_addr).unwrap();
1088        match &diff.balance {
1089            Delta::Changed(change) => {
1090                assert_eq!(change.from.0, BigInt::from(1000));
1091                assert_eq!(change.to.0, BigInt::from(2000));
1092            }
1093            _ => panic!("Expected Delta::Changed for balance"),
1094        }
1095        assert!(diff.nonce.is_unchanged());
1096    }
1097
1098    #[test]
1099    fn test_build_state_diff_balance_decrease() {
1100        let actor_id = 1002u64;
1101        let pre_actor = create_test_actor(5000, 10);
1102        let post_actor = create_test_actor(3000, 10);
1103        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
1104
1105        let mut touched_addresses = HashSet::new();
1106        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1107
1108        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1109
1110        let eth_addr = create_masked_id_eth_address(actor_id);
1111        let diff = state_diff.0.get(&eth_addr).unwrap();
1112        match &diff.balance {
1113            Delta::Changed(change) => {
1114                assert_eq!(change.from.0, BigInt::from(5000));
1115                assert_eq!(change.to.0, BigInt::from(3000));
1116            }
1117            _ => panic!("Expected Delta::Changed for balance"),
1118        }
1119        assert!(diff.nonce.is_unchanged());
1120    }
1121
1122    #[test]
1123    fn test_build_state_diff_nonce_increment() {
1124        let actor_id = 1003u64;
1125        let pre_actor = create_test_actor(1000, 5);
1126        let post_actor = create_test_actor(1000, 6);
1127        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
1128
1129        let mut touched_addresses = HashSet::new();
1130        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1131
1132        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1133
1134        let eth_addr = create_masked_id_eth_address(actor_id);
1135        let diff = state_diff.0.get(&eth_addr).unwrap();
1136        assert!(diff.balance.is_unchanged());
1137        match &diff.nonce {
1138            Delta::Changed(change) => {
1139                assert_eq!(change.from.0, 5);
1140                assert_eq!(change.to.0, 6);
1141            }
1142            _ => panic!("Expected Delta::Changed for nonce"),
1143        }
1144    }
1145
1146    #[test]
1147    fn test_build_state_diff_both_balance_and_nonce_change() {
1148        let actor_id = 1004u64;
1149        let pre_actor = create_test_actor(10000, 100);
1150        let post_actor = create_test_actor(9000, 101);
1151        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
1152
1153        let mut touched_addresses = HashSet::new();
1154        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1155
1156        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1157
1158        let eth_addr = create_masked_id_eth_address(actor_id);
1159        let diff = state_diff.0.get(&eth_addr).unwrap();
1160        match &diff.balance {
1161            Delta::Changed(change) => {
1162                assert_eq!(change.from.0, BigInt::from(10000));
1163                assert_eq!(change.to.0, BigInt::from(9000));
1164            }
1165            _ => panic!("Expected Delta::Changed for balance"),
1166        }
1167        match &diff.nonce {
1168            Delta::Changed(change) => {
1169                assert_eq!(change.from.0, 100);
1170                assert_eq!(change.to.0, 101);
1171            }
1172            _ => panic!("Expected Delta::Changed for nonce"),
1173        }
1174    }
1175
1176    #[test]
1177    fn test_build_state_diff_account_creation() {
1178        let actor_id = 1005u64;
1179        let post_actor = create_test_actor(5000, 0);
1180        let trees = TestStateTrees::with_created_actor(actor_id, post_actor).unwrap();
1181
1182        let mut touched_addresses = HashSet::new();
1183        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1184
1185        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1186
1187        let eth_addr = create_masked_id_eth_address(actor_id);
1188        let diff = state_diff.0.get(&eth_addr).unwrap();
1189        match &diff.balance {
1190            Delta::Added(balance) => {
1191                assert_eq!(balance.0, BigInt::from(5000));
1192            }
1193            _ => panic!("Expected Delta::Added for balance"),
1194        }
1195        match &diff.nonce {
1196            Delta::Added(nonce) => {
1197                assert_eq!(nonce.0, 0);
1198            }
1199            _ => panic!("Expected Delta::Added for nonce"),
1200        }
1201    }
1202
1203    #[test]
1204    fn test_build_state_diff_account_deletion() {
1205        let actor_id = 1006u64;
1206        let pre_actor = create_test_actor(3000, 10);
1207        let trees = TestStateTrees::with_deleted_actor(actor_id, pre_actor).unwrap();
1208
1209        let mut touched_addresses = HashSet::new();
1210        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1211
1212        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1213
1214        let eth_addr = create_masked_id_eth_address(actor_id);
1215        let diff = state_diff.0.get(&eth_addr).unwrap();
1216        match &diff.balance {
1217            Delta::Removed(balance) => {
1218                assert_eq!(balance.0, BigInt::from(3000));
1219            }
1220            _ => panic!("Expected Delta::Removed for balance"),
1221        }
1222        match &diff.nonce {
1223            Delta::Removed(nonce) => {
1224                assert_eq!(nonce.0, 10);
1225            }
1226            _ => panic!("Expected Delta::Removed for nonce"),
1227        }
1228    }
1229
1230    #[test]
1231    fn test_build_state_diff_multiple_addresses() {
1232        let store = Arc::new(MemoryDB::default());
1233        let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap();
1234        let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap();
1235
1236        // Actor 1: balance increase
1237        let addr1 = FilecoinAddress::new_id(2001);
1238        pre_state
1239            .set_actor(&addr1, create_test_actor(1000, 0))
1240            .unwrap();
1241        post_state
1242            .set_actor(&addr1, create_test_actor(2000, 0))
1243            .unwrap();
1244
1245        // Actor 2: nonce increase
1246        let addr2 = FilecoinAddress::new_id(2002);
1247        pre_state
1248            .set_actor(&addr2, create_test_actor(500, 5))
1249            .unwrap();
1250        post_state
1251            .set_actor(&addr2, create_test_actor(500, 6))
1252            .unwrap();
1253
1254        // Actor 3: no change (should not appear in diff)
1255        let addr3 = FilecoinAddress::new_id(2003);
1256        pre_state
1257            .set_actor(&addr3, create_test_actor(100, 1))
1258            .unwrap();
1259        post_state
1260            .set_actor(&addr3, create_test_actor(100, 1))
1261            .unwrap();
1262
1263        let mut touched_addresses = HashSet::new();
1264        touched_addresses.insert(create_masked_id_eth_address(2001));
1265        touched_addresses.insert(create_masked_id_eth_address(2002));
1266        touched_addresses.insert(create_masked_id_eth_address(2003));
1267
1268        let state_diff =
1269            build_state_diff(store.as_ref(), &pre_state, &post_state, &touched_addresses).unwrap();
1270
1271        assert_eq!(state_diff.0.len(), 2);
1272        assert!(
1273            state_diff
1274                .0
1275                .contains_key(&create_masked_id_eth_address(2001))
1276        );
1277        assert!(
1278            state_diff
1279                .0
1280                .contains_key(&create_masked_id_eth_address(2002))
1281        );
1282        assert!(
1283            !state_diff
1284                .0
1285                .contains_key(&create_masked_id_eth_address(2003))
1286        );
1287    }
1288
1289    #[test]
1290    fn test_build_state_diff_evm_actor_scenarios() {
1291        struct TestCase {
1292            name: &'static str,
1293            pre: Option<(u64, u64, Option<&'static [u8]>)>, // balance, nonce, bytecode
1294            post: Option<(u64, u64, Option<&'static [u8]>)>,
1295            expected_balance: Delta<EthBigInt>,
1296            expected_nonce: Delta<EthUint64>,
1297            expected_code: Delta<EthBytes>,
1298        }
1299
1300        let bytecode1: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52];
1301        let bytecode2: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52, 0x00];
1302
1303        let cases = vec![
1304            TestCase {
1305                name: "No change",
1306                pre: Some((1000, 5, Some(bytecode1))),
1307                post: Some((1000, 5, Some(bytecode1))),
1308                expected_balance: Delta::Unchanged,
1309                expected_nonce: Delta::Unchanged,
1310                expected_code: Delta::Unchanged,
1311            },
1312            TestCase {
1313                name: "Balance increase",
1314                pre: Some((1000, 5, Some(bytecode1))),
1315                post: Some((2000, 5, Some(bytecode1))),
1316                expected_balance: Delta::Changed(ChangedType {
1317                    from: EthBigInt(BigInt::from(1000)),
1318                    to: EthBigInt(BigInt::from(2000)),
1319                }),
1320                expected_nonce: Delta::Unchanged,
1321                expected_code: Delta::Unchanged,
1322            },
1323            TestCase {
1324                name: "Nonce increment",
1325                pre: Some((1000, 5, Some(bytecode1))),
1326                post: Some((1000, 6, Some(bytecode1))),
1327                expected_balance: Delta::Unchanged,
1328                expected_nonce: Delta::Changed(ChangedType {
1329                    from: EthUint64(5),
1330                    to: EthUint64(6),
1331                }),
1332                expected_code: Delta::Unchanged,
1333            },
1334            TestCase {
1335                name: "Bytecode change",
1336                pre: Some((1000, 5, Some(bytecode1))),
1337                post: Some((1000, 5, Some(bytecode2))),
1338                expected_balance: Delta::Unchanged,
1339                expected_nonce: Delta::Unchanged,
1340                expected_code: Delta::Changed(ChangedType {
1341                    from: EthBytes(bytecode1.to_vec()),
1342                    to: EthBytes(bytecode2.to_vec()),
1343                }),
1344            },
1345            TestCase {
1346                name: "Balance and Nonce change",
1347                pre: Some((1000, 5, Some(bytecode1))),
1348                post: Some((2000, 6, Some(bytecode1))),
1349                expected_balance: Delta::Changed(ChangedType {
1350                    from: EthBigInt(BigInt::from(1000)),
1351                    to: EthBigInt(BigInt::from(2000)),
1352                }),
1353                expected_nonce: Delta::Changed(ChangedType {
1354                    from: EthUint64(5),
1355                    to: EthUint64(6),
1356                }),
1357                expected_code: Delta::Unchanged,
1358            },
1359            TestCase {
1360                name: "Creation",
1361                pre: None,
1362                post: Some((5000, 0, Some(bytecode1))),
1363                expected_balance: Delta::Added(EthBigInt(BigInt::from(5000))),
1364                expected_nonce: Delta::Added(EthUint64(0)),
1365                expected_code: Delta::Added(EthBytes(bytecode1.to_vec())),
1366            },
1367            TestCase {
1368                name: "Deletion",
1369                pre: Some((3000, 10, Some(bytecode1))),
1370                post: None,
1371                expected_balance: Delta::Removed(EthBigInt(BigInt::from(3000))),
1372                expected_nonce: Delta::Removed(EthUint64(10)),
1373                expected_code: Delta::Removed(EthBytes(bytecode1.to_vec())),
1374            },
1375        ];
1376
1377        for case in cases {
1378            let store = Arc::new(MemoryDB::default());
1379            let actor_id = 10000u64; // arbitrary ID
1380
1381            let pre_actor = case.pre.and_then(|(bal, nonce, code)| {
1382                create_evm_actor_with_bytecode(&store, bal, 0, nonce, code)
1383            });
1384            let post_actor = case.post.and_then(|(bal, nonce, code)| {
1385                create_evm_actor_with_bytecode(&store, bal, 0, nonce, code)
1386            });
1387
1388            let mut pre_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap();
1389            let mut post_state = StateTree::new(store.clone(), StateTreeVersion::V5).unwrap();
1390            let addr = FilecoinAddress::new_id(actor_id);
1391
1392            if let Some(actor) = pre_actor {
1393                pre_state.set_actor(&addr, actor).unwrap();
1394            }
1395            if let Some(actor) = post_actor {
1396                post_state.set_actor(&addr, actor).unwrap();
1397            }
1398
1399            let mut touched_addresses = HashSet::new();
1400            touched_addresses.insert(create_masked_id_eth_address(actor_id));
1401
1402            let state_diff =
1403                build_state_diff(store.as_ref(), &pre_state, &post_state, &touched_addresses)
1404                    .unwrap();
1405
1406            if case.expected_balance == Delta::Unchanged
1407                && case.expected_nonce == Delta::Unchanged
1408                && case.expected_code == Delta::Unchanged
1409            {
1410                assert!(
1411                    state_diff.0.is_empty(),
1412                    "Test case '{}' failed: expected empty diff",
1413                    case.name
1414                );
1415            } else {
1416                let eth_addr = create_masked_id_eth_address(actor_id);
1417                let diff = state_diff.0.get(&eth_addr).unwrap_or_else(|| {
1418                    panic!("Test case '{}' failed: missing diff entry", case.name)
1419                });
1420
1421                assert_eq!(
1422                    diff.balance, case.expected_balance,
1423                    "Test case '{}' failed: balance mismatch",
1424                    case.name
1425                );
1426                assert_eq!(
1427                    diff.nonce, case.expected_nonce,
1428                    "Test case '{}' failed: nonce mismatch",
1429                    case.name
1430                );
1431                assert_eq!(
1432                    diff.code, case.expected_code,
1433                    "Test case '{}' failed: code mismatch",
1434                    case.name
1435                );
1436            }
1437        }
1438    }
1439
1440    #[test]
1441    fn test_build_state_diff_non_evm_actor_no_code() {
1442        // Non-EVM actors should have no code in their diff
1443        let actor_id = 4005u64;
1444        let pre_actor = create_test_actor(1000, 5);
1445        let post_actor = create_test_actor(2000, 6);
1446        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
1447
1448        let mut touched_addresses = HashSet::new();
1449        touched_addresses.insert(create_masked_id_eth_address(actor_id));
1450
1451        let state_diff = trees.build_diff(&touched_addresses).unwrap();
1452
1453        let eth_addr = create_masked_id_eth_address(actor_id);
1454        let diff = state_diff.0.get(&eth_addr).unwrap();
1455
1456        // Balance and nonce should change
1457        assert!(!diff.balance.is_unchanged());
1458        assert!(!diff.nonce.is_unchanged());
1459
1460        // Code should be unchanged (None -> None for non-EVM actors)
1461        assert!(diff.code.is_unchanged());
1462    }
1463}