Skip to main content

revm_state/
lib.rs

1//! Account and storage state.
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3#![cfg_attr(not(feature = "std"), no_std)]
4
5#[cfg(not(feature = "std"))]
6extern crate alloc as std;
7
8mod account_info;
9pub mod bal;
10mod types;
11
12pub use bytecode;
13
14pub use account_info::{AccountId, AccountInfo};
15pub use bytecode::Bytecode;
16pub use primitives;
17pub use types::{EvmState, EvmStorage, TransientStorage};
18
19use bitflags::bitflags;
20use nonmax::NonMaxU32;
21use primitives::{hardfork::SpecId, HashMap, StorageKey, StorageValue, U256};
22use std::boxed::Box;
23
24/// Transaction id used to track when account or storage slot was touched/loaded into the journal.
25///
26/// Wraps a [`NonMaxU32`] so that `Option<TransactionId>` benefits from niche optimization.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[cfg_attr(feature = "serde", serde(transparent))]
30pub struct TransactionId(NonMaxU32);
31
32impl TransactionId {
33    /// The zero transaction id.
34    pub const ZERO: Self = Self(NonMaxU32::ZERO);
35
36    /// Creates a new [`TransactionId`].
37    ///
38    /// Returns `None` if the value does not fit in the internal representation.
39    #[inline]
40    pub fn new(id: usize) -> Option<Self> {
41        let id = u32::try_from(id).ok()?;
42        NonMaxU32::new(id).map(Self)
43    }
44
45    /// Returns the transaction id as a usize.
46    #[inline]
47    pub const fn get(self) -> usize {
48        self.0.get() as usize
49    }
50
51    /// Increments the transaction id by 1.
52    ///
53    /// # Panics
54    ///
55    /// Panics if the resulting value would equal `u32::MAX`.
56    #[inline]
57    pub const fn increment(&mut self) {
58        self.0 = match NonMaxU32::new(self.0.get() + 1) {
59            Some(id) => id,
60            None => panic!("transaction id overflow"),
61        };
62    }
63}
64
65/// The main account type used inside Revm. It is stored inside Journal and contains all the information about the account.
66///
67/// Other than standard Account information it contains its status that can be both cold and warm
68/// additional to that it contains BAL that is used to load data for this particular account.
69///
70/// On loading from database:
71///     * If CompiledBal is present, load values from BAL into Account (Assume account has read data from database)
72///     * In case of parallel execution, AccountInfo would be same over all parallel executions.
73///     * Maybe use transaction_id as a way to notify user that this is obsolete data.
74///     * Database needs to load account and tie to with BAL writes
75/// If CompiledBal is not present, use loaded values
76///     * Account is already up to date (uses present flow).
77#[derive(Debug, Clone, Eq, Default)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize))]
79pub struct Account {
80    /// Balance, nonce, and code
81    pub info: AccountInfo,
82    /// Transaction id, used to track when account was touched/loaded into journal.
83    pub transaction_id: TransactionId,
84    /// Storage cache
85    pub storage: EvmStorage,
86    /// Account status flags
87    pub status: AccountStatus,
88
89    /// Original account info used by BAL, changed only on cold load by BAL.
90    /// `None` means `Default::default()`, to avoid allocations.
91    original_info: Option<Box<AccountInfo>>,
92}
93
94impl PartialEq for Account {
95    #[inline]
96    fn eq(&self, other: &Self) -> bool {
97        self.info == other.info
98            && self.transaction_id == other.transaction_id
99            && self.storage == other.storage
100            && self.status == other.status
101            && self.original_info() == other.original_info()
102    }
103}
104
105impl Account {
106    /// Creates new account and mark it as non existing.
107    #[inline]
108    pub fn new_not_existing(transaction_id: TransactionId) -> Self {
109        Self {
110            transaction_id,
111            status: AccountStatus::LoadedAsNotExisting,
112            ..Default::default()
113        }
114    }
115
116    /// Make changes to the caller account.
117    ///
118    /// It marks the account as touched, changes the balance and bumps the nonce if `is_call` is true.
119    ///
120    /// Returns the old balance.
121    #[inline]
122    pub fn caller_initial_modification(&mut self, new_balance: U256, is_call: bool) -> U256 {
123        // Touch account so we know it is changed.
124        self.mark_touch();
125
126        if is_call {
127            // Nonce is already checked
128            self.info.nonce = self.info.nonce.saturating_add(1);
129        }
130
131        core::mem::replace(&mut self.info.balance, new_balance)
132    }
133
134    /// Checks if account is empty and check if empty state before spurious dragon hardfork.
135    #[inline]
136    pub fn state_clear_aware_is_empty(&self, spec: SpecId) -> bool {
137        if SpecId::is_enabled_in(spec, SpecId::SPURIOUS_DRAGON) {
138            self.is_empty()
139        } else {
140            self.is_loaded_as_not_existing_not_touched()
141        }
142    }
143
144    /// Returns the original account info.
145    #[inline]
146    pub fn original_info(&self) -> AccountInfo {
147        self.original_info.as_deref().cloned().unwrap_or_default()
148    }
149
150    /// Returns a mutable reference to the original account info.
151    #[inline]
152    pub fn original_info_mut(&mut self) -> &mut AccountInfo {
153        self.original_info.get_or_insert_default()
154    }
155
156    /// Clones the current info into the original info.
157    pub fn set_current_info_as_original(&mut self) {
158        if self.original_info.is_none() && self.info.is_default() {
159            return;
160        }
161        self.original_info
162            .get_or_insert_default()
163            .as_mut()
164            .clone_from(&self.info);
165    }
166
167    /// Marks the account as self destructed.
168    #[inline]
169    pub fn mark_selfdestruct(&mut self) {
170        self.status |= AccountStatus::SelfDestructed;
171    }
172
173    /// Unmarks the account as self destructed.
174    #[inline]
175    pub fn unmark_selfdestruct(&mut self) {
176        self.status -= AccountStatus::SelfDestructed;
177    }
178
179    /// Is account marked for self destruct.
180    #[inline]
181    pub const fn is_selfdestructed(&self) -> bool {
182        self.status.contains(AccountStatus::SelfDestructed)
183    }
184
185    /// Marks the account as touched
186    #[inline]
187    pub fn mark_touch(&mut self) {
188        self.status |= AccountStatus::Touched;
189    }
190
191    /// Unmarks the touch flag.
192    #[inline]
193    pub fn unmark_touch(&mut self) {
194        self.status -= AccountStatus::Touched;
195    }
196
197    /// If account status is marked as touched.
198    #[inline]
199    pub const fn is_touched(&self) -> bool {
200        self.status.contains(AccountStatus::Touched)
201    }
202
203    /// Returns true if account info was changed.
204    #[inline]
205    pub fn is_changed(&self) -> bool {
206        self.original_info.as_deref().map_or_else(
207            || !self.info.is_default(),
208            |original| self.info != *original,
209        )
210    }
211
212    /// Marks the account as newly created.
213    #[inline]
214    pub fn mark_created(&mut self) {
215        self.status |= AccountStatus::Created;
216    }
217
218    /// Unmarks the created flag.
219    #[inline]
220    pub fn unmark_created(&mut self) {
221        self.status -= AccountStatus::Created;
222    }
223
224    /// Marks the account as cold.
225    #[inline]
226    pub fn mark_cold(&mut self) {
227        self.status |= AccountStatus::Cold;
228    }
229
230    /// Is account warm for given transaction id.
231    #[inline]
232    pub const fn is_cold_transaction_id(&self, transaction_id: TransactionId) -> bool {
233        self.transaction_id.get() != transaction_id.get()
234            || self.status.contains(AccountStatus::Cold)
235    }
236
237    /// Marks the account as warm and return true if it was previously cold.
238    #[inline]
239    pub fn mark_warm_with_transaction_id(&mut self, transaction_id: TransactionId) -> bool {
240        let is_cold = self.is_cold_transaction_id(transaction_id);
241        self.status -= AccountStatus::Cold;
242        self.transaction_id = transaction_id;
243        is_cold
244    }
245
246    /// Is account locally created
247    #[inline]
248    pub const fn is_created_locally(&self) -> bool {
249        self.status.contains(AccountStatus::CreatedLocal)
250    }
251
252    /// Is account locally selfdestructed
253    #[inline]
254    pub const fn is_selfdestructed_locally(&self) -> bool {
255        self.status.contains(AccountStatus::SelfDestructedLocal)
256    }
257
258    /// Selfdestruct the account by clearing its storage and resetting its account info
259    #[inline]
260    pub fn selfdestruct(&mut self) {
261        self.storage.clear();
262        self.info = AccountInfo::default();
263    }
264
265    /// Mark account as locally created and mark global created flag.
266    ///
267    /// Returns true if it is created globally for first time.
268    #[inline]
269    pub fn mark_created_locally(&mut self) -> bool {
270        self.mark_local_and_global(AccountStatus::CreatedLocal, AccountStatus::Created)
271    }
272
273    /// Unmark account as locally created
274    #[inline]
275    pub fn unmark_created_locally(&mut self) {
276        self.status -= AccountStatus::CreatedLocal;
277    }
278
279    /// Mark account as locally and globally selfdestructed
280    #[inline]
281    pub fn mark_selfdestructed_locally(&mut self) -> bool {
282        self.mark_local_and_global(
283            AccountStatus::SelfDestructedLocal,
284            AccountStatus::SelfDestructed,
285        )
286    }
287
288    #[inline]
289    fn mark_local_and_global(
290        &mut self,
291        local_flag: AccountStatus,
292        global_flag: AccountStatus,
293    ) -> bool {
294        self.status |= local_flag;
295        let is_global_first_time = !self.status.contains(global_flag);
296        self.status |= global_flag;
297        is_global_first_time
298    }
299
300    /// Unmark account as locally selfdestructed
301    #[inline]
302    pub fn unmark_selfdestructed_locally(&mut self) {
303        self.status -= AccountStatus::SelfDestructedLocal;
304    }
305
306    /// Is account loaded as not existing from database.
307    ///
308    /// This is needed for pre spurious dragon hardforks where
309    /// existing and empty were two separate states.
310    pub const fn is_loaded_as_not_existing(&self) -> bool {
311        self.status.contains(AccountStatus::LoadedAsNotExisting)
312    }
313
314    /// Is account loaded as not existing from database and not touched.
315    pub const fn is_loaded_as_not_existing_not_touched(&self) -> bool {
316        self.is_loaded_as_not_existing() && !self.is_touched()
317    }
318
319    /// Is account newly created in this transaction.
320    pub const fn is_created(&self) -> bool {
321        self.status.contains(AccountStatus::Created)
322    }
323
324    /// Is account empty, check if nonce and balance are zero and code is empty.
325    pub fn is_empty(&self) -> bool {
326        self.info.is_empty()
327    }
328
329    /// Returns an iterator over the storage slots that have been changed.
330    ///
331    /// See also [EvmStorageSlot::is_changed].
332    pub fn changed_storage_slots(&self) -> impl Iterator<Item = (&StorageKey, &EvmStorageSlot)> {
333        self.storage.iter().filter(|(_, slot)| slot.is_changed())
334    }
335
336    /// Sets account info and returns self for method chaining.
337    pub fn with_info(mut self, info: AccountInfo) -> Self {
338        self.info = info;
339        self
340    }
341
342    /// Populates storage from an iterator of storage slots and returns self for method chaining.
343    pub fn with_storage<I>(mut self, storage_iter: I) -> Self
344    where
345        I: Iterator<Item = (StorageKey, EvmStorageSlot)>,
346    {
347        for (key, slot) in storage_iter {
348            self.storage.insert(key, slot);
349        }
350        self
351    }
352
353    /// Marks the account as self destructed and returns self for method chaining.
354    pub fn with_selfdestruct_mark(mut self) -> Self {
355        self.mark_selfdestruct();
356        self
357    }
358
359    /// Marks the account as touched and returns self for method chaining.
360    pub fn with_touched_mark(mut self) -> Self {
361        self.mark_touch();
362        self
363    }
364
365    /// Marks the account as newly created and returns self for method chaining.
366    pub fn with_created_mark(mut self) -> Self {
367        self.mark_created();
368        self
369    }
370
371    /// Marks the account as cold and returns self for method chaining.
372    pub fn with_cold_mark(mut self) -> Self {
373        self.mark_cold();
374        self
375    }
376
377    /// Marks the account as warm (not cold) and returns self for method chaining.
378    /// Also returns whether the account was previously cold.
379    pub fn with_warm_mark(mut self, transaction_id: TransactionId) -> (Self, bool) {
380        let was_cold = self.mark_warm_with_transaction_id(transaction_id);
381        (self, was_cold)
382    }
383
384    /// Variant of with_warm_mark that doesn't return the previous state.
385    pub fn with_warm(mut self, transaction_id: TransactionId) -> Self {
386        self.mark_warm_with_transaction_id(transaction_id);
387        self
388    }
389}
390
391impl From<AccountInfo> for Account {
392    fn from(info: AccountInfo) -> Self {
393        let original_info = if info.is_default() {
394            None
395        } else {
396            Some(Box::new(info.clone()))
397        };
398        Self {
399            info,
400            original_info,
401            transaction_id: TransactionId::ZERO,
402            storage: HashMap::default(),
403            status: AccountStatus::empty(),
404        }
405    }
406}
407
408#[cfg(feature = "serde")]
409mod serde_impl {
410    use super::*;
411    use serde::Deserialize;
412
413    /// Distinguishes missing field (old format) from explicit `null` (new format).
414    #[derive(Default)]
415    enum MaybeOriginalInfo {
416        /// Field was missing from JSON (old format).
417        #[default]
418        Missing,
419        /// Present in JSON: `null` means default, `Some` is the value.
420        Present(Option<AccountInfo>),
421    }
422
423    impl<'de> Deserialize<'de> for MaybeOriginalInfo {
424        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
425        where
426            D: serde::Deserializer<'de>,
427        {
428            Option::<AccountInfo>::deserialize(deserializer).map(MaybeOriginalInfo::Present)
429        }
430    }
431
432    #[derive(Deserialize)]
433    // Field order must match `Account`'s declaration order: the derived `Serialize` emits
434    // fields positionally, and non-self-describing formats (bincode, postcard) replay them in
435    // this struct's declared order — only self-describing formats match fields by name.
436    struct AccountSerde {
437        info: AccountInfo,
438        transaction_id: TransactionId,
439        storage: EvmStorage,
440        status: AccountStatus,
441        #[serde(default)]
442        original_info: MaybeOriginalInfo,
443    }
444
445    impl<'de> Deserialize<'de> for super::Account {
446        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
447        where
448            D: serde::Deserializer<'de>,
449        {
450            let AccountSerde {
451                info,
452                original_info,
453                storage,
454                transaction_id,
455                status,
456            } = Deserialize::deserialize(deserializer)?;
457
458            let original_info = match original_info {
459                // Old format: field missing → use info as original.
460                MaybeOriginalInfo::Missing => Some(Box::new(info.clone())),
461                // New format: null → None (default).
462                MaybeOriginalInfo::Present(None) => None,
463                // New format: explicit value.
464                MaybeOriginalInfo::Present(Some(oi)) => Some(Box::new(oi)),
465            };
466
467            Ok(Account {
468                info,
469                original_info,
470                storage,
471                transaction_id,
472                status,
473            })
474        }
475    }
476}
477
478// The `bitflags!` macro generates `struct`s that manage a set of flags.
479bitflags! {
480    /// Account status flags. Generated by bitflags crate.
481    ///
482    /// With multi transaction feature there is a need to have both global and local fields.
483    /// Global across multiple transaction and local across one transaction execution.
484    ///
485    /// Empty state without any flags set represent account that is loaded from db but not interacted with.
486    ///
487    /// `Touched` flag is used by database to check if account is potentially changed in some way.
488    /// Additionally, after EIP-161 touch on empty-existing account would remove this account from state
489    /// after transaction execution ends. Touch can span across multiple transactions as it is needed
490    /// to be marked only once so it is safe to have only one global flag.
491    /// Only first touch have different behaviour from others, and touch in first transaction will invalidate
492    /// touch functionality in next transactions.
493    ///
494    /// `Created` flag is used to mark account as newly created in this transaction. This is used for optimization
495    /// where if this flag is set we will not access database to fetch storage values.
496    ///
497    /// `CreatedLocal` flag is used after cancun to enable selfdestruct cleanup if account is created in same transaction.
498    ///
499    /// `Selfdestructed` flag is used to mark account as selfdestructed. On multiple calls this flag is preserved
500    /// and on revert will stay selfdestructed.
501    ///
502    /// `SelfdestructLocal` is needed to award refund on first selfdestruct call. This flag is cleared on account loading.
503    /// Over multiple transaction account can be selfdestructed in one tx, created in second tx and selfdestructed again in
504    /// third tx.
505    /// Additionally if account is loaded in second tx, storage and account that was destroyed in first tx needs to be cleared.
506    ///
507    /// `LoadedAsNotExisting` is used to mark account as loaded from database but with `balance == 0 && nonce == 0 && code = 0x`.
508    /// This flag is fine to span across multiple transactions as it interucts with `Touched` flag this is used in global scope.
509    ///
510    /// `CreatedLocal`, `SelfdestructedLocal` and `Cold` flags are reset on first account loading of local scope.
511    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
512    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
513    #[cfg_attr(feature = "serde", serde(transparent))]
514    pub struct AccountStatus: u8 {
515        /// When account is newly created we will not access database
516        /// to fetch storage values.
517        const Created = 0b00000001;
518        /// When accounts gets loaded this flag is set to false. Create will always be true if CreatedLocal is true.
519        const CreatedLocal = 0b10000000;
520        /// If account is marked for self destruction.
521        const SelfDestructed = 0b00000010;
522        /// If account is marked for self destruction.
523        const SelfDestructedLocal = 0b01000000;
524        /// Only when account is marked as touched we will save it to database.
525        /// Additionally first touch on empty existing account (After EIP-161) will mark it
526        /// for removal from state after transaction execution.
527        const Touched = 0b00000100;
528        /// used only for pre spurious dragon hardforks where existing and empty were two separate states.
529        /// it became same state after EIP-161: State trie clearing
530        const LoadedAsNotExisting = 0b00001000;
531        /// used to mark account as cold.
532        /// It is used only in local scope and it is reset on account loading.
533        const Cold = 0b00010000;
534    }
535}
536
537impl AccountStatus {
538    /// Returns true if the account status is touched.
539    #[inline]
540    pub const fn is_touched(&self) -> bool {
541        self.contains(AccountStatus::Touched)
542    }
543}
544
545impl Default for AccountStatus {
546    fn default() -> Self {
547        AccountStatus::empty()
548    }
549}
550
551/// This type keeps track of the current value of a storage slot.
552#[derive(Debug, Clone, Default, PartialEq, Eq)]
553#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
554pub struct EvmStorageSlot {
555    /// Original value of the storage slot
556    pub original_value: StorageValue,
557    /// Present value of the storage slot
558    pub present_value: StorageValue,
559    /// Transaction id, used to track when storage slot was made warm.
560    pub transaction_id: TransactionId,
561    /// Represents if the storage slot is cold
562    pub is_cold: bool,
563}
564
565impl EvmStorageSlot {
566    /// Creates a new _unchanged_ `EvmStorageSlot` for the given value.
567    pub const fn new(original: StorageValue, transaction_id: TransactionId) -> Self {
568        Self {
569            original_value: original,
570            present_value: original,
571            transaction_id,
572            is_cold: false,
573        }
574    }
575
576    /// Creates a new _changed_ `EvmStorageSlot`.
577    pub const fn new_changed(
578        original_value: StorageValue,
579        present_value: StorageValue,
580        transaction_id: TransactionId,
581    ) -> Self {
582        Self {
583            original_value,
584            present_value,
585            transaction_id,
586            is_cold: false,
587        }
588    }
589    /// Returns true if the present value differs from the original value.
590    pub fn is_changed(&self) -> bool {
591        self.original_value != self.present_value
592    }
593
594    /// Returns the original value of the storage slot.
595    #[inline]
596    pub const fn original_value(&self) -> StorageValue {
597        self.original_value
598    }
599
600    /// Returns the current value of the storage slot.
601    #[inline]
602    pub const fn present_value(&self) -> StorageValue {
603        self.present_value
604    }
605
606    /// Marks the storage slot as cold. Does not change transaction_id.
607    #[inline]
608    pub const fn mark_cold(&mut self) {
609        self.is_cold = true;
610    }
611
612    /// Is storage slot cold for given transaction id.
613    #[inline]
614    pub const fn is_cold_transaction_id(&self, transaction_id: TransactionId) -> bool {
615        self.transaction_id.get() != transaction_id.get() || self.is_cold
616    }
617
618    /// Marks the storage slot as warm and sets transaction_id to the given value
619    ///
620    ///
621    /// Returns false if old transition_id is different from given id or in case they are same return `Self::is_cold` value.
622    #[inline]
623    pub const fn mark_warm_with_transaction_id(&mut self, transaction_id: TransactionId) -> bool {
624        let is_cold = self.is_cold_transaction_id(transaction_id);
625        // Re-baseline the EIP-2200 `original_value` only when the slot belongs to a *previous*
626        // transaction (transaction id mismatch). `is_cold` also covers a slot flagged cold
627        // within the same transaction (e.g. a reverted `StorageWarmed` entry, or an explicit
628        // `mark_cold`); those must keep the original value captured at the start of this
629        // transaction. The committed/original value is tx-scoped, not access-list-scoped.
630        if self.transaction_id.get() != transaction_id.get() {
631            self.original_value = self.present_value;
632        }
633        self.transaction_id = transaction_id;
634        self.is_cold = false;
635        is_cold
636    }
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::EvmStorageSlot;
643    use primitives::{StorageKey, KECCAK_EMPTY, U256};
644
645    #[test]
646    fn account_is_empty_balance() {
647        let mut account = Account::default();
648        assert!(account.is_empty());
649
650        account.info.balance = U256::from(1);
651        assert!(!account.is_empty());
652
653        account.info.balance = U256::ZERO;
654        assert!(account.is_empty());
655    }
656
657    #[test]
658    fn account_is_empty_nonce() {
659        let mut account = Account::default();
660        assert!(account.is_empty());
661
662        account.info.nonce = 1;
663        assert!(!account.is_empty());
664
665        account.info.nonce = 0;
666        assert!(account.is_empty());
667    }
668
669    #[test]
670    fn account_is_empty_code_hash() {
671        let mut account = Account::default();
672        assert!(account.is_empty());
673
674        account.info.code_hash = [1; 32].into();
675        assert!(!account.is_empty());
676
677        account.info.code_hash = [0; 32].into();
678        assert!(account.is_empty());
679
680        account.info.code_hash = KECCAK_EMPTY;
681        assert!(account.is_empty());
682    }
683
684    #[test]
685    fn account_state() {
686        let mut account = Account::default();
687
688        assert!(!account.is_touched());
689        assert!(!account.is_selfdestructed());
690
691        account.mark_touch();
692        assert!(account.is_touched());
693        assert!(!account.is_selfdestructed());
694
695        account.mark_selfdestruct();
696        assert!(account.is_touched());
697        assert!(account.is_selfdestructed());
698
699        account.unmark_selfdestruct();
700        assert!(account.is_touched());
701        assert!(!account.is_selfdestructed());
702    }
703
704    #[test]
705    fn account_is_cold() {
706        let mut account = Account::default();
707
708        // Account is not cold by default
709        assert!(!account.status.contains(crate::AccountStatus::Cold));
710
711        // When marking warm account as warm again, it should return false
712        assert!(!account.mark_warm_with_transaction_id(TransactionId::ZERO));
713
714        // Mark account as cold
715        account.mark_cold();
716
717        // Account is cold
718        assert!(account.status.contains(crate::AccountStatus::Cold));
719
720        // When marking cold account as warm, it should return true
721        assert!(account.mark_warm_with_transaction_id(TransactionId::ZERO));
722    }
723
724    #[test]
725    fn test_account_with_info() {
726        let info = AccountInfo::default();
727        let account = Account::default().with_info(info.clone());
728
729        assert_eq!(account.info, info);
730        assert_eq!(account.storage, HashMap::default());
731        assert_eq!(account.status, AccountStatus::empty());
732    }
733
734    #[test]
735    fn test_account_with_storage() {
736        let mut storage = HashMap::<StorageKey, EvmStorageSlot>::default();
737        let key1 = StorageKey::from(1);
738        let key2 = StorageKey::from(2);
739        let slot1 = EvmStorageSlot::new(StorageValue::from(10), TransactionId::ZERO);
740        let slot2 = EvmStorageSlot::new(StorageValue::from(20), TransactionId::ZERO);
741
742        storage.insert(key1, slot1.clone());
743        storage.insert(key2, slot2.clone());
744
745        let account = Account::default().with_storage(storage.clone().into_iter());
746
747        assert_eq!(account.storage.len(), 2);
748        assert_eq!(account.storage.get(&key1), Some(&slot1));
749        assert_eq!(account.storage.get(&key2), Some(&slot2));
750    }
751
752    #[test]
753    fn test_account_with_selfdestruct_mark() {
754        let account = Account::default().with_selfdestruct_mark();
755
756        assert!(account.is_selfdestructed());
757        assert!(!account.is_touched());
758        assert!(!account.is_created());
759    }
760
761    #[test]
762    #[cfg(feature = "serde")]
763    fn test_account_serialize_deserialize() {
764        let account = Account::default().with_selfdestruct_mark();
765        let serialized = serde_json::to_string(&account).unwrap();
766        let deserialized: Account = serde_json::from_str(&serialized).unwrap();
767        assert_eq!(account, deserialized);
768    }
769
770    #[test]
771    #[cfg(feature = "serde")]
772    fn test_account_postcard_round_trip() {
773        // Positional formats carry no field names, so a `Serialize`/`Deserialize` field-order
774        // mismatch is invisible to the JSON tests above and a default account round-trips by
775        // accident. Populate every field with a distinct non-default value, `original_info`
776        // differing from `info`, to pin each field's position.
777        let mut account = Account::from(AccountInfo {
778            nonce: 5,
779            ..AccountInfo::default()
780        });
781        account.info.nonce = 7;
782        account.transaction_id = TransactionId::new(3).unwrap();
783        account.storage.insert(
784            StorageKey::from(42u64),
785            EvmStorageSlot {
786                original_value: U256::from(1u64),
787                present_value: U256::from(2u64),
788                transaction_id: account.transaction_id,
789                is_cold: false,
790            },
791        );
792        account.status = AccountStatus::Touched;
793
794        let bytes = postcard::to_allocvec(&account).unwrap();
795        let decoded: Account = postcard::from_bytes(&bytes).unwrap();
796
797        assert_eq!(account, decoded);
798    }
799
800    #[test]
801    #[cfg(feature = "serde")]
802    fn test_account_original_info_none_roundtrip() {
803        let account = Account::new_not_existing(TransactionId::new(2).unwrap());
804        assert!(account.original_info.is_none());
805        let serialized = serde_json::to_string(&account).unwrap();
806        let deserialized: Account = serde_json::from_str(&serialized).unwrap();
807        assert!(deserialized.original_info.is_none());
808        assert_eq!(account, deserialized);
809    }
810
811    #[test]
812    #[cfg(feature = "serde")]
813    fn test_account_deserialize_original_info_missing_null_present() {
814        let code = r#"{"LegacyAnalyzed":{"bytecode":"0x00","original_len":0,"jump_table":{"order":"bitvec::order::Lsb0","head":{"width":8,"index":0},"bits":0,"data":[]}}}"#;
815        let info = format!(
816            r#"{{"balance":"0x2386f26fc10000","nonce":1,"code_hash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","code":{code}}}"#
817        );
818
819        // Missing field (old format): original_info = Some(info.clone()).
820        let json =
821            format!(r#"{{"info":{info},"transaction_id":0,"storage":{{}},"status":"Touched"}}"#);
822        let acct: Account = serde_json::from_str(&json).unwrap();
823        assert!(acct.original_info.is_some());
824        assert_eq!(acct.original_info(), acct.info);
825
826        // Null (new format): original_info = None (default).
827        let json = format!(
828            r#"{{"info":{info},"original_info":null,"transaction_id":0,"storage":{{}},"status":"Touched"}}"#
829        );
830        let acct: Account = serde_json::from_str(&json).unwrap();
831        assert!(acct.original_info.is_none());
832
833        // Present value.
834        let original = r#"{"balance":"0x0","nonce":0,"code_hash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","code":null}"#;
835        let json = format!(
836            r#"{{"info":{info},"original_info":{original},"transaction_id":0,"storage":{{}},"status":"Touched"}}"#
837        );
838        let acct: Account = serde_json::from_str(&json).unwrap();
839        assert!(acct.original_info.is_some());
840        assert_eq!(acct.original_info().nonce, 0);
841        assert_eq!(acct.original_info().balance, U256::ZERO);
842    }
843
844    #[test]
845    fn test_account_with_touched_mark() {
846        let account = Account::default().with_touched_mark();
847
848        assert!(!account.is_selfdestructed());
849        assert!(account.is_touched());
850        assert!(!account.is_created());
851    }
852
853    #[test]
854    fn test_account_with_created_mark() {
855        let account = Account::default().with_created_mark();
856
857        assert!(!account.is_selfdestructed());
858        assert!(!account.is_touched());
859        assert!(account.is_created());
860    }
861
862    #[test]
863    fn test_account_with_cold_mark() {
864        let account = Account::default().with_cold_mark();
865
866        assert!(account.status.contains(AccountStatus::Cold));
867    }
868
869    #[test]
870    fn test_storage_mark_warm_with_transaction_id() {
871        let tx_zero = TransactionId::ZERO;
872        let tx_one = TransactionId::new(1).unwrap();
873        let mut slot = EvmStorageSlot::new(U256::ZERO, tx_zero);
874        slot.is_cold = true;
875        slot.transaction_id = tx_zero;
876        assert!(slot.mark_warm_with_transaction_id(tx_one));
877
878        slot.is_cold = false;
879        slot.transaction_id = tx_zero;
880        assert!(slot.mark_warm_with_transaction_id(tx_one));
881
882        slot.is_cold = true;
883        slot.transaction_id = tx_one;
884        assert!(slot.mark_warm_with_transaction_id(tx_one));
885
886        slot.is_cold = false;
887        slot.transaction_id = tx_one;
888        // Only if transaction id is same and is_cold is false, return false.
889        assert!(!slot.mark_warm_with_transaction_id(tx_one));
890    }
891
892    #[test]
893    fn test_account_with_warm_mark() {
894        // Start with a cold account
895        let cold_account = Account::default().with_cold_mark();
896        assert!(cold_account.status.contains(AccountStatus::Cold));
897
898        // Use with_warm_mark to warm it
899        let (warm_account, was_cold) = cold_account.with_warm_mark(TransactionId::ZERO);
900
901        // Check that it's now warm and previously was cold
902        assert!(!warm_account.status.contains(AccountStatus::Cold));
903        assert!(was_cold);
904
905        // Try with an already warm account
906        let (still_warm_account, was_cold) = warm_account.with_warm_mark(TransactionId::ZERO);
907        assert!(!still_warm_account.status.contains(AccountStatus::Cold));
908        assert!(!was_cold);
909    }
910
911    #[test]
912    fn test_account_with_warm() {
913        // Start with a cold account
914        let cold_account = Account::default().with_cold_mark();
915        assert!(cold_account.status.contains(AccountStatus::Cold));
916
917        // Use with_warm to warm it
918        let warm_account = cold_account.with_warm(TransactionId::ZERO);
919
920        // Check that it's now warm
921        assert!(!warm_account.status.contains(AccountStatus::Cold));
922    }
923
924    #[test]
925    fn test_account_builder_chaining() {
926        let info = AccountInfo {
927            nonce: 5,
928            ..AccountInfo::default()
929        };
930
931        let slot_key = StorageKey::from(42);
932        let slot_value = EvmStorageSlot::new(StorageValue::from(123), TransactionId::ZERO);
933        let mut storage = HashMap::<StorageKey, EvmStorageSlot>::default();
934        storage.insert(slot_key, slot_value.clone());
935
936        // Chain multiple builder methods together
937        let account = Account::default()
938            .with_info(info.clone())
939            .with_storage(storage.into_iter())
940            .with_created_mark()
941            .with_touched_mark()
942            .with_cold_mark()
943            .with_warm(TransactionId::ZERO);
944
945        // Verify all modifications were applied
946        assert_eq!(account.info, info);
947        assert_eq!(account.storage.get(&slot_key), Some(&slot_value));
948        assert!(account.is_created());
949        assert!(account.is_touched());
950        assert!(!account.status.contains(AccountStatus::Cold));
951    }
952
953    #[test]
954    fn test_account_is_cold_transaction_id() {
955        let tx_zero = TransactionId::ZERO;
956        let tx_one = TransactionId::new(1).unwrap();
957        let mut account = Account::default();
958        // only case where it is warm.
959        assert!(!account.is_cold_transaction_id(tx_zero));
960
961        // all other cases are cold
962        assert!(account.is_cold_transaction_id(tx_one));
963        account.mark_cold();
964        assert!(account.is_cold_transaction_id(tx_zero));
965        assert!(account.is_cold_transaction_id(tx_one));
966    }
967}