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    /// Marks the account as newly created.
204    #[inline]
205    pub fn mark_created(&mut self) {
206        self.status |= AccountStatus::Created;
207    }
208
209    /// Unmarks the created flag.
210    #[inline]
211    pub fn unmark_created(&mut self) {
212        self.status -= AccountStatus::Created;
213    }
214
215    /// Marks the account as cold.
216    #[inline]
217    pub fn mark_cold(&mut self) {
218        self.status |= AccountStatus::Cold;
219    }
220
221    /// Is account warm for given transaction id.
222    #[inline]
223    pub const fn is_cold_transaction_id(&self, transaction_id: TransactionId) -> bool {
224        self.transaction_id.get() != transaction_id.get()
225            || self.status.contains(AccountStatus::Cold)
226    }
227
228    /// Marks the account as warm and return true if it was previously cold.
229    #[inline]
230    pub fn mark_warm_with_transaction_id(&mut self, transaction_id: TransactionId) -> bool {
231        let is_cold = self.is_cold_transaction_id(transaction_id);
232        self.status -= AccountStatus::Cold;
233        self.transaction_id = transaction_id;
234        is_cold
235    }
236
237    /// Is account locally created
238    #[inline]
239    pub const fn is_created_locally(&self) -> bool {
240        self.status.contains(AccountStatus::CreatedLocal)
241    }
242
243    /// Is account locally selfdestructed
244    #[inline]
245    pub const fn is_selfdestructed_locally(&self) -> bool {
246        self.status.contains(AccountStatus::SelfDestructedLocal)
247    }
248
249    /// Selfdestruct the account by clearing its storage and resetting its account info
250    #[inline]
251    pub fn selfdestruct(&mut self) {
252        self.storage.clear();
253        self.info = AccountInfo::default();
254    }
255
256    /// Mark account as locally created and mark global created flag.
257    ///
258    /// Returns true if it is created globally for first time.
259    #[inline]
260    pub fn mark_created_locally(&mut self) -> bool {
261        self.mark_local_and_global(AccountStatus::CreatedLocal, AccountStatus::Created)
262    }
263
264    /// Unmark account as locally created
265    #[inline]
266    pub fn unmark_created_locally(&mut self) {
267        self.status -= AccountStatus::CreatedLocal;
268    }
269
270    /// Mark account as locally and globally selfdestructed
271    #[inline]
272    pub fn mark_selfdestructed_locally(&mut self) -> bool {
273        self.mark_local_and_global(
274            AccountStatus::SelfDestructedLocal,
275            AccountStatus::SelfDestructed,
276        )
277    }
278
279    #[inline]
280    fn mark_local_and_global(
281        &mut self,
282        local_flag: AccountStatus,
283        global_flag: AccountStatus,
284    ) -> bool {
285        self.status |= local_flag;
286        let is_global_first_time = !self.status.contains(global_flag);
287        self.status |= global_flag;
288        is_global_first_time
289    }
290
291    /// Unmark account as locally selfdestructed
292    #[inline]
293    pub fn unmark_selfdestructed_locally(&mut self) {
294        self.status -= AccountStatus::SelfDestructedLocal;
295    }
296
297    /// Is account loaded as not existing from database.
298    ///
299    /// This is needed for pre spurious dragon hardforks where
300    /// existing and empty were two separate states.
301    pub const fn is_loaded_as_not_existing(&self) -> bool {
302        self.status.contains(AccountStatus::LoadedAsNotExisting)
303    }
304
305    /// Is account loaded as not existing from database and not touched.
306    pub const fn is_loaded_as_not_existing_not_touched(&self) -> bool {
307        self.is_loaded_as_not_existing() && !self.is_touched()
308    }
309
310    /// Is account newly created in this transaction.
311    pub const fn is_created(&self) -> bool {
312        self.status.contains(AccountStatus::Created)
313    }
314
315    /// Is account empty, check if nonce and balance are zero and code is empty.
316    pub fn is_empty(&self) -> bool {
317        self.info.is_empty()
318    }
319
320    /// Returns an iterator over the storage slots that have been changed.
321    ///
322    /// See also [EvmStorageSlot::is_changed].
323    pub fn changed_storage_slots(&self) -> impl Iterator<Item = (&StorageKey, &EvmStorageSlot)> {
324        self.storage.iter().filter(|(_, slot)| slot.is_changed())
325    }
326
327    /// Sets account info and returns self for method chaining.
328    pub fn with_info(mut self, info: AccountInfo) -> Self {
329        self.info = info;
330        self
331    }
332
333    /// Populates storage from an iterator of storage slots and returns self for method chaining.
334    pub fn with_storage<I>(mut self, storage_iter: I) -> Self
335    where
336        I: Iterator<Item = (StorageKey, EvmStorageSlot)>,
337    {
338        for (key, slot) in storage_iter {
339            self.storage.insert(key, slot);
340        }
341        self
342    }
343
344    /// Marks the account as self destructed and returns self for method chaining.
345    pub fn with_selfdestruct_mark(mut self) -> Self {
346        self.mark_selfdestruct();
347        self
348    }
349
350    /// Marks the account as touched and returns self for method chaining.
351    pub fn with_touched_mark(mut self) -> Self {
352        self.mark_touch();
353        self
354    }
355
356    /// Marks the account as newly created and returns self for method chaining.
357    pub fn with_created_mark(mut self) -> Self {
358        self.mark_created();
359        self
360    }
361
362    /// Marks the account as cold and returns self for method chaining.
363    pub fn with_cold_mark(mut self) -> Self {
364        self.mark_cold();
365        self
366    }
367
368    /// Marks the account as warm (not cold) and returns self for method chaining.
369    /// Also returns whether the account was previously cold.
370    pub fn with_warm_mark(mut self, transaction_id: TransactionId) -> (Self, bool) {
371        let was_cold = self.mark_warm_with_transaction_id(transaction_id);
372        (self, was_cold)
373    }
374
375    /// Variant of with_warm_mark that doesn't return the previous state.
376    pub fn with_warm(mut self, transaction_id: TransactionId) -> Self {
377        self.mark_warm_with_transaction_id(transaction_id);
378        self
379    }
380}
381
382impl From<AccountInfo> for Account {
383    fn from(info: AccountInfo) -> Self {
384        let original_info = if info.is_default() {
385            None
386        } else {
387            Some(Box::new(info.clone()))
388        };
389        Self {
390            info,
391            original_info,
392            transaction_id: TransactionId::ZERO,
393            storage: HashMap::default(),
394            status: AccountStatus::empty(),
395        }
396    }
397}
398
399#[cfg(feature = "serde")]
400mod serde_impl {
401    use super::*;
402    use serde::Deserialize;
403
404    /// Distinguishes missing field (old format) from explicit `null` (new format).
405    #[derive(Default)]
406    enum MaybeOriginalInfo {
407        /// Field was missing from JSON (old format).
408        #[default]
409        Missing,
410        /// Present in JSON: `null` means default, `Some` is the value.
411        Present(Option<AccountInfo>),
412    }
413
414    impl<'de> Deserialize<'de> for MaybeOriginalInfo {
415        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
416        where
417            D: serde::Deserializer<'de>,
418        {
419            Option::<AccountInfo>::deserialize(deserializer).map(MaybeOriginalInfo::Present)
420        }
421    }
422
423    #[derive(Deserialize)]
424    struct AccountSerde {
425        info: AccountInfo,
426        #[serde(default)]
427        original_info: MaybeOriginalInfo,
428        storage: EvmStorage,
429        transaction_id: TransactionId,
430        status: AccountStatus,
431    }
432
433    impl<'de> Deserialize<'de> for super::Account {
434        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
435        where
436            D: serde::Deserializer<'de>,
437        {
438            let AccountSerde {
439                info,
440                original_info,
441                storage,
442                transaction_id,
443                status,
444            } = Deserialize::deserialize(deserializer)?;
445
446            let original_info = match original_info {
447                // Old format: field missing → use info as original.
448                MaybeOriginalInfo::Missing => Some(Box::new(info.clone())),
449                // New format: null → None (default).
450                MaybeOriginalInfo::Present(None) => None,
451                // New format: explicit value.
452                MaybeOriginalInfo::Present(Some(oi)) => Some(Box::new(oi)),
453            };
454
455            Ok(Account {
456                info,
457                original_info,
458                storage,
459                transaction_id,
460                status,
461            })
462        }
463    }
464}
465
466// The `bitflags!` macro generates `struct`s that manage a set of flags.
467bitflags! {
468    /// Account status flags. Generated by bitflags crate.
469    ///
470    /// With multi transaction feature there is a need to have both global and local fields.
471    /// Global across multiple transaction and local across one transaction execution.
472    ///
473    /// Empty state without any flags set represent account that is loaded from db but not interacted with.
474    ///
475    /// `Touched` flag is used by database to check if account is potentially changed in some way.
476    /// Additionally, after EIP-161 touch on empty-existing account would remove this account from state
477    /// after transaction execution ends. Touch can span across multiple transactions as it is needed
478    /// to be marked only once so it is safe to have only one global flag.
479    /// Only first touch have different behaviour from others, and touch in first transaction will invalidate
480    /// touch functionality in next transactions.
481    ///
482    /// `Created` flag is used to mark account as newly created in this transaction. This is used for optimization
483    /// where if this flag is set we will not access database to fetch storage values.
484    ///
485    /// `CreatedLocal` flag is used after cancun to enable selfdestruct cleanup if account is created in same transaction.
486    ///
487    /// `Selfdestructed` flag is used to mark account as selfdestructed. On multiple calls this flag is preserved
488    /// and on revert will stay selfdestructed.
489    ///
490    /// `SelfdestructLocal` is needed to award refund on first selfdestruct call. This flag is cleared on account loading.
491    /// Over multiple transaction account can be selfdestructed in one tx, created in second tx and selfdestructed again in
492    /// third tx.
493    /// Additionally if account is loaded in second tx, storage and account that was destroyed in first tx needs to be cleared.
494    ///
495    /// `LoadedAsNotExisting` is used to mark account as loaded from database but with `balance == 0 && nonce == 0 && code = 0x`.
496    /// This flag is fine to span across multiple transactions as it interucts with `Touched` flag this is used in global scope.
497    ///
498    /// `CreatedLocal`, `SelfdestructedLocal` and `Cold` flags are reset on first account loading of local scope.
499    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
500    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
501    #[cfg_attr(feature = "serde", serde(transparent))]
502    pub struct AccountStatus: u8 {
503        /// When account is newly created we will not access database
504        /// to fetch storage values.
505        const Created = 0b00000001;
506        /// When accounts gets loaded this flag is set to false. Create will always be true if CreatedLocal is true.
507        const CreatedLocal = 0b10000000;
508        /// If account is marked for self destruction.
509        const SelfDestructed = 0b00000010;
510        /// If account is marked for self destruction.
511        const SelfDestructedLocal = 0b01000000;
512        /// Only when account is marked as touched we will save it to database.
513        /// Additionally first touch on empty existing account (After EIP-161) will mark it
514        /// for removal from state after transaction execution.
515        const Touched = 0b00000100;
516        /// used only for pre spurious dragon hardforks where existing and empty were two separate states.
517        /// it became same state after EIP-161: State trie clearing
518        const LoadedAsNotExisting = 0b00001000;
519        /// used to mark account as cold.
520        /// It is used only in local scope and it is reset on account loading.
521        const Cold = 0b00010000;
522    }
523}
524
525impl AccountStatus {
526    /// Returns true if the account status is touched.
527    #[inline]
528    pub const fn is_touched(&self) -> bool {
529        self.contains(AccountStatus::Touched)
530    }
531}
532
533impl Default for AccountStatus {
534    fn default() -> Self {
535        AccountStatus::empty()
536    }
537}
538
539/// This type keeps track of the current value of a storage slot.
540#[derive(Debug, Clone, Default, PartialEq, Eq)]
541#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
542pub struct EvmStorageSlot {
543    /// Original value of the storage slot
544    pub original_value: StorageValue,
545    /// Present value of the storage slot
546    pub present_value: StorageValue,
547    /// Transaction id, used to track when storage slot was made warm.
548    pub transaction_id: TransactionId,
549    /// Represents if the storage slot is cold
550    pub is_cold: bool,
551}
552
553impl EvmStorageSlot {
554    /// Creates a new _unchanged_ `EvmStorageSlot` for the given value.
555    pub const fn new(original: StorageValue, transaction_id: TransactionId) -> Self {
556        Self {
557            original_value: original,
558            present_value: original,
559            transaction_id,
560            is_cold: false,
561        }
562    }
563
564    /// Creates a new _changed_ `EvmStorageSlot`.
565    pub const fn new_changed(
566        original_value: StorageValue,
567        present_value: StorageValue,
568        transaction_id: TransactionId,
569    ) -> Self {
570        Self {
571            original_value,
572            present_value,
573            transaction_id,
574            is_cold: false,
575        }
576    }
577    /// Returns true if the present value differs from the original value.
578    pub fn is_changed(&self) -> bool {
579        self.original_value != self.present_value
580    }
581
582    /// Returns the original value of the storage slot.
583    #[inline]
584    pub const fn original_value(&self) -> StorageValue {
585        self.original_value
586    }
587
588    /// Returns the current value of the storage slot.
589    #[inline]
590    pub const fn present_value(&self) -> StorageValue {
591        self.present_value
592    }
593
594    /// Marks the storage slot as cold. Does not change transaction_id.
595    #[inline]
596    pub const fn mark_cold(&mut self) {
597        self.is_cold = true;
598    }
599
600    /// Is storage slot cold for given transaction id.
601    #[inline]
602    pub const fn is_cold_transaction_id(&self, transaction_id: TransactionId) -> bool {
603        self.transaction_id.get() != transaction_id.get() || self.is_cold
604    }
605
606    /// Marks the storage slot as warm and sets transaction_id to the given value
607    ///
608    ///
609    /// Returns false if old transition_id is different from given id or in case they are same return `Self::is_cold` value.
610    #[inline]
611    pub const fn mark_warm_with_transaction_id(&mut self, transaction_id: TransactionId) -> bool {
612        let is_cold = self.is_cold_transaction_id(transaction_id);
613        if is_cold {
614            // if slot is cold original value should be reset to present value.
615            self.original_value = self.present_value;
616        }
617        self.transaction_id = transaction_id;
618        self.is_cold = false;
619        is_cold
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::EvmStorageSlot;
627    use primitives::{StorageKey, KECCAK_EMPTY, U256};
628
629    #[test]
630    fn account_is_empty_balance() {
631        let mut account = Account::default();
632        assert!(account.is_empty());
633
634        account.info.balance = U256::from(1);
635        assert!(!account.is_empty());
636
637        account.info.balance = U256::ZERO;
638        assert!(account.is_empty());
639    }
640
641    #[test]
642    fn account_is_empty_nonce() {
643        let mut account = Account::default();
644        assert!(account.is_empty());
645
646        account.info.nonce = 1;
647        assert!(!account.is_empty());
648
649        account.info.nonce = 0;
650        assert!(account.is_empty());
651    }
652
653    #[test]
654    fn account_is_empty_code_hash() {
655        let mut account = Account::default();
656        assert!(account.is_empty());
657
658        account.info.code_hash = [1; 32].into();
659        assert!(!account.is_empty());
660
661        account.info.code_hash = [0; 32].into();
662        assert!(account.is_empty());
663
664        account.info.code_hash = KECCAK_EMPTY;
665        assert!(account.is_empty());
666    }
667
668    #[test]
669    fn account_state() {
670        let mut account = Account::default();
671
672        assert!(!account.is_touched());
673        assert!(!account.is_selfdestructed());
674
675        account.mark_touch();
676        assert!(account.is_touched());
677        assert!(!account.is_selfdestructed());
678
679        account.mark_selfdestruct();
680        assert!(account.is_touched());
681        assert!(account.is_selfdestructed());
682
683        account.unmark_selfdestruct();
684        assert!(account.is_touched());
685        assert!(!account.is_selfdestructed());
686    }
687
688    #[test]
689    fn account_is_cold() {
690        let mut account = Account::default();
691
692        // Account is not cold by default
693        assert!(!account.status.contains(crate::AccountStatus::Cold));
694
695        // When marking warm account as warm again, it should return false
696        assert!(!account.mark_warm_with_transaction_id(TransactionId::ZERO));
697
698        // Mark account as cold
699        account.mark_cold();
700
701        // Account is cold
702        assert!(account.status.contains(crate::AccountStatus::Cold));
703
704        // When marking cold account as warm, it should return true
705        assert!(account.mark_warm_with_transaction_id(TransactionId::ZERO));
706    }
707
708    #[test]
709    fn test_account_with_info() {
710        let info = AccountInfo::default();
711        let account = Account::default().with_info(info.clone());
712
713        assert_eq!(account.info, info);
714        assert_eq!(account.storage, HashMap::default());
715        assert_eq!(account.status, AccountStatus::empty());
716    }
717
718    #[test]
719    fn test_account_with_storage() {
720        let mut storage = HashMap::<StorageKey, EvmStorageSlot>::default();
721        let key1 = StorageKey::from(1);
722        let key2 = StorageKey::from(2);
723        let slot1 = EvmStorageSlot::new(StorageValue::from(10), TransactionId::ZERO);
724        let slot2 = EvmStorageSlot::new(StorageValue::from(20), TransactionId::ZERO);
725
726        storage.insert(key1, slot1.clone());
727        storage.insert(key2, slot2.clone());
728
729        let account = Account::default().with_storage(storage.clone().into_iter());
730
731        assert_eq!(account.storage.len(), 2);
732        assert_eq!(account.storage.get(&key1), Some(&slot1));
733        assert_eq!(account.storage.get(&key2), Some(&slot2));
734    }
735
736    #[test]
737    fn test_account_with_selfdestruct_mark() {
738        let account = Account::default().with_selfdestruct_mark();
739
740        assert!(account.is_selfdestructed());
741        assert!(!account.is_touched());
742        assert!(!account.is_created());
743    }
744
745    #[test]
746    #[cfg(feature = "serde")]
747    fn test_account_serialize_deserialize() {
748        let account = Account::default().with_selfdestruct_mark();
749        let serialized = serde_json::to_string(&account).unwrap();
750        let deserialized: Account = serde_json::from_str(&serialized).unwrap();
751        assert_eq!(account, deserialized);
752    }
753
754    #[test]
755    #[cfg(feature = "serde")]
756    fn test_account_original_info_none_roundtrip() {
757        let account = Account::new_not_existing(TransactionId::new(2).unwrap());
758        assert!(account.original_info.is_none());
759        let serialized = serde_json::to_string(&account).unwrap();
760        let deserialized: Account = serde_json::from_str(&serialized).unwrap();
761        assert!(deserialized.original_info.is_none());
762        assert_eq!(account, deserialized);
763    }
764
765    #[test]
766    #[cfg(feature = "serde")]
767    fn test_account_deserialize_original_info_missing_null_present() {
768        let code = r#"{"LegacyAnalyzed":{"bytecode":"0x00","original_len":0,"jump_table":{"order":"bitvec::order::Lsb0","head":{"width":8,"index":0},"bits":0,"data":[]}}}"#;
769        let info = format!(
770            r#"{{"balance":"0x2386f26fc10000","nonce":1,"code_hash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","code":{code}}}"#
771        );
772
773        // Missing field (old format): original_info = Some(info.clone()).
774        let json =
775            format!(r#"{{"info":{info},"transaction_id":0,"storage":{{}},"status":"Touched"}}"#);
776        let acct: Account = serde_json::from_str(&json).unwrap();
777        assert!(acct.original_info.is_some());
778        assert_eq!(acct.original_info(), acct.info);
779
780        // Null (new format): original_info = None (default).
781        let json = format!(
782            r#"{{"info":{info},"original_info":null,"transaction_id":0,"storage":{{}},"status":"Touched"}}"#
783        );
784        let acct: Account = serde_json::from_str(&json).unwrap();
785        assert!(acct.original_info.is_none());
786
787        // Present value.
788        let original = r#"{"balance":"0x0","nonce":0,"code_hash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","code":null}"#;
789        let json = format!(
790            r#"{{"info":{info},"original_info":{original},"transaction_id":0,"storage":{{}},"status":"Touched"}}"#
791        );
792        let acct: Account = serde_json::from_str(&json).unwrap();
793        assert!(acct.original_info.is_some());
794        assert_eq!(acct.original_info().nonce, 0);
795        assert_eq!(acct.original_info().balance, U256::ZERO);
796    }
797
798    #[test]
799    fn test_account_with_touched_mark() {
800        let account = Account::default().with_touched_mark();
801
802        assert!(!account.is_selfdestructed());
803        assert!(account.is_touched());
804        assert!(!account.is_created());
805    }
806
807    #[test]
808    fn test_account_with_created_mark() {
809        let account = Account::default().with_created_mark();
810
811        assert!(!account.is_selfdestructed());
812        assert!(!account.is_touched());
813        assert!(account.is_created());
814    }
815
816    #[test]
817    fn test_account_with_cold_mark() {
818        let account = Account::default().with_cold_mark();
819
820        assert!(account.status.contains(AccountStatus::Cold));
821    }
822
823    #[test]
824    fn test_storage_mark_warm_with_transaction_id() {
825        let tx_zero = TransactionId::ZERO;
826        let tx_one = TransactionId::new(1).unwrap();
827        let mut slot = EvmStorageSlot::new(U256::ZERO, tx_zero);
828        slot.is_cold = true;
829        slot.transaction_id = tx_zero;
830        assert!(slot.mark_warm_with_transaction_id(tx_one));
831
832        slot.is_cold = false;
833        slot.transaction_id = tx_zero;
834        assert!(slot.mark_warm_with_transaction_id(tx_one));
835
836        slot.is_cold = true;
837        slot.transaction_id = tx_one;
838        assert!(slot.mark_warm_with_transaction_id(tx_one));
839
840        slot.is_cold = false;
841        slot.transaction_id = tx_one;
842        // Only if transaction id is same and is_cold is false, return false.
843        assert!(!slot.mark_warm_with_transaction_id(tx_one));
844    }
845
846    #[test]
847    fn test_account_with_warm_mark() {
848        // Start with a cold account
849        let cold_account = Account::default().with_cold_mark();
850        assert!(cold_account.status.contains(AccountStatus::Cold));
851
852        // Use with_warm_mark to warm it
853        let (warm_account, was_cold) = cold_account.with_warm_mark(TransactionId::ZERO);
854
855        // Check that it's now warm and previously was cold
856        assert!(!warm_account.status.contains(AccountStatus::Cold));
857        assert!(was_cold);
858
859        // Try with an already warm account
860        let (still_warm_account, was_cold) = warm_account.with_warm_mark(TransactionId::ZERO);
861        assert!(!still_warm_account.status.contains(AccountStatus::Cold));
862        assert!(!was_cold);
863    }
864
865    #[test]
866    fn test_account_with_warm() {
867        // Start with a cold account
868        let cold_account = Account::default().with_cold_mark();
869        assert!(cold_account.status.contains(AccountStatus::Cold));
870
871        // Use with_warm to warm it
872        let warm_account = cold_account.with_warm(TransactionId::ZERO);
873
874        // Check that it's now warm
875        assert!(!warm_account.status.contains(AccountStatus::Cold));
876    }
877
878    #[test]
879    fn test_account_builder_chaining() {
880        let info = AccountInfo {
881            nonce: 5,
882            ..AccountInfo::default()
883        };
884
885        let slot_key = StorageKey::from(42);
886        let slot_value = EvmStorageSlot::new(StorageValue::from(123), TransactionId::ZERO);
887        let mut storage = HashMap::<StorageKey, EvmStorageSlot>::default();
888        storage.insert(slot_key, slot_value.clone());
889
890        // Chain multiple builder methods together
891        let account = Account::default()
892            .with_info(info.clone())
893            .with_storage(storage.into_iter())
894            .with_created_mark()
895            .with_touched_mark()
896            .with_cold_mark()
897            .with_warm(TransactionId::ZERO);
898
899        // Verify all modifications were applied
900        assert_eq!(account.info, info);
901        assert_eq!(account.storage.get(&slot_key), Some(&slot_value));
902        assert!(account.is_created());
903        assert!(account.is_touched());
904        assert!(!account.status.contains(AccountStatus::Cold));
905    }
906
907    #[test]
908    fn test_account_is_cold_transaction_id() {
909        let tx_zero = TransactionId::ZERO;
910        let tx_one = TransactionId::new(1).unwrap();
911        let mut account = Account::default();
912        // only case where it is warm.
913        assert!(!account.is_cold_transaction_id(tx_zero));
914
915        // all other cases are cold
916        assert!(account.is_cold_transaction_id(tx_one));
917        account.mark_cold();
918        assert!(account.is_cold_transaction_id(tx_zero));
919        assert!(account.is_cold_transaction_id(tx_one));
920    }
921}