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