Skip to main content

ethrex_levm/
call_frame.rs

1use crate::{
2    account::LevmAccount,
3    constants::STACK_LIMIT,
4    errors::{ExceptionalHalt, InternalError, VMError},
5    memory::{CallData, Memory},
6    utils::restore_cache_state,
7    vm::VM,
8};
9use bytes::Bytes;
10use ethrex_common::seeded_hash::SeededHashMap;
11use ethrex_common::types::block_access_list::BlockAccessListCheckpoint;
12use ethrex_common::{Address, H256, U256, types::Code};
13use std::{
14    fmt,
15    hash::{Hash, Hasher},
16    hint::assert_unchecked,
17};
18
19/// [`u64`]s that make up a [`U256`]
20const U64_PER_U256: usize = U256::MAX.0.len();
21
22#[derive(Clone, PartialEq, Eq)]
23/// The EVM uses a stack-based architecture and does not use registers like some other VMs.
24///
25/// The specification says the stack is limited to 1024 items, aka. 32KiB, which is reasonable
26/// enough for allocating it all at once to make sense. Every time an item is pushed into the stack,
27/// its bounds have to be checked; by making the stack grow downwards, the underflow detection of
28/// the offset update operation can also be reused to check for stack overflow.
29///
30/// A few opcodes require pushing and/or popping multiple elements. The [`push`](Self::push) and
31/// [`pop`](Self::pop) methods support working with multiple elements instead of a single one,
32/// reducing the number of checks performed on the stack.
33pub struct Stack {
34    pub values: Box<[U256; STACK_LIMIT]>,
35    pub offset: usize,
36}
37
38impl Stack {
39    #[inline]
40    pub fn pop<const N: usize>(&mut self) -> Result<&[U256; N], ExceptionalHalt> {
41        // Compile-time check for stack underflow.
42        const {
43            assert!(N <= STACK_LIMIT);
44        }
45
46        // The following operation can never overflow as both `self.offset` and N are within
47        // STACK_LIMIT (1024).
48        let next_offset = self.offset.wrapping_add(N);
49
50        // The index cannot fail because `self.offset` is known to be valid. The `first_chunk()`
51        // method will ensure that `next_offset` is within `STACK_LIMIT`, so there's no need to
52        // check it again.
53        #[expect(unsafe_code)]
54        let values = unsafe {
55            self.values
56                .get_unchecked(self.offset..)
57                .first_chunk::<N>()
58                .ok_or(ExceptionalHalt::StackUnderflow)?
59        };
60        // Due to previous error check in first_chunk, next_offset is guaranteed to be < STACK_LIMIT
61        self.offset = next_offset;
62
63        Ok(values)
64    }
65
66    #[inline]
67    pub fn pop1(&mut self) -> Result<U256, ExceptionalHalt> {
68        let value = *self
69            .values
70            .get(self.offset)
71            .ok_or(ExceptionalHalt::StackUnderflow)?;
72        // The following operation can never overflow as both `self.offset` and N are within
73        // STACK_LIMIT (1024).
74        self.offset = self.offset.wrapping_add(1);
75
76        Ok(value)
77    }
78
79    /// Mutable reference to the top item without changing depth (one underflow check,
80    /// no `offset` write). For stack-neutral unary ops (ISZERO, NOT, CLZ), replacing
81    /// `pop1` + `push` with `*top_mut() = f(*top)` avoids the read-modify-write of the
82    /// shared `offset`, which is the per-opcode serial dependency that pins dispatch IPC.
83    #[inline]
84    pub fn top_mut(&mut self) -> Result<&mut U256, ExceptionalHalt> {
85        self.values
86            .get_mut(self.offset)
87            .ok_or(ExceptionalHalt::StackUnderflow)
88    }
89
90    /// Overwrites the top value without a bounds check.
91    ///
92    /// # Safety
93    /// The caller must have already validated that a top element exists (e.g. a
94    /// prior `top_mut()?`/`pop1()?` in the same stack-neutral opcode) and must
95    /// not have changed the stack depth since, so `self.offset` is in bounds.
96    #[expect(unsafe_code, reason = "top validated by caller; depth unchanged")]
97    #[inline]
98    pub unsafe fn set_top_unchecked(&mut self, value: U256) {
99        unsafe {
100            *self.values.get_unchecked_mut(self.offset) = value;
101        }
102    }
103
104    /// Pop the top value and return it together with a mutable reference to the new top.
105    /// For binary ops: `let (a, b) = pop1_and_top_mut()?; *b = f(a, *b)` writes the result
106    /// in place (one `offset` write instead of `pop::<2>` + `push`'s two), where `a` is the
107    /// original top and `*b` the original second operand.
108    #[inline]
109    pub fn pop1_and_top_mut(&mut self) -> Result<(U256, &mut U256), ExceptionalHalt> {
110        let a = self.pop1()?;
111        Ok((a, self.top_mut()?))
112    }
113
114    /// Push a single U256 value to the stack, faster than the generic push.
115    #[inline]
116    pub fn push(&mut self, value: U256) -> Result<(), ExceptionalHalt> {
117        // Since the stack grows downwards, when an offset underflow is detected the stack is
118        // overflowing.
119        let next_offset = self
120            .offset
121            .checked_sub(1)
122            .ok_or(ExceptionalHalt::StackOverflow)?;
123
124        // The following index cannot fail because `next_offset` has already been checked and
125        // `self.offset` is known to be within `STACK_LIMIT`.
126        // Store each limb individually so LLVM treats them as 4 independent i64 scalars.
127        // This prevents LLVM from grouping limbs[1..3] into a [24 x i8] alloca that would
128        // then need a memset + memcpy round-trip for values with known-zero upper limbs
129        // (e.g. PUSH1-PUSH31), allowing it to emit direct zero stores instead.
130        #[expect(unsafe_code, reason = "next_offset == self.offset - 1 >= 0")]
131        unsafe {
132            let slot = self.values.get_unchecked_mut(next_offset);
133            slot.0[0] = value.0[0];
134            slot.0[1] = value.0[1];
135            slot.0[2] = value.0[2];
136            slot.0[3] = value.0[3];
137        }
138        self.offset = next_offset;
139
140        Ok(())
141    }
142
143    #[inline]
144    pub fn push_zero(&mut self) -> Result<(), ExceptionalHalt> {
145        // Since the stack grows downwards, when an offset underflow is detected the stack is
146        // overflowing.
147        let next_offset = self
148            .offset
149            .checked_sub(1)
150            .ok_or(ExceptionalHalt::StackOverflow)?;
151
152        // The following index cannot fail because `next_offset` has already been checked and
153        // `self.offset` is known to be within `STACK_LIMIT`.
154        #[expect(unsafe_code, reason = "next_offset == self.offset - 1 >= 0")]
155        unsafe {
156            *self
157                .values
158                .get_unchecked_mut(next_offset)
159                .0
160                .as_mut_ptr()
161                .cast() = [0u64; U64_PER_U256];
162        }
163        self.offset = next_offset;
164
165        Ok(())
166    }
167
168    pub fn len(&self) -> usize {
169        // The following operation cannot underflow because `self.offset` is known to be less than
170        // or equal to `self.values.len()` (aka. `STACK_LIMIT`).
171        #[expect(clippy::arithmetic_side_effects)]
172        {
173            self.values.len() - self.offset
174        }
175    }
176
177    pub fn is_empty(&self) -> bool {
178        self.offset == self.values.len()
179    }
180
181    #[inline(always)]
182    pub fn swap<const N: usize>(&mut self) -> Result<(), ExceptionalHalt> {
183        // Compile-time check that ensures `self.offset + N` is safe,
184        // since self.offset is bounded by STACK_LIMIT
185        const {
186            assert!(STACK_LIMIT.checked_add(N).is_some());
187        }
188        #[expect(clippy::arithmetic_side_effects)]
189        let index = self.offset + N;
190
191        if index >= self.values.len() {
192            return Err(ExceptionalHalt::StackUnderflow);
193        }
194
195        #[expect(unsafe_code, reason = "self.offset always < STACK_LIMIT")]
196        unsafe {
197            assert_unchecked(self.offset < STACK_LIMIT)
198        };
199
200        self.values.swap(self.offset, index);
201        Ok(())
202    }
203
204    pub fn clear(&mut self) {
205        self.offset = STACK_LIMIT;
206    }
207
208    /// Pushes a copy of the value at depth N
209    #[inline]
210    pub fn dup<const N: usize>(&mut self) -> Result<(), ExceptionalHalt> {
211        // Compile-time check that ensures `self.offset + N` is safe,
212        // since self.offset is bounded by STACK_LIMIT
213        const {
214            assert!(STACK_LIMIT.checked_add(N).is_some());
215        }
216        #[expect(clippy::arithmetic_side_effects)]
217        let index = self.offset + N;
218        if index >= self.values.len() {
219            return Err(ExceptionalHalt::StackUnderflow);
220        }
221
222        self.offset = self
223            .offset
224            .checked_sub(1)
225            .ok_or(ExceptionalHalt::StackOverflow)?;
226
227        #[expect(unsafe_code, reason = "index < size, offset-1 >= 0")]
228        unsafe {
229            std::ptr::copy_nonoverlapping(
230                self.values.get_unchecked_mut(index).0.as_mut_ptr(),
231                self.values.get_unchecked_mut(self.offset).0.as_mut_ptr(),
232                U64_PER_U256,
233            );
234        }
235        Ok(())
236    }
237}
238
239impl Default for Stack {
240    fn default() -> Self {
241        Self {
242            values: Box::new([U256::zero(); STACK_LIMIT]),
243            offset: STACK_LIMIT,
244        }
245    }
246}
247
248impl fmt::Debug for Stack {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        struct StackValues<'a>(&'a [U256]);
251
252        impl fmt::Debug for StackValues<'_> {
253            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254                f.debug_list().entries(self.0.iter().rev()).finish()
255            }
256        }
257
258        #[expect(clippy::indexing_slicing)]
259        f.debug_tuple("Stack")
260            .field(&StackValues(&self.values[self.offset..]))
261            .finish()
262    }
263}
264
265impl Hash for Stack {
266    #[expect(
267        clippy::indexing_slicing,
268        reason = "offset is always within bounds of values"
269    )]
270    fn hash<H: Hasher>(&self, state: &mut H) {
271        self.values[self.offset..].hash(state);
272    }
273}
274
275#[derive(Debug)]
276/// A call frame, or execution environment, is the context in which
277/// the EVM is currently executing.
278/// One context can trigger another with opcodes like CALL or CREATE.
279/// Call frames relationships can be thought of as a parent-child relation.
280pub struct CallFrame {
281    /// Max gas a callframe can use
282    pub gas_limit: u64,
283    /// Keeps track of the remaining gas in the current context.
284    ///
285    /// This is a i64 for performance reasons, to allow faster gas cost substraction and checks.
286    ///
287    /// Additionally, gas limit won't be a problem since https://eips.ethereum.org/EIPS/eip-7825 limits it to 2^24, which is lower than i64::MAX.
288    pub gas_remaining: i64,
289    /// Program Counter
290    pub pc: usize,
291    /// Address of the account that sent the message
292    pub msg_sender: Address,
293    /// Address of the recipient of the message
294    pub to: Address,
295    /// Address of the code to execute. Usually the same as `to`, but can be different
296    pub code_address: Address,
297    /// Bytecode to execute.
298    /// Its hash field will be bogus for initcodes, as it is inaccessible to the VM
299    /// unless associated to an account, which doesn't happen for its initcode.
300    pub bytecode: Code,
301    /// Value sent along the transaction
302    pub msg_value: U256,
303    pub stack: Stack,
304    pub memory: Memory,
305    /// Data sent along the transaction. Empty in CREATE transactions.
306    pub calldata: CallData,
307    /// Return data of the CURRENT CONTEXT (see docs for more details)
308    pub output: Bytes,
309    /// Return data of the SUB-CONTEXT (see docs for more details)
310    pub sub_return_data: Bytes,
311    /// Indicates if current context is static (if it is, it can't alter state)
312    pub is_static: bool,
313    /// Call stack current depth
314    pub depth: usize,
315    /// This is set to true if the function that created this callframe is CREATE or CREATE2
316    pub is_create: bool,
317    /// Everytime we want to write an account during execution of a callframe we store the pre-write state so that we can restore if it reverts
318    pub call_frame_backup: CallFrameBackup,
319    /// Return data offset
320    pub ret_offset: usize,
321    /// Return data size
322    pub ret_size: usize,
323    /// If true then transfer value from caller to callee
324    pub should_transfer_value: bool,
325    /// EIP-8037: snapshot of VM.state_gas_used (signed) at child-frame entry.
326    /// Used to restore parent's state_gas_used on child revert.
327    pub state_gas_used_at_entry: i64,
328    /// EIP-8037: EELS per-frame `state_gas_spilled`; LIFO refund source;
329    /// propagated to parent on child success.
330    pub frame_state_gas_spilled: u64,
331    /// EIP-8037 (#3002): whether this CREATE/CREATE2 child's target address was
332    /// already alive (existed and non-empty) before the create mutated state.
333    /// Read in `handle_return_create` to refund the unconditional new-account
334    /// state gas on the success path (no new account leaf is created).
335    pub target_alive: bool,
336    /// EIP-8037: whether the parent charged new-account state gas for this CALL
337    /// (value transfer to an empty account). Refunded on child revert/error,
338    /// mirroring EELS `generic_call` `credit_state_gas_refund(NEW_ACCOUNT)`.
339    pub new_account_state_gas_charged: bool,
340}
341
342#[derive(Debug, Clone, Eq, PartialEq, Default)]
343pub struct CallFrameBackup {
344    /// Both maps are seeded: the
345    /// accounts and slots a frame touches are chosen by contract code.
346    pub original_accounts_info: SeededHashMap<Address, LevmAccount>,
347    pub original_account_storage_slots: SeededHashMap<Address, SeededHashMap<H256, U256>>,
348    /// BAL checkpoint for EIP-7928 - used to restore state changes on revert
349    /// while preserving touched_addresses.
350    pub bal_checkpoint: Option<BlockAccessListCheckpoint>,
351    /// Code hashes this frame inserted into the by-hash code cache
352    /// (`GeneralizedDatabase::codes`) for codes it deployed. Removed from the
353    /// cache on revert: a stale entry would make a later read of the same
354    /// hash (from a pre-existing account) hit the cache instead of the store,
355    /// hiding the read from execution-witness recording (EIP-8025).
356    pub inserted_code_hashes: Vec<H256>,
357}
358
359impl CallFrameBackup {
360    pub fn backup_account_info(
361        &mut self,
362        address: Address,
363        account: &LevmAccount,
364    ) -> Result<(), InternalError> {
365        self.original_accounts_info
366            .entry(address)
367            .or_insert_with(|| LevmAccount {
368                info: account.info.clone(),
369                storage: Default::default(),
370                status: account.status.clone(),
371                has_storage: account.has_storage,
372                exists: account.exists,
373            });
374
375        Ok(())
376    }
377
378    pub fn clear(&mut self) {
379        self.original_accounts_info.clear();
380        self.original_account_storage_slots.clear();
381        self.bal_checkpoint = None;
382        self.inserted_code_hashes.clear();
383    }
384
385    /// Merges `other` into `self`, per-address. For slots present in both,
386    /// `other`'s values win. Callers MUST pass the older/more-original backup
387    /// as `other` so the truly-original value is preserved (matches the
388    /// `or_insert` semantic in `backup_storage_slot`).
389    pub fn extend(&mut self, other: CallFrameBackup) {
390        // Per-slot merge: plain HashMap::extend would let `other`'s inner slot map
391        // replace `self`'s, dropping any slots `self` had for the same address.
392        for (address, other_storage) in other.original_account_storage_slots {
393            self.original_account_storage_slots
394                .entry(address)
395                .or_default()
396                .extend(other_storage);
397        }
398        self.original_accounts_info
399            .extend(other.original_accounts_info);
400        self.inserted_code_hashes.extend(other.inserted_code_hashes);
401        // Don't extend bal_checkpoint - it's specific to each call frame
402    }
403
404    /// Absorb another backup, keeping the EARLIEST original value for each
405    /// account/slot (first-seen-wins). Mirrors `merge_call_frame_backup_with_parent`.
406    /// Does NOT touch `bal_checkpoint` (set once at the tx level).
407    ///
408    /// Unlike `extend` (last-wins `HashMap::extend`), this preserves the value
409    /// that was already recorded, which is required when accumulating per-frame
410    /// backups into a tx-level rollback: the original to restore is the value
411    /// from before the FIRST frame that modified it.
412    pub fn absorb(&mut self, other: &CallFrameBackup) {
413        for (address, account) in other.original_accounts_info.iter() {
414            self.original_accounts_info
415                .entry(*address)
416                .or_insert_with(|| account.clone());
417        }
418        for (address, storage) in other.original_account_storage_slots.iter() {
419            let slot = self
420                .original_account_storage_slots
421                .entry(*address)
422                .or_default();
423            for (key, value) in storage.iter() {
424                slot.entry(*key).or_insert(*value);
425            }
426        }
427    }
428}
429
430impl CallFrame {
431    #[expect(
432        clippy::too_many_arguments,
433        reason = "inlined constructor, many args needed for performance"
434    )]
435    // Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
436    // because passing so much data is costly. Verified with samply.
437    #[inline(always)]
438    pub fn new(
439        msg_sender: Address,
440        to: Address,
441        code_address: Address,
442        bytecode: Code,
443        msg_value: U256,
444        calldata: CallData,
445        is_static: bool,
446        gas_limit: u64,
447        depth: usize,
448        should_transfer_value: bool,
449        is_create: bool,
450        ret_offset: usize,
451        ret_size: usize,
452        stack: Stack,
453        memory: Memory,
454    ) -> Self {
455        // Note: Do not use ..Default::default() because it has runtime cost.
456
457        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
458        Self {
459            gas_limit,
460            gas_remaining: gas_limit as i64,
461            msg_sender,
462            to,
463            code_address,
464            bytecode,
465            msg_value,
466            calldata,
467            is_static,
468            depth,
469            should_transfer_value,
470            is_create,
471            ret_offset,
472            ret_size,
473            stack,
474            memory,
475            call_frame_backup: CallFrameBackup::default(),
476            output: Bytes::default(),
477            pc: 0,
478            sub_return_data: Bytes::default(),
479            state_gas_used_at_entry: 0,
480            frame_state_gas_spilled: 0,
481            target_alive: false,
482            new_account_state_gas_charged: false,
483        }
484    }
485
486    #[inline(always)]
487    pub fn next_opcode(&self) -> u8 {
488        // SAFETY: pc reaches at most bytecode_len + 32 (a PUSH32 at the last real
489        // byte advances 33 total: +1 in the dispatch loop, +32 in the handler).
490        // dispatch_buf() is bytecode_len + BYTECODE_PADDING (33) long, so the read
491        // is always in bounds.
492        #[expect(unsafe_code, reason = "pc bounded by padded bytecode len")]
493        unsafe {
494            *self.bytecode.dispatch_buf().get_unchecked(self.pc)
495        }
496    }
497
498    pub fn pc(&self) -> usize {
499        self.pc
500    }
501
502    /// Increases gas consumption of CallFrame and Environment, returning an error if the callframe gas limit is reached.
503    #[inline(always)]
504    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
505    #[expect(clippy::arithmetic_side_effects, reason = "arithmethic checked")]
506    pub fn increase_consumed_gas(&mut self, gas: u64) -> Result<(), ExceptionalHalt> {
507        self.gas_remaining -= gas as i64;
508
509        if self.gas_remaining < 0 {
510            return Err(ExceptionalHalt::OutOfGas);
511        }
512
513        Ok(())
514    }
515
516    /// EELS' `check_gas`: assert gas is available without consuming it.
517    #[inline(always)]
518    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
519    pub fn check_gas(&self, gas: u64) -> Result<(), ExceptionalHalt> {
520        if self.gas_remaining < 0 || (self.gas_remaining as u64) < gas {
521            return Err(ExceptionalHalt::OutOfGas);
522        }
523        Ok(())
524    }
525
526    pub fn set_code(&mut self, code: Code) -> Result<(), VMError> {
527        self.bytecode = code;
528        Ok(())
529    }
530}
531
532impl<'a> VM<'a> {
533    /// Adds current calframe to call_frames, sets current call frame to the passed callframe.
534    #[inline(always)]
535    pub fn add_callframe(&mut self, new_call_frame: CallFrame) {
536        // Reserve once on the first sub-call (p99 depth ~10, max 27): keeps the ~43% of txs that
537        // never make a call alloc-free (`call_frames` starts as `Vec::new()`), while avoiding the
538        // repeated reallocs a call-heavy tx would otherwise incur as depth grows.
539        if self.call_frames.is_empty() {
540            self.call_frames.reserve(8);
541        }
542        self.call_frames.push(new_call_frame);
543        #[allow(unsafe_code, reason = "just pushed, so the vec is not empty")]
544        unsafe {
545            std::mem::swap(
546                &mut self.current_call_frame,
547                self.call_frames.last_mut().unwrap_unchecked(),
548            );
549        }
550    }
551
552    #[inline(always)]
553    pub fn pop_call_frame(&mut self) -> Result<CallFrame, InternalError> {
554        let mut new = self.call_frames.pop().ok_or(InternalError::CallFrame)?;
555
556        std::mem::swap(&mut new, &mut self.current_call_frame);
557
558        Ok(new)
559    }
560
561    pub fn is_initial_call_frame(&self) -> bool {
562        self.call_frames.is_empty()
563    }
564
565    /// Restores the cache state to the state before changes made during a callframe.
566    pub fn restore_cache_state(&mut self) -> Result<(), VMError> {
567        let callframe_backup = self.current_call_frame.call_frame_backup.clone();
568        restore_cache_state(self.db, callframe_backup)
569    }
570
571    /// Like [`Self::restore_cache_state`] but moves the current frame's backup out instead of
572    /// cloning it. Only sound when nothing reads `call_frame_backup` afterward: the inner-call
573    /// revert in `handle_return` (the frame is popped right after, so its backup is dead), and
574    /// the top-level / invalid-tx revert when no `BackupHook` is installed (normal L1 block
575    /// execution, gated on `VM::preserve_top_level_backup`).
576    ///
577    /// When a `BackupHook` IS present (L2 / stateless) the top-level paths must keep cloning,
578    /// because `BackupHook::finalize` reads the backup to build the tx-level undo snapshot.
579    pub fn restore_cache_state_consuming(&mut self) -> Result<(), VMError> {
580        let callframe_backup = std::mem::take(&mut self.current_call_frame.call_frame_backup);
581        restore_cache_state(self.db, callframe_backup)
582    }
583
584    // The CallFrameBackup of the current callframe has to be merged with the backup of its parent, in the following way:
585    //   - For every account that's present in the parent backup, do nothing (i.e. keep the one that's already there).
586    //   - For every account that's NOT present in the parent backup but is on the child backup, add the child backup to it.
587    //   - Do the same for every individual storage slot.
588    pub fn merge_call_frame_backup_with_parent(
589        &mut self,
590        child_call_frame_backup: &CallFrameBackup,
591    ) -> Result<(), VMError> {
592        let parent_backup_accounts = &mut self
593            .current_call_frame
594            .call_frame_backup
595            .original_accounts_info;
596        for (address, account) in child_call_frame_backup.original_accounts_info.iter() {
597            if parent_backup_accounts.get(address).is_none() {
598                parent_backup_accounts.insert(*address, account.clone());
599            }
600        }
601
602        let parent_backup_storage = &mut self
603            .current_call_frame
604            .call_frame_backup
605            .original_account_storage_slots;
606        for (address, storage) in child_call_frame_backup
607            .original_account_storage_slots
608            .iter()
609        {
610            let parent_storage = parent_backup_storage.entry(*address).or_default();
611            for (key, value) in storage {
612                if parent_storage.get(key).is_none() {
613                    parent_storage.insert(*key, *value);
614                }
615            }
616        }
617
618        // Propagate code-cache insertions so a revert of the parent also
619        // evicts codes deployed by the (committed) child frame.
620        self.current_call_frame
621            .call_frame_backup
622            .inserted_code_hashes
623            .extend(child_call_frame_backup.inserted_code_hashes.iter().copied());
624
625        Ok(())
626    }
627
628    #[inline(always)]
629    #[expect(
630        clippy::arithmetic_side_effects,
631        reason = "pc bounded by padded bytecode len"
632    )]
633    pub fn advance_pc(&mut self) {
634        self.current_call_frame.pc += 1;
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use ethrex_common::types::AccountInfo;
642
643    #[test]
644    fn absorb_keeps_earliest_original_value() {
645        let addr = Address::from_low_u64_be(1);
646        let mut acc = CallFrameBackup::default();
647
648        let mut first = CallFrameBackup::default();
649        first
650            .original_account_storage_slots
651            .entry(addr)
652            .or_default()
653            .insert(H256::zero(), U256::from(10));
654
655        let mut second = CallFrameBackup::default();
656        second
657            .original_account_storage_slots
658            .entry(addr)
659            .or_default()
660            .insert(H256::zero(), U256::from(20));
661
662        acc.absorb(&first);
663        acc.absorb(&second);
664
665        // First-seen-wins: the earliest original (10) is preserved.
666        let value = acc
667            .original_account_storage_slots
668            .get(&addr)
669            .and_then(|slots| slots.get(&H256::zero()))
670            .copied();
671        assert_eq!(value, Some(U256::from(10)));
672    }
673
674    #[test]
675    fn absorb_keeps_earliest_account_info() {
676        let addr = Address::from_low_u64_be(2);
677        let mut acc = CallFrameBackup::default();
678
679        let first_acc = LevmAccount {
680            info: AccountInfo {
681                nonce: 1,
682                ..Default::default()
683            },
684            ..Default::default()
685        };
686        let mut first = CallFrameBackup::default();
687        first.original_accounts_info.insert(addr, first_acc);
688
689        let second_acc = LevmAccount {
690            info: AccountInfo {
691                nonce: 2,
692                ..Default::default()
693            },
694            ..Default::default()
695        };
696        let mut second = CallFrameBackup::default();
697        second.original_accounts_info.insert(addr, second_acc);
698
699        acc.absorb(&first);
700        acc.absorb(&second);
701
702        let nonce = acc.original_accounts_info.get(&addr).map(|a| a.info.nonce);
703        assert_eq!(nonce, Some(1));
704    }
705}