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