Skip to main content

forest/rpc/methods/eth/trace/
state_diff.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! State diff computation for `trace_call` and related RPC methods.
5//!
6//! Compares pre- and post-execution actor states to produce per-account diffs
7//! covering balance, nonce, code, and storage.
8
9use super::super::EthBigInt;
10use super::super::types::{EthAddress, EthHash};
11use super::super::utils::ActorStateEthExt as _;
12use super::types::{AccountDiff, ChangedType, Delta, StateDiff};
13use super::utils::{ZERO_HASH, u256_to_eth_hash};
14use crate::prelude::*;
15use crate::shim::actors::{EVMActorStateLoad as _, evm, is_evm_actor};
16use crate::shim::state_tree::{ActorState, StateTree};
17use ahash::{HashMap, HashSet};
18use fil_actor_evm_state::evm_shared::v17::uints::U256;
19use fvm_ipld_kamt::{AsHashedKey, Config as KamtConfig, HashedKey, Kamt};
20use std::borrow::Cow;
21use std::collections::BTreeMap;
22use tracing::debug;
23
24/// KAMT configuration matching the EVM actor in builtin-actors.
25// Code is taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L47
26fn evm_kamt_config() -> KamtConfig {
27    KamtConfig {
28        bit_width: 5,       // 32 children per node (2^5)
29        min_data_depth: 0,  // Data can be stored at root level
30        max_array_width: 1, // Max 1 key-value pair per bucket
31    }
32}
33
34/// Hash algorithm for EVM storage KAMT.
35// Code taken from: https://github.com/filecoin-project/builtin-actors/blob/v17.0.0/actors/evm/src/interpreter/system.rs#L49.
36struct EvmStateHashAlgorithm;
37
38impl AsHashedKey<U256, 32> for EvmStateHashAlgorithm {
39    fn as_hashed_key(key: &U256) -> Cow<'_, HashedKey<32>> {
40        Cow::Owned(key.to_big_endian())
41    }
42}
43
44/// Type alias for EVM storage KAMT with configuration.
45type EvmStorageKamt<BS> = Kamt<BS, U256, U256, EvmStateHashAlgorithm>;
46
47/// Build state diff by comparing pre and post-execution states for touched addresses.
48pub fn build_state_diff<S: Blockstore, T: Blockstore>(
49    store: &S,
50    pre_state: &StateTree<T>,
51    post_state: &StateTree<T>,
52    touched_addresses: &HashSet<EthAddress>,
53) -> anyhow::Result<StateDiff> {
54    let mut state_diff = StateDiff::new();
55
56    for eth_addr in touched_addresses {
57        let fil_addr = eth_addr.to_filecoin_address()?;
58
59        // Get actor state before and after
60        let pre_actor = pre_state
61            .get_actor(&fil_addr)
62            .context("failed to get actor state")?;
63
64        let post_actor = post_state
65            .get_actor(&fil_addr)
66            .context("failed to get actor state")?;
67
68        let account_diff = build_account_diff(store, pre_actor.as_ref(), post_actor.as_ref())?;
69
70        // Only include it if there were actual changes
71        state_diff.insert_if_changed(*eth_addr, account_diff);
72    }
73
74    Ok(state_diff)
75}
76
77/// Build account diff by comparing pre and post actor states.
78fn build_account_diff<DB: Blockstore>(
79    store: &DB,
80    pre_actor: Option<&ActorState>,
81    post_actor: Option<&ActorState>,
82) -> anyhow::Result<AccountDiff> {
83    let mut diff = AccountDiff::default();
84
85    // Compare balance
86    let pre_balance = pre_actor.map(|a| EthBigInt::from(a.balance.atto()));
87    let post_balance = post_actor.map(|a| EthBigInt::from(a.balance.atto()));
88    diff.balance = Delta::from_comparison(pre_balance, post_balance);
89
90    // Compare nonce
91    let pre_nonce = pre_actor.map(|a| a.eth_nonce(store)).transpose()?;
92    let post_nonce = post_actor.map(|a| a.eth_nonce(store)).transpose()?;
93    diff.nonce = Delta::from_comparison(pre_nonce, post_nonce);
94
95    // Compare code (bytecode for EVM actors)
96    let pre_code = pre_actor
97        .map(|a| a.eth_bytecode(store))
98        .transpose()?
99        .flatten();
100    let post_code = post_actor
101        .map(|a| a.eth_bytecode(store))
102        .transpose()?
103        .flatten();
104    diff.code = Delta::from_comparison(pre_code, post_code);
105
106    // Compare storage slots for EVM actors
107    diff.storage = diff_evm_storage_for_actors(store, pre_actor, post_actor)?;
108
109    Ok(diff)
110}
111
112/// Compute storage diff between pre and post actor states.
113///
114/// Uses different Delta types based on the scenario:
115/// - Account created (None → EVM): storage slots are `Delta::Added`
116/// - Account deleted (EVM → None): storage slots are `Delta::Removed`
117/// - Account modified (EVM → EVM): storage slots are `Delta::Changed`
118/// - Actor type changed (EVM ↔ non-EVM): treated as deletion + creation
119fn diff_evm_storage_for_actors<DB: Blockstore>(
120    store: &DB,
121    pre_actor: Option<&ActorState>,
122    post_actor: Option<&ActorState>,
123) -> anyhow::Result<BTreeMap<EthHash, Delta<EthHash>>> {
124    let pre_is_evm = pre_actor.is_some_and(|a| is_evm_actor(&a.code));
125    let post_is_evm = post_actor.is_some_and(|a| is_evm_actor(&a.code));
126
127    // Extract storage entries from EVM actors (empty map for non-EVM or missing actors)
128    let pre_entries = extract_evm_storage_entries(store, pre_actor);
129    let post_entries = extract_evm_storage_entries(store, post_actor);
130
131    // If both are empty, no storage diff
132    if pre_entries.is_empty() && post_entries.is_empty() {
133        return Ok(BTreeMap::new());
134    }
135
136    let mut diff = BTreeMap::new();
137
138    match (pre_is_evm, post_is_evm) {
139        (false, true) => {
140            for (key_bytes, value) in &post_entries {
141                let key_hash = EthHash(ethereum_types::H256(*key_bytes));
142                diff.insert(key_hash, Delta::Added(u256_to_eth_hash(value)));
143            }
144        }
145        (true, false) => {
146            for (key_bytes, value) in &pre_entries {
147                let key_hash = EthHash(ethereum_types::H256(*key_bytes));
148                diff.insert(key_hash, Delta::Removed(u256_to_eth_hash(value)));
149            }
150        }
151        (true, true) => {
152            for (key_bytes, pre_value) in &pre_entries {
153                let key_hash = EthHash(ethereum_types::H256(*key_bytes));
154                let pre_hash = u256_to_eth_hash(pre_value);
155
156                match post_entries.get(key_bytes) {
157                    Some(post_value) if pre_value != post_value => {
158                        // Value changed
159                        diff.insert(
160                            key_hash,
161                            Delta::Changed(ChangedType {
162                                from: pre_hash,
163                                to: u256_to_eth_hash(post_value),
164                            }),
165                        );
166                    }
167                    Some(_) => {
168                        // Value unchanged, skip
169                    }
170                    None => {
171                        // Slot cleared (value → zero)
172                        diff.insert(
173                            key_hash,
174                            Delta::Changed(ChangedType {
175                                from: pre_hash,
176                                to: ZERO_HASH,
177                            }),
178                        );
179                    }
180                }
181            }
182
183            // Check for newly written entries (zero → value)
184            for (key_bytes, post_value) in &post_entries {
185                if !pre_entries.contains_key(key_bytes) {
186                    let key_hash = EthHash(ethereum_types::H256(*key_bytes));
187                    diff.insert(
188                        key_hash,
189                        Delta::Changed(ChangedType {
190                            from: ZERO_HASH,
191                            to: u256_to_eth_hash(post_value),
192                        }),
193                    );
194                }
195            }
196        }
197        // Neither EVM: no storage diff
198        (false, false) => {}
199    }
200
201    Ok(diff)
202}
203
204/// Extract all storage entries from an EVM actor's KAMT.
205/// Returns empty map if actor is None, not an EVM actor, or state cannot be loaded.
206pub fn extract_evm_storage_entries<DB: Blockstore>(
207    store: &DB,
208    actor: Option<&ActorState>,
209) -> HashMap<[u8; 32], U256> {
210    let actor = match actor {
211        Some(a) if is_evm_actor(&a.code) => a,
212        _ => return HashMap::default(),
213    };
214
215    let evm_state = match evm::State::load(store, actor.code, actor.state) {
216        Ok(state) => state,
217        Err(e) => {
218            debug!("failed to load EVM state for storage extraction: {e:#}");
219            return HashMap::default();
220        }
221    };
222
223    let storage_cid = evm_state.contract_state();
224    let config = evm_kamt_config();
225
226    let kamt: EvmStorageKamt<&DB> = match Kamt::load_with_config(&storage_cid, store, config) {
227        Ok(k) => k,
228        Err(e) => {
229            debug!("failed to load storage KAMT: {e}");
230            return HashMap::default();
231        }
232    };
233
234    let mut entries = HashMap::default();
235    if let Err(e) = kamt.for_each(|key, value| {
236        entries.insert(key.to_big_endian(), *value);
237        Ok(())
238    }) {
239        debug!("failed to iterate storage KAMT: {e}");
240        return HashMap::default();
241    }
242
243    entries
244}
245
246// Compute the set of storage keys that differ between pre and post actor states.
247pub fn diff_entry_keys(
248    pre_entries: &HashMap<[u8; 32], U256>,
249    post_entries: &HashMap<[u8; 32], U256>,
250) -> HashSet<[u8; 32]> {
251    let mut changed = HashSet::default();
252
253    for (k, v) in pre_entries {
254        match post_entries.get(k) {
255            Some(pv) if pv == v => {} // unchanged
256            _ => {
257                changed.insert(*k);
258            }
259        }
260    }
261
262    for k in post_entries.keys() {
263        if !pre_entries.contains_key(k) {
264            changed.insert(*k);
265        }
266    }
267
268    changed
269}
270
271#[cfg(test)]
272mod tests {
273    use super::super::test_helpers::*;
274    use super::*;
275    use crate::db::MemoryDB;
276    use crate::rpc::eth::EthUint64;
277    use crate::rpc::eth::types::EthBytes;
278    use crate::shim::address::Address as FilecoinAddress;
279    use crate::shim::state_tree::StateTreeVersion;
280
281    #[test]
282    fn test_build_state_diff_empty_touched_addresses() {
283        let trees = TestStateTrees::new().unwrap();
284        let touched_addresses = HashSet::new();
285
286        let state_diff = trees.build_diff(&touched_addresses).unwrap();
287
288        // No addresses touched = empty state diff
289        assert!(state_diff.0.is_empty());
290    }
291
292    #[test]
293    fn test_build_state_diff_nonexistent_address() {
294        let trees = TestStateTrees::new().unwrap();
295        let mut touched_addresses = HashSet::new();
296        touched_addresses.insert(create_masked_id_eth_address(9999));
297
298        let state_diff = trees.build_diff(&touched_addresses).unwrap();
299
300        // Address doesn't exist in either state, so no diff (both None = unchanged)
301        assert!(state_diff.0.is_empty());
302    }
303
304    #[test]
305    fn test_build_state_diff_balance_increase() {
306        let actor_id = 1001u64;
307        let pre_actor = create_test_actor(1000, 5);
308        let post_actor = create_test_actor(2000, 5);
309        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
310
311        let mut touched_addresses = HashSet::new();
312        touched_addresses.insert(create_masked_id_eth_address(actor_id));
313
314        let state_diff = trees.build_diff(&touched_addresses).unwrap();
315
316        assert_eq!(state_diff.0.len(), 1);
317        let eth_addr = create_masked_id_eth_address(actor_id);
318        let diff = state_diff.0.get(&eth_addr).unwrap();
319        match &diff.balance {
320            Delta::Changed(change) => {
321                assert_eq!(change.from, EthBigInt::from(1000));
322                assert_eq!(change.to, EthBigInt::from(2000));
323            }
324            _ => panic!("Expected Delta::Changed for balance"),
325        }
326        assert!(diff.nonce.is_unchanged());
327    }
328
329    #[test]
330    fn test_build_state_diff_balance_decrease() {
331        let actor_id = 1002u64;
332        let pre_actor = create_test_actor(5000, 10);
333        let post_actor = create_test_actor(3000, 10);
334        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
335
336        let mut touched_addresses = HashSet::new();
337        touched_addresses.insert(create_masked_id_eth_address(actor_id));
338
339        let state_diff = trees.build_diff(&touched_addresses).unwrap();
340
341        let eth_addr = create_masked_id_eth_address(actor_id);
342        let diff = state_diff.0.get(&eth_addr).unwrap();
343        match &diff.balance {
344            Delta::Changed(change) => {
345                assert_eq!(change.from, EthBigInt::from(5000));
346                assert_eq!(change.to, EthBigInt::from(3000));
347            }
348            _ => panic!("Expected Delta::Changed for balance"),
349        }
350        assert!(diff.nonce.is_unchanged());
351    }
352
353    #[test]
354    fn test_build_state_diff_nonce_increment() {
355        let actor_id = 1003u64;
356        let pre_actor = create_test_actor(1000, 5);
357        let post_actor = create_test_actor(1000, 6);
358        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
359
360        let mut touched_addresses = HashSet::new();
361        touched_addresses.insert(create_masked_id_eth_address(actor_id));
362
363        let state_diff = trees.build_diff(&touched_addresses).unwrap();
364
365        let eth_addr = create_masked_id_eth_address(actor_id);
366        let diff = state_diff.0.get(&eth_addr).unwrap();
367        assert!(diff.balance.is_unchanged());
368        match &diff.nonce {
369            Delta::Changed(change) => {
370                assert_eq!(change.from.0, 5);
371                assert_eq!(change.to.0, 6);
372            }
373            _ => panic!("Expected Delta::Changed for nonce"),
374        }
375    }
376
377    #[test]
378    fn test_build_state_diff_both_balance_and_nonce_change() {
379        let actor_id = 1004u64;
380        let pre_actor = create_test_actor(10000, 100);
381        let post_actor = create_test_actor(9000, 101);
382        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
383
384        let mut touched_addresses = HashSet::new();
385        touched_addresses.insert(create_masked_id_eth_address(actor_id));
386
387        let state_diff = trees.build_diff(&touched_addresses).unwrap();
388
389        let eth_addr = create_masked_id_eth_address(actor_id);
390        let diff = state_diff.0.get(&eth_addr).unwrap();
391        match &diff.balance {
392            Delta::Changed(change) => {
393                assert_eq!(change.from, EthBigInt::from(10000));
394                assert_eq!(change.to, EthBigInt::from(9000));
395            }
396            _ => panic!("Expected Delta::Changed for balance"),
397        }
398        match &diff.nonce {
399            Delta::Changed(change) => {
400                assert_eq!(change.from.0, 100);
401                assert_eq!(change.to.0, 101);
402            }
403            _ => panic!("Expected Delta::Changed for nonce"),
404        }
405    }
406
407    #[test]
408    fn test_build_state_diff_account_creation() {
409        let actor_id = 1005u64;
410        let post_actor = create_test_actor(5000, 0);
411        let trees = TestStateTrees::with_created_actor(actor_id, post_actor).unwrap();
412
413        let mut touched_addresses = HashSet::new();
414        touched_addresses.insert(create_masked_id_eth_address(actor_id));
415
416        let state_diff = trees.build_diff(&touched_addresses).unwrap();
417
418        let eth_addr = create_masked_id_eth_address(actor_id);
419        let diff = state_diff.0.get(&eth_addr).unwrap();
420        match &diff.balance {
421            Delta::Added(balance) => {
422                assert_eq!(balance, &EthBigInt::from(5000));
423            }
424            _ => panic!("Expected Delta::Added for balance"),
425        }
426        match &diff.nonce {
427            Delta::Added(nonce) => {
428                assert_eq!(nonce.0, 0);
429            }
430            _ => panic!("Expected Delta::Added for nonce"),
431        }
432    }
433
434    #[test]
435    fn test_build_state_diff_account_deletion() {
436        let actor_id = 1006u64;
437        let pre_actor = create_test_actor(3000, 10);
438        let trees = TestStateTrees::with_deleted_actor(actor_id, pre_actor).unwrap();
439
440        let mut touched_addresses = HashSet::new();
441        touched_addresses.insert(create_masked_id_eth_address(actor_id));
442
443        let state_diff = trees.build_diff(&touched_addresses).unwrap();
444
445        let eth_addr = create_masked_id_eth_address(actor_id);
446        let diff = state_diff.0.get(&eth_addr).unwrap();
447        match &diff.balance {
448            Delta::Removed(balance) => {
449                assert_eq!(balance, &EthBigInt::from(3000));
450            }
451            _ => panic!("Expected Delta::Removed for balance"),
452        }
453        match &diff.nonce {
454            Delta::Removed(nonce) => {
455                assert_eq!(nonce.0, 10);
456            }
457            _ => panic!("Expected Delta::Removed for nonce"),
458        }
459    }
460
461    #[test]
462    fn test_build_state_diff_multiple_addresses() {
463        let store = Arc::new(MemoryDB::default());
464        let mut pre_state = StateTree::new(&store, StateTreeVersion::V5).unwrap();
465        let mut post_state = StateTree::new(&store, StateTreeVersion::V5).unwrap();
466
467        // Actor 1: balance increase
468        let addr1 = FilecoinAddress::new_id(2001);
469        pre_state
470            .set_actor(&addr1, create_test_actor(1000, 0))
471            .unwrap();
472        post_state
473            .set_actor(&addr1, create_test_actor(2000, 0))
474            .unwrap();
475
476        // Actor 2: nonce increase
477        let addr2 = FilecoinAddress::new_id(2002);
478        pre_state
479            .set_actor(&addr2, create_test_actor(500, 5))
480            .unwrap();
481        post_state
482            .set_actor(&addr2, create_test_actor(500, 6))
483            .unwrap();
484
485        // Actor 3: no change (should not appear in diff)
486        let addr3 = FilecoinAddress::new_id(2003);
487        pre_state
488            .set_actor(&addr3, create_test_actor(100, 1))
489            .unwrap();
490        post_state
491            .set_actor(&addr3, create_test_actor(100, 1))
492            .unwrap();
493
494        let mut touched_addresses = HashSet::new();
495        touched_addresses.insert(create_masked_id_eth_address(2001));
496        touched_addresses.insert(create_masked_id_eth_address(2002));
497        touched_addresses.insert(create_masked_id_eth_address(2003));
498
499        let state_diff =
500            build_state_diff(store.as_ref(), &pre_state, &post_state, &touched_addresses).unwrap();
501
502        assert_eq!(state_diff.0.len(), 2);
503        assert!(
504            state_diff
505                .0
506                .contains_key(&create_masked_id_eth_address(2001))
507        );
508        assert!(
509            state_diff
510                .0
511                .contains_key(&create_masked_id_eth_address(2002))
512        );
513        assert!(
514            !state_diff
515                .0
516                .contains_key(&create_masked_id_eth_address(2003))
517        );
518    }
519
520    #[test]
521    fn test_build_state_diff_evm_actor_scenarios() {
522        struct TestCase {
523            name: &'static str,
524            pre: Option<(u64, u64, Option<&'static [u8]>)>, // balance, nonce, bytecode
525            post: Option<(u64, u64, Option<&'static [u8]>)>,
526            expected_balance: Delta<EthBigInt>,
527            expected_nonce: Delta<EthUint64>,
528            expected_code: Delta<EthBytes>,
529        }
530
531        let bytecode1: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52];
532        let bytecode2: &[u8] = &[0x60, 0x80, 0x60, 0x40, 0x52, 0x00];
533
534        let cases = vec![
535            TestCase {
536                name: "No change",
537                pre: Some((1000, 5, Some(bytecode1))),
538                post: Some((1000, 5, Some(bytecode1))),
539                expected_balance: Delta::Unchanged,
540                expected_nonce: Delta::Unchanged,
541                expected_code: Delta::Unchanged,
542            },
543            TestCase {
544                name: "Balance increase",
545                pre: Some((1000, 5, Some(bytecode1))),
546                post: Some((2000, 5, Some(bytecode1))),
547                expected_balance: Delta::Changed(ChangedType {
548                    from: EthBigInt::from(1000),
549                    to: EthBigInt::from(2000),
550                }),
551                expected_nonce: Delta::Unchanged,
552                expected_code: Delta::Unchanged,
553            },
554            TestCase {
555                name: "Nonce increment",
556                pre: Some((1000, 5, Some(bytecode1))),
557                post: Some((1000, 6, Some(bytecode1))),
558                expected_balance: Delta::Unchanged,
559                expected_nonce: Delta::Changed(ChangedType {
560                    from: EthUint64(5),
561                    to: EthUint64(6),
562                }),
563                expected_code: Delta::Unchanged,
564            },
565            TestCase {
566                name: "Bytecode change",
567                pre: Some((1000, 5, Some(bytecode1))),
568                post: Some((1000, 5, Some(bytecode2))),
569                expected_balance: Delta::Unchanged,
570                expected_nonce: Delta::Unchanged,
571                expected_code: Delta::Changed(ChangedType {
572                    from: EthBytes(bytecode1.to_vec()),
573                    to: EthBytes(bytecode2.to_vec()),
574                }),
575            },
576            TestCase {
577                name: "Balance and Nonce change",
578                pre: Some((1000, 5, Some(bytecode1))),
579                post: Some((2000, 6, Some(bytecode1))),
580                expected_balance: Delta::Changed(ChangedType {
581                    from: EthBigInt::from(1000),
582                    to: EthBigInt::from(2000),
583                }),
584                expected_nonce: Delta::Changed(ChangedType {
585                    from: EthUint64(5),
586                    to: EthUint64(6),
587                }),
588                expected_code: Delta::Unchanged,
589            },
590            TestCase {
591                name: "Creation",
592                pre: None,
593                post: Some((5000, 0, Some(bytecode1))),
594                expected_balance: Delta::Added(EthBigInt::from(5000)),
595                expected_nonce: Delta::Added(EthUint64(0)),
596                expected_code: Delta::Added(EthBytes(bytecode1.to_vec())),
597            },
598            TestCase {
599                name: "Deletion",
600                pre: Some((3000, 10, Some(bytecode1))),
601                post: None,
602                expected_balance: Delta::Removed(EthBigInt::from(3000)),
603                expected_nonce: Delta::Removed(EthUint64(10)),
604                expected_code: Delta::Removed(EthBytes(bytecode1.to_vec())),
605            },
606        ];
607
608        for case in cases {
609            let store = Arc::new(MemoryDB::default());
610            let actor_id = 10000u64; // arbitrary ID
611
612            let pre_actor = case.pre.and_then(|(bal, nonce, code)| {
613                create_evm_actor_with_bytecode(&store, bal, 0, nonce, code)
614            });
615            let post_actor = case.post.and_then(|(bal, nonce, code)| {
616                create_evm_actor_with_bytecode(&store, bal, 0, nonce, code)
617            });
618
619            let mut pre_state = StateTree::new(&store, StateTreeVersion::V5).unwrap();
620            let mut post_state = StateTree::new(&store, StateTreeVersion::V5).unwrap();
621            let addr = FilecoinAddress::new_id(actor_id);
622
623            if let Some(actor) = pre_actor {
624                pre_state.set_actor(&addr, actor).unwrap();
625            }
626            if let Some(actor) = post_actor {
627                post_state.set_actor(&addr, actor).unwrap();
628            }
629
630            let mut touched_addresses = HashSet::new();
631            touched_addresses.insert(create_masked_id_eth_address(actor_id));
632
633            let state_diff =
634                build_state_diff(store.as_ref(), &pre_state, &post_state, &touched_addresses)
635                    .unwrap();
636
637            if case.expected_balance == Delta::Unchanged
638                && case.expected_nonce == Delta::Unchanged
639                && case.expected_code == Delta::Unchanged
640            {
641                assert!(
642                    state_diff.0.is_empty(),
643                    "Test case '{}' failed: expected empty diff",
644                    case.name
645                );
646            } else {
647                let eth_addr = create_masked_id_eth_address(actor_id);
648                let diff = state_diff.0.get(&eth_addr).unwrap_or_else(|| {
649                    panic!("Test case '{}' failed: missing diff entry", case.name)
650                });
651
652                assert_eq!(
653                    diff.balance, case.expected_balance,
654                    "Test case '{}' failed: balance mismatch",
655                    case.name
656                );
657                assert_eq!(
658                    diff.nonce, case.expected_nonce,
659                    "Test case '{}' failed: nonce mismatch",
660                    case.name
661                );
662                assert_eq!(
663                    diff.code, case.expected_code,
664                    "Test case '{}' failed: code mismatch",
665                    case.name
666                );
667            }
668        }
669    }
670
671    #[test]
672    fn test_build_state_diff_non_evm_actor_no_code() {
673        // Non-EVM actors should have no code in their diff
674        let actor_id = 4005u64;
675        let pre_actor = create_test_actor(1000, 5);
676        let post_actor = create_test_actor(2000, 6);
677        let trees = TestStateTrees::with_changed_actor(actor_id, pre_actor, post_actor).unwrap();
678
679        let mut touched_addresses = HashSet::new();
680        touched_addresses.insert(create_masked_id_eth_address(actor_id));
681
682        let state_diff = trees.build_diff(&touched_addresses).unwrap();
683
684        let eth_addr = create_masked_id_eth_address(actor_id);
685        let diff = state_diff.0.get(&eth_addr).unwrap();
686
687        // Balance and nonce should change
688        assert!(!diff.balance.is_unchanged());
689        assert!(!diff.nonce.is_unchanged());
690
691        // Code should be unchanged (None -> None for non-EVM actors)
692        assert!(diff.code.is_unchanged());
693    }
694
695    #[test]
696    fn test_actor_nonce_non_evm() {
697        let store = MemoryDB::default();
698        let actor = create_test_actor(1000, 42);
699        let nonce = actor.eth_nonce(&store).unwrap();
700        assert_eq!(nonce.0, 42);
701    }
702
703    #[test]
704    fn test_actor_nonce_evm() {
705        let store = Arc::new(MemoryDB::default());
706        let actor = create_evm_actor_with_bytecode(&store, 1000, 0, 7, Some(&[0x60]))
707            .expect("failed to create EVM actor fixture");
708        let nonce = actor.eth_nonce(store.as_ref()).unwrap();
709        // EVM actors use the EVM nonce field, not the actor sequence
710        assert_eq!(nonce.0, 7);
711    }
712
713    #[test]
714    fn test_actor_bytecode_non_evm() {
715        let store = MemoryDB::default();
716        let actor = create_test_actor(1000, 0);
717        assert!(actor.eth_bytecode(&store).unwrap().is_none());
718    }
719
720    #[test]
721    fn test_actor_bytecode_evm() {
722        let store = Arc::new(MemoryDB::default());
723        let bytecode = &[0x60, 0x80, 0x60, 0x40, 0x52];
724        let actor = create_evm_actor_with_bytecode(&store, 1000, 0, 1, Some(bytecode))
725            .expect("failed to create EVM actor fixture");
726        let result = actor.eth_bytecode(store.as_ref()).unwrap();
727        assert_eq!(result, Some(EthBytes(bytecode.to_vec())));
728    }
729
730    #[test]
731    fn test_actor_bytecode_evm_no_bytecode() {
732        let store = Arc::new(MemoryDB::default());
733        let actor = create_evm_actor_with_bytecode(&store, 1000, 0, 1, None)
734            .expect("failed to create EVM actor fixture");
735        // No bytecode stored => None (Cid::default() won't resolve to raw data)
736        let result = actor.eth_bytecode(store.as_ref()).unwrap();
737        assert!(result.is_none());
738    }
739
740    #[test]
741    fn test_diff_entry_keys_both_empty() {
742        let pre = HashMap::default();
743        let post = HashMap::default();
744        let keys = diff_entry_keys(&pre, &post);
745        assert!(keys.is_empty());
746    }
747
748    #[test]
749    fn test_diff_entry_keys_non_evm_actors() {
750        let store = MemoryDB::default();
751        let pre = create_test_actor(1000, 5);
752        let post = create_test_actor(2000, 6);
753        // Non-EVM actors have no EVM storage, so no changed keys
754        let pre_entries = extract_evm_storage_entries(&store, Some(&pre));
755        let post_entries = extract_evm_storage_entries(&store, Some(&post));
756        let keys = diff_entry_keys(&pre_entries, &post_entries);
757        assert!(keys.is_empty());
758    }
759
760    #[test]
761    fn test_diff_entry_keys_pre_none_post_non_evm() {
762        let store = MemoryDB::default();
763        let post = create_test_actor(1000, 0);
764        let pre_entries = extract_evm_storage_entries(&store, None);
765        let post_entries = extract_evm_storage_entries(&store, Some(&post));
766        let keys = diff_entry_keys(&pre_entries, &post_entries);
767        assert!(keys.is_empty());
768    }
769}