Skip to main content

ethrex_levm/
utils.rs

1use crate::{
2    EVMConfig, Environment,
3    account::{AccountStatus, LevmAccount},
4    call_frame::CallFrameBackup,
5    constants::*,
6    db::gen_db::GeneralizedDatabase,
7    errors::{ExceptionalHalt, InternalError, TxValidationError, VMError},
8    gas_cost::{
9        self, ACCOUNT_WRITE_AMSTERDAM, BLOB_GAS_PER_BLOB, CREATE_BASE_COST,
10        PER_AUTH_BASE_COST_AMSTERDAM, STANDARD_TOKEN_COST, WARM_ADDRESS_ACCESS_COST,
11        cold_account_access_cost, floor_tokens_in_access_list, total_cost_floor_per_token,
12        tx_base_cost,
13    },
14    vm::{Substate, VM},
15};
16use ExceptionalHalt::OutOfGas;
17use bytes::Bytes;
18use ethrex_common::constants::SYSTEM_ADDRESS;
19use ethrex_common::types::Log;
20use ethrex_common::{
21    Address, H256, U256,
22    evm::calculate_create_address,
23    types::{Account, Code, Fork, Transaction, fake_exponential, tx_fields::*},
24    utils::{keccak, u256_to_big_endian},
25};
26use ethrex_common::{types::TxKind, utils::u256_from_big_endian_const};
27use ethrex_rlp;
28use rustc_hash::{FxHashMap, FxHashSet};
29pub type Storage = FxHashMap<U256, H256>;
30
31// ================== Address related functions ======================
32/// Converts address (H160) to word (U256)
33pub fn address_to_word(address: Address) -> U256 {
34    let mut word = [0u8; 32];
35
36    for (word_byte, address_byte) in word.iter_mut().skip(12).zip(address.as_bytes().iter()) {
37        *word_byte = *address_byte;
38    }
39
40    u256_from_big_endian_const(word)
41}
42
43/// Calculates the address of a new contract using the CREATE2 opcode as follows
44///
45/// initialization_code = memory[offset:offset+size]
46///
47/// address = keccak256(0xff || sender_address || salt || keccak256(initialization_code))[12:]
48pub fn calculate_create2_address(
49    sender_address: Address,
50    initialization_code: &[u8],
51    salt: U256,
52) -> Result<Address, InternalError> {
53    let init_code_hash = keccak(initialization_code);
54
55    let generated_address = Address::from_slice(
56        keccak(
57            [
58                &[0xff],
59                sender_address.as_bytes(),
60                &salt.to_big_endian(),
61                init_code_hash.as_bytes(),
62            ]
63            .concat(),
64        )
65        .as_bytes()
66        .get(12..)
67        .ok_or(InternalError::Slicing)?,
68    );
69    Ok(generated_address)
70}
71
72// ================== Backup related functions =======================
73
74/// Restore the state of the cache to the state it in the callframe backup.
75/// Also restores BAL recorder state changes (but not touched_addresses) per EIP-7928.
76pub fn restore_cache_state(
77    db: &mut GeneralizedDatabase,
78    callframe_backup: CallFrameBackup,
79) -> Result<(), VMError> {
80    for (address, account) in callframe_backup.original_accounts_info {
81        if let Some(current_account) = db.current_accounts_state.get_mut(&address) {
82            current_account.info = account.info;
83            current_account.status = account.status;
84            current_account.has_storage = account.has_storage;
85            current_account.exists = account.exists;
86        }
87    }
88
89    for (address, storage) in callframe_backup.original_account_storage_slots {
90        // This call to `get_account_mut` should never return None, because we are looking up accounts
91        // that had their storage modified, which means they should be in the cache. That's why
92        // we return an internal error in case we haven't found it.
93        let account = db
94            .current_accounts_state
95            .get_mut(&address)
96            .ok_or(InternalError::AccountNotFound)?;
97
98        for (key, value) in storage {
99            account.storage.insert(key, value);
100        }
101    }
102
103    // Evict codes the reverted frame(s) deployed: a stale by-hash cache entry
104    // would serve a later read of the same hash (from a pre-existing account)
105    // without hitting the store, hiding the read from execution-witness
106    // recording (EIP-8025). Only hashes that were NOT cached before the frame
107    // are tracked, so committed or store-loaded codes are never evicted.
108    for code_hash in callframe_backup.inserted_code_hashes {
109        db.codes.remove(&code_hash);
110    }
111
112    // Restore BAL recorder to checkpoint (but keep touched_addresses per EIP-7928)
113    if let Some(checkpoint) = callframe_backup.bal_checkpoint
114        && let Some(recorder) = db.bal_recorder.as_mut()
115    {
116        recorder.restore(checkpoint);
117    }
118
119    Ok(())
120}
121
122// ================= Blob hash related functions =====================
123pub fn get_base_fee_per_blob_gas(
124    block_excess_blob_gas: Option<u64>,
125    evm_config: &EVMConfig,
126) -> Result<U256, VMError> {
127    let base_fee_update_fraction = evm_config.blob_schedule.base_fee_update_fraction;
128    let excess_blob_gas = block_excess_blob_gas.unwrap_or_default();
129
130    fake_exponential(
131        MIN_BASE_FEE_PER_BLOB_GAS.into(),
132        excess_blob_gas.into(),
133        base_fee_update_fraction,
134    )
135    .map_err(|err| VMError::Internal(InternalError::FakeExponentialError(err)))
136}
137
138/// Gets the max blob gas cost for a transaction that a user is
139/// willing to pay.
140pub fn get_max_blob_gas_price(
141    tx_blob_hashes: &[H256],
142    tx_max_fee_per_blob_gas: Option<U256>,
143) -> Result<U256, VMError> {
144    let blobhash_amount: u64 = tx_blob_hashes
145        .len()
146        .try_into()
147        .map_err(|_| InternalError::TypeConversion)?;
148
149    let blob_gas_used: u64 = blobhash_amount
150        .checked_mul(BLOB_GAS_PER_BLOB)
151        .unwrap_or_default();
152
153    let max_blob_gas_cost = tx_max_fee_per_blob_gas
154        .unwrap_or_default()
155        .checked_mul(blob_gas_used.into())
156        .ok_or(InternalError::Overflow)?;
157
158    Ok(max_blob_gas_cost)
159}
160/// Calculate the actual blob gas cost.
161pub fn calculate_blob_gas_cost(
162    tx_blob_hashes: &[H256],
163    base_blob_fee_per_gas: U256,
164) -> Result<U256, VMError> {
165    let blobhash_amount: u64 = tx_blob_hashes
166        .len()
167        .try_into()
168        .map_err(|_| InternalError::TypeConversion)?;
169
170    let blob_gas_used: u64 = blobhash_amount
171        .checked_mul(BLOB_GAS_PER_BLOB)
172        .unwrap_or_default();
173
174    let blob_gas_used: U256 = blob_gas_used.into();
175    let blob_fee: U256 = blob_gas_used
176        .checked_mul(base_blob_fee_per_gas)
177        .ok_or(InternalError::Overflow)?;
178
179    Ok(blob_fee)
180}
181
182// ==================== Word related functions =======================
183pub fn word_to_address(word: U256) -> Address {
184    Address::from_slice(&u256_to_big_endian(word)[12..])
185}
186
187// ================== EIP-7702 related functions =====================
188
189pub fn code_has_delegation(code: &[u8]) -> Result<bool, VMError> {
190    // Delegate to the canonical predicate in `ethrex-common` so the
191    // EIP-7702 designation check (`0xef0100 || address`, exactly 23 bytes)
192    // has a single source of truth. Signature kept as `&[u8]` for callers.
193    Ok(ethrex_common::types::is_eip7702_delegation(code))
194}
195
196/// Gets the address inside the bytecode if it has been
197/// delegated as the EIP7702 determines.
198pub fn get_authorized_address_from_code(code: &[u8]) -> Result<Address, VMError> {
199    if code_has_delegation(code)? {
200        let address_bytes = &code
201            .get(SET_CODE_DELEGATION_BYTES.len()..)
202            .ok_or(InternalError::Slicing)?;
203        // It shouldn't panic when doing Address::from_slice()
204        // because the length is checked inside the code_has_delegation() function
205        let address = Address::from_slice(address_bytes);
206        Ok(address)
207    } else {
208        // if we end up here, it means that the address wasn't previously delegated.
209        Err(InternalError::AccountNotDelegated.into())
210    }
211}
212
213pub fn eip7702_recover_address(
214    auth_tuple: &AuthorizationTuple,
215    crypto: &dyn ethrex_crypto::Crypto,
216) -> Result<Option<Address>, VMError> {
217    use ethrex_rlp::encode::RLPEncode;
218
219    if auth_tuple.s_signature > *SECP256K1_ORDER_OVER2 || U256::zero() >= auth_tuple.s_signature {
220        return Ok(None);
221    }
222    if auth_tuple.r_signature > *SECP256K1_ORDER || U256::zero() >= auth_tuple.r_signature {
223        return Ok(None);
224    }
225    if auth_tuple.y_parity != U256::one() && auth_tuple.y_parity != U256::zero() {
226        return Ok(None);
227    }
228
229    let mut rlp_buf = Vec::with_capacity(128);
230    rlp_buf.push(MAGIC);
231    (auth_tuple.chain_id, auth_tuple.address, auth_tuple.nonce).encode(&mut rlp_buf);
232    let msg = crypto.keccak256(&rlp_buf);
233
234    let y_parity: u8 =
235        TryInto::<u8>::try_into(auth_tuple.y_parity).map_err(|_| InternalError::TypeConversion)?;
236
237    let mut sig = [0u8; 65];
238    sig[..32].copy_from_slice(&auth_tuple.r_signature.to_big_endian());
239    sig[32..64].copy_from_slice(&auth_tuple.s_signature.to_big_endian());
240    sig[64] = y_parity;
241
242    match crypto.recover_signer(&sig, &msg) {
243        Ok(address) => Ok(Some(address)),
244        Err(_) => Ok(None),
245    }
246}
247
248/// Gets code of an account, returning early if it's not a delegated account, otherwise
249/// Returns tuple (is_delegated, eip7702_cost, code_address, code).
250/// Notice that it also inserts the delegated account to the "accessed accounts" set.
251///
252/// Where:
253/// - `is_delegated`: True if account is a delegated account.
254/// - `eip7702_cost`: Cost of accessing the delegated account (if any)
255/// - `code_address`: Code address (if delegated, returns the delegated address)
256/// - `code`: Bytecode of the code_address, what the EVM will execute.
257pub fn eip7702_get_code(
258    db: &mut GeneralizedDatabase,
259    accrued_substate: &mut Substate,
260    address: Address,
261    fork: Fork,
262) -> Result<(bool, u64, Address, Code), VMError> {
263    let (bytecode, delegation) = eip7702_peek_delegation(db, accrued_substate, address, fork)?;
264    let Some((auth_address, access_cost)) = delegation else {
265        return Ok((false, 0, address, bytecode));
266    };
267
268    accrued_substate.add_accessed_address(auth_address);
269    let authorized_bytecode = db.get_account_code(auth_address)?.clone();
270
271    Ok((true, access_cost, auth_address, authorized_bytecode))
272}
273
274/// First half of [`eip7702_get_code`]: read `address`'s code and detect a
275/// delegation designation WITHOUT touching the delegate account.
276///
277/// Returns `address`'s code and, when delegated, the delegate address with
278/// its warm/cold access cost (computed from the current substate, not
279/// recorded). CALL-family opcodes use this to gas-check the delegation
280/// access cost before reading the delegate (EELS order); reading it earlier
281/// would leak the delegate account into execution witnesses on OOG.
282pub fn eip7702_peek_delegation(
283    db: &mut GeneralizedDatabase,
284    substate: &Substate,
285    address: Address,
286    fork: Fork,
287) -> Result<(Code, Option<(Address, u64)>), VMError> {
288    let bytecode = db.get_account_code(address)?.clone();
289    if !code_has_delegation(bytecode.code())? {
290        return Ok((bytecode, None));
291    }
292    let auth_address = get_authorized_address_from_code(bytecode.code())?;
293    let access_cost = if substate.is_address_accessed(&auth_address) {
294        WARM_ADDRESS_ACCESS_COST
295    } else {
296        // EIP-8038 (glamsterdam-devnet-6): cold account access is repriced
297        // 2600 -> 3000 at Amsterdam, so the 7702 delegate-access cost must be
298        // fork-dependent rather than the flat COLD_ADDRESS_ACCESS_COST.
299        cold_account_access_cost(fork)
300    };
301    Ok((bytecode, Some((auth_address, access_cost))))
302}
303
304/// Precomputed intrinsic-gas components for a transaction.
305///
306/// Computed once per tx in the prepare-execution hook and reused by
307/// [`VM::validate_min_gas_limit`](crate::hooks::default_hook::validate_min_gas_limit)
308/// and [`VM::add_intrinsic_gas`]. Previously the full calldata / access-list /
309/// auth-list walk ran 2-3x per tx (once in each function, plus the pre-Amsterdam
310/// floor's own `tx_calldata`).
311#[derive(Clone, Copy, Debug)]
312pub struct IntrinsicGas {
313    /// Regular (EIP-8037) intrinsic-gas arm.
314    pub regular: u64,
315    /// State (EIP-8037, Amsterdam+) intrinsic-gas arm; always 0 pre-Amsterdam.
316    pub state: u64,
317    /// `gas_cost::tx_calldata` over `current_call_frame.calldata`. Reused by the
318    /// pre-Amsterdam floor check (same byte string, same point in execution).
319    pub calldata_cost: u64,
320}
321
322impl<'a> VM<'a> {
323    /// Applies the EIP-7702 authorizations and charges their state-dependent costs at
324    /// the top frame, mirroring EELS `set_delegation`
325    /// (`amsterdam/vm/eoa_delegation.py::set_delegation`).
326    ///
327    /// Amsterdam+ (EIP-8037/8038): the per-auth intrinsic regular charge is only
328    /// `REGULAR_PER_AUTH_BASE_COST`; the state-dependent charges are levied here, in the
329    /// atomic prepare region (so an OOG rolls the whole region back and burns all gas
330    /// rather than rejecting the tx). Per valid authorization, in EELS order:
331    /// - `NEW_ACCOUNT` (state) when the authority's account leaf does not yet exist;
332    /// - `ACCOUNT_WRITE` (regular) when this is the transaction's first write to
333    ///   the authority's leaf (the sender's leaf was written at inclusion, and the
334    ///   recipient's when `value > 0`, so those pay nothing here — a self-sponsored
335    ///   authority and repeated authorizations on one authority pay `ACCOUNT_WRITE` at
336    ///   most once);
337    /// - `AUTH_BASE` (state) when a net-new delegation indicator is written (the
338    ///   authority was not delegated before the tx and none was set for it earlier in
339    ///   the tx). Charged at most once per authority and never credited back.
340    ///
341    /// On any `OutOfGas` from a charge, `fail_prepare_region` rolls back the region and
342    /// this returns `Ok` (the full-gas revert is emitted by `run_execution`); it never
343    /// propagates an `Err` (that would wrongly invalidate the block).
344    ///
345    /// Pre-Amsterdam keeps the legacy per-auth model: `PER_EMPTY_ACCOUNT_COST` was
346    /// charged at intrinsic and `REFUND_AUTH_PER_EXISTING_ACCOUNT` is credited here for
347    /// an authority that already exists in the trie.
348    pub fn eip7702_set_access_code(&mut self) -> Result<(), VMError> {
349        let amsterdam = self.env.config.fork >= Fork::Amsterdam;
350
351        // Pre-Amsterdam existing-authority refund accumulator.
352        let mut refunded_gas: u64 = 0;
353
354        // EELS `delegated_before_tx`: whether each authority was already delegated at
355        // tx start. The first time an authority is seen its code is still the pre-tx
356        // code (auth processing runs before execution), so we cache it then.
357        let mut delegated_before_tx: FxHashMap<Address, bool> = FxHashMap::default();
358
359        // EELS `set_delegation` bookkeeping (Amsterdam+ only):
360        // - `written_accounts`: leaves this tx has already written. The sender's leaf
361        //   was written at inclusion (nonce bump + fee deduction); the recipient's when
362        //   value is transferred. An authority already in this set pays no ACCOUNT_WRITE.
363        // - `delegation_set_for`: authorities a delegation indicator was set for earlier
364        //   in this tx (so AUTH_BASE is charged at most once per authority).
365        let mut written_accounts: FxHashSet<Address> = FxHashSet::default();
366        let mut delegation_set_for: FxHashSet<Address> = FxHashSet::default();
367        if amsterdam {
368            written_accounts.insert(self.env.origin);
369            if !self.tx.value().is_zero() {
370                written_accounts.insert(self.current_call_frame.to);
371            }
372        }
373
374        // IMPORTANT:
375        // If any of the below steps fail, immediately stop processing that tuple and continue to the next tuple in the list. It will in the case of multiple tuples for the same authority, set the code using the address in the last valid occurrence.
376        // If transaction execution results in failure (any exceptional condition or code reverting), setting delegation designations is not rolled back (unless the atomic prepare region OOGs, which rolls back everything).
377        for auth_tuple in self.tx.authorization_list().cloned().unwrap_or_default() {
378            let chain_id_not_equals_this_chain_id = auth_tuple.chain_id != self.env.chain_id;
379            let chain_id_not_zero = !auth_tuple.chain_id.is_zero();
380
381            // 1. Verify the chain id is either 0 or the chain’s current ID.
382            if chain_id_not_zero && chain_id_not_equals_this_chain_id {
383                continue;
384            }
385
386            // 2. Verify the nonce is less than 2**64 - 1.
387            // NOTE: nonce is a u64, it's always less than or equal to u64::MAX
388            if auth_tuple.nonce == u64::MAX {
389                continue;
390            }
391
392            // 3. authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s)
393            //      s value must be less than or equal to secp256k1n/2, as specified in EIP-2.
394            let Some(authority_address) = eip7702_recover_address(&auth_tuple, self.crypto)? else {
395                continue;
396            };
397
398            // 4. Add authority to accessed_addresses (as defined in EIP-2929).
399            let authority_account = self.db.get_account(authority_address)?;
400            let authority_exists = authority_account.exists;
401            let authority_info = authority_account.info.clone();
402            let authority_code = self.db.get_code(authority_info.code_hash)?;
403            self.substate.add_accessed_address(authority_address);
404
405            // 5. Verify the code of authority is either empty or already delegated.
406            // Check this BEFORE recording to BAL so we can release the borrow on authority_code.
407            let authority_code_is_empty = authority_code.is_empty();
408            let delegated_now = code_has_delegation(authority_code.code())?;
409            let empty_or_delegated = authority_code_is_empty || delegated_now;
410            // First sighting of an authority captures its pre-tx delegation state
411            // (EELS `delegated_before_tx` from `get_pre_state_account`).
412            let pre_delegated = *delegated_before_tx
413                .entry(authority_address)
414                .or_insert(delegated_now);
415
416            // Record authority as touched for BAL per EIP-7928, even if validation fails later.
417            // This ensures authority appears in BAL with empty change set when:
418            // - Authority was loaded (above)
419            // - But validation fails (checks below)
420            if let Some(recorder) = self.db.bal_recorder.as_mut() {
421                recorder.record_touched_address(authority_address);
422            }
423
424            if !empty_or_delegated {
425                continue;
426            }
427
428            // 6. Verify the nonce of authority is equal to nonce. In case authority does not exist in the trie, verify that nonce is equal to 0.
429            // If it doesn't exist, it means the nonce is zero. The get_account() function will return Account::default()
430            // If it has nonce, the account.info.nonce should equal auth_tuple.nonce
431            if authority_info.nonce != auth_tuple.nonce {
432                continue;
433            }
434
435            if amsterdam {
436                // EELS `set_delegation` charges, in order:
437                // (a) NEW_ACCOUNT (state) when the authority leaf is absent. Uses `exists`
438                //     (EELS `account_exists`), NOT `!is_empty()`; a repeated authority
439                //     that a prior tuple already materialized reads `exists == true`.
440                if !authority_exists && self.increase_state_gas(self.state_gas_new_account).is_err()
441                {
442                    self.fail_prepare_region();
443                    return Ok(());
444                }
445
446                // (b) ACCOUNT_WRITE (regular) on the first write to the authority leaf.
447                if !written_accounts.contains(&authority_address) {
448                    if self
449                        .current_call_frame
450                        .increase_consumed_gas(ACCOUNT_WRITE_AMSTERDAM)
451                        .is_err()
452                    {
453                        self.fail_prepare_region();
454                        return Ok(());
455                    }
456                    written_accounts.insert(authority_address);
457                }
458
459                // (c) AUTH_BASE (state) when a net-new delegation indicator is written:
460                //     setting a non-null delegation, the authority was not delegated
461                //     before the tx, and none was set for it earlier this tx.
462                if auth_tuple.address != Address::zero() {
463                    if !pre_delegated
464                        && !delegation_set_for.contains(&authority_address)
465                        && self.increase_state_gas(self.state_gas_auth_base).is_err()
466                    {
467                        self.fail_prepare_region();
468                        return Ok(());
469                    }
470                    delegation_set_for.insert(authority_address);
471                }
472            } else if authority_exists {
473                // Pre-Amsterdam: existing authority refund (legacy model).
474                refunded_gas = refunded_gas
475                    .checked_add(REFUND_AUTH_PER_EXISTING_ACCOUNT)
476                    .ok_or(InternalError::Overflow)?;
477            }
478
479            // 8. Set the code of authority to be 0xef0100 || address. This is a delegation designation.
480            let delegation_bytes = [
481                &SET_CODE_DELEGATION_BYTES[..],
482                auth_tuple.address.as_bytes(),
483            ]
484            .concat();
485
486            // As a special case, if address is 0x0000000000000000000000000000000000000000 do not write the designation.
487            // Clear the account’s code and reset the account’s code hash to the empty hash.
488            let code = if auth_tuple.address != Address::zero() {
489                delegation_bytes.into()
490            } else {
491                Bytes::new()
492            };
493            self.update_account_bytecode(
494                authority_address,
495                Code::from_bytecode(code, self.crypto),
496            )?;
497
498            // 9. Increase the nonce of authority by one.
499            self.increment_account_nonce(authority_address)
500                .map_err(|_| TxValidationError::NonceIsMax)?;
501        }
502
503        // Pre-Amsterdam legacy refund channel (Amsterdam+ leaves `refunded_gas` at 0).
504        if refunded_gas > 0 {
505            self.substate.refunded_gas = self
506                .substate
507                .refunded_gas
508                .checked_add(refunded_gas)
509                .ok_or(InternalError::Overflow)?;
510        }
511
512        // EELS `auth_state_gas_used` lock-in (`interpreter.py`: after `set_delegation`,
513        // `evm.auth_state_gas_used = frame_state_gas_used(evm)`,
514        // `message.state_gas_reservoir = evm.state_gas_left`, `evm.state_gas_spilled = 0`).
515        // Re-seed the frame's state-gas baseline past the committed auth state gas so a
516        // later *execution* revert's `refill_frame_state_gas` cannot credit auth state
517        // gas back, and zero the frame spill so the auth spill stays consumed. The
518        // subsequent prepare-dispatch (create/value/delegation-resolve) charges sit above
519        // this baseline and stay refillable on an execution revert. The prep-OOG path
520        // instead refills to `prep_baseline_state_gas` (pre-region), so it fully rolls
521        // auth back too. Skipped if the auth loop already OOG'd (early return above).
522        if amsterdam {
523            self.current_call_frame.state_gas_used_at_entry = self.state_gas_used;
524            self.current_call_frame.frame_state_gas_spilled = 0;
525        }
526
527        Ok(())
528    }
529
530    pub fn add_intrinsic_gas(&mut self, intrinsic: &IntrinsicGas) -> Result<(), VMError> {
531        // Intrinsic gas is the gas consumed by the transaction before the execution of the opcodes. Section 6.2 in the Yellow Paper.
532
533        let regular_gas = intrinsic.regular;
534        let state_gas = intrinsic.state;
535
536        let total_gas = regular_gas.checked_add(state_gas).ok_or(OutOfGas)?;
537
538        self.current_call_frame
539            .increase_consumed_gas(total_gas)
540            .map_err(|_| TxValidationError::IntrinsicGasTooLow)?;
541
542        // state_gas_used is i64; intrinsic state gas is bounded by tx gas limit (< i64::MAX).
543        self.state_gas_used = self
544            .state_gas_used
545            .checked_add(i64::try_from(state_gas).map_err(|_| InternalError::Overflow)?)
546            .ok_or(InternalError::Overflow)?;
547        // Remember the intrinsic split so we can leave it in state_gas_used on top-level
548        // error (matches EELS `tx_env.intrinsic_state_gas`, which is kept separate from
549        // `tx_output.state_gas_used` and never refunded).
550        debug_assert_eq!(self.intrinsic_state_gas, 0, "intrinsic_state_gas set twice");
551        self.intrinsic_state_gas = state_gas;
552
553        // EIP-8037 (Amsterdam+): compute state gas reservoir from excess gas_limit.
554        // execution_gas = what remains after all intrinsic gas; regular_gas_budget = how much
555        // regular execution gas is allowed (capped at TX_MAX_GAS_LIMIT_AMSTERDAM); the difference becomes
556        // the reservoir for drawing state gas without consuming regular gas_remaining.
557        if self.env.config.fork >= Fork::Amsterdam {
558            if self.env.is_system_call {
559                // EIP-8037: system
560                // transactions get a dedicated state-gas reservoir of
561                // `state_gas_storage_set * SYSTEM_MAX_SSTORES_PER_CALL` ON TOP of
562                // the full SYS_CALL_GAS_LIMIT regular budget — so SSTORE-heavy
563                // system contracts (EIP-2935, EIP-4788) cannot OOG on state-gas
564                // growth alone. Skip the regular reservoir computation so we don't
565                // pre-consume `gas_remaining`; EELS sets `intrinsic_regular_gas=0`
566                // and `gas=SYSTEM_TRANSACTION_GAS` for the message
567                // (amsterdam/fork.py::process_unchecked_system_transaction).
568                let sys_reservoir = self
569                    .state_gas_storage_set
570                    .saturating_mul(SYSTEM_MAX_SSTORES_PER_CALL);
571                self.state_gas_reservoir = sys_reservoir;
572                self.state_gas_reservoir_initial = sys_reservoir;
573            } else {
574                let gas_limit = self.tx.gas_limit();
575                let execution_gas = gas_limit.saturating_sub(total_gas);
576                let regular_gas_budget = TX_MAX_GAS_LIMIT_AMSTERDAM.saturating_sub(regular_gas);
577                // A simulation that lifted the cap (eth_call, debug_traceCall) spends its
578                // whole gas limit on execution and funds no reservoir, as geth does.
579                let gas_left = if self.env.disable_execution_gas_cap {
580                    execution_gas
581                } else {
582                    regular_gas_budget.min(execution_gas)
583                };
584                let reservoir = execution_gas.saturating_sub(gas_left);
585                if reservoir > 0 {
586                    // Pre-consume reservoir from gas_remaining so GAS opcode returns <= TX_MAX_GAS_LIMIT_AMSTERDAM
587                    let reservoir_i64 =
588                        i64::try_from(reservoir).map_err(|_| InternalError::Overflow)?;
589                    self.current_call_frame.gas_remaining = self
590                        .current_call_frame
591                        .gas_remaining
592                        .checked_sub(reservoir_i64)
593                        .ok_or(InternalError::Overflow)?;
594                    self.state_gas_reservoir = reservoir;
595                }
596                // Capture initial reservoir for block-dimensional regular gas computation.
597                self.state_gas_reservoir_initial = reservoir;
598            }
599
600            // EIP-8037: seed the top frame's state-gas entry baseline to the post-intrinsic
601            // value of `state_gas_used` (the intrinsic state gas was already added above).
602            // A top-level revert/halt refill (`refill_frame_state_gas`) then rolls back only
603            // the execution portion and preserves the intrinsic state gas (which EELS keeps
604            // separate as `tx_env.intrinsic_state_gas` and never refunds).
605            self.current_call_frame.state_gas_used_at_entry = self.state_gas_used;
606        }
607
608        Ok(())
609    }
610
611    // ==================== Gas related functions =======================
612    /// Returns `(regular_gas, state_gas)` intrinsic gas for the transaction.
613    /// For Amsterdam+, state_gas is the EIP-8037 state portion.
614    /// For pre-Amsterdam, state_gas is always 0.
615    pub fn get_intrinsic_gas(&self) -> Result<IntrinsicGas, VMError> {
616        // Intrinsic Gas = Calldata cost + Create cost + Base cost + Access list cost
617        let mut regular_gas: u64 = 0;
618        // Amsterdam+ intrinsic state gas is 0 (CREATE/value NEW_ACCOUNT and all EIP-7702
619        // auth state charges are levied in the atomic prepare region). Pre-Amsterdam has
620        // no state dimension. Kept as a named binding for the `IntrinsicGas` result.
621        let state_gas: u64 = 0;
622        let fork = self.env.config.fork;
623
624        // Calldata Cost
625        // 4 gas for each zero byte in the transaction data 16 gas for each non-zero byte in the transaction.
626        let calldata_cost = self
627            .current_call_frame
628            .calldata
629            .with_slice(gas_cost::tx_calldata)??;
630
631        regular_gas = regular_gas.checked_add(calldata_cost).ok_or(OutOfGas)?;
632
633        let is_create = self.is_create()?;
634
635        if fork >= Fork::Amsterdam {
636            // EIP-2780 (merged EIPs#11645): resource-based intrinsic gas.
637            // The flat 21000 base is decomposed into a sender base + recipient
638            // access charge + value-transfer charge. These tx-level sender/to
639            // charges are ALWAYS the cold rate; access lists do NOT warm
640            // tx-level accounts. Because the intrinsic uses fixed constants
641            // (not substate warmth), the cold rate is applied automatically.
642            let sender = self.env.origin;
643            let to = self.tx.to();
644
645            // Sender base: always TX_BASE_COST_AMSTERDAM (12000).
646            regular_gas = regular_gas
647                .checked_add(tx_base_cost(fork))
648                .ok_or(OutOfGas)?;
649
650            // tx.to + tx.value recipient charge (EELS `calculate_intrinsic_cost`
651            // items 2-3), shared with `intrinsic_gas_dimensions` and the
652            // calldata-floor anchor via `recipient_regular_gas`.
653            regular_gas = regular_gas
654                .checked_add(gas_cost::recipient_regular_gas(
655                    &to,
656                    self.tx.value(),
657                    sender,
658                    fork,
659                ))
660                .ok_or(OutOfGas)?;
661
662            // Contract-creation: NEW_ACCOUNT state gas is charged in-region by
663            // `prepare_execution` (EELS `prepare_dispatch` create branch), not
664            // at intrinsic time. Amsterdam+ create intrinsic state is 0.
665        } else {
666            // Base Cost
667            regular_gas = regular_gas.checked_add(TX_BASE_COST).ok_or(OutOfGas)?;
668
669            // Create Cost
670            if is_create {
671                // https://eips.ethereum.org/EIPS/eip-2#specification
672                regular_gas = regular_gas.checked_add(CREATE_BASE_COST).ok_or(OutOfGas)?;
673            }
674        }
675
676        // EIP-3860 init code words (Shanghai+), unchanged by EIP-2780.
677        if is_create && fork >= Fork::Shanghai {
678            let number_of_words = &self.current_call_frame.calldata.len().div_ceil(WORD_SIZE);
679            let double_number_of_words: u64 = number_of_words
680                .checked_mul(2)
681                .ok_or(OutOfGas)?
682                .try_into()
683                .map_err(|_| InternalError::TypeConversion)?;
684
685            regular_gas = regular_gas
686                .checked_add(double_number_of_words)
687                .ok_or(OutOfGas)?;
688        }
689
690        // Access List Cost
691        let mut access_lists_cost: u64 = 0;
692        for (_, keys) in self.tx.access_list() {
693            access_lists_cost = access_lists_cost
694                .checked_add(gas_cost::access_list_address_cost(fork))
695                .ok_or(OutOfGas)?;
696            for _ in keys {
697                access_lists_cost = access_lists_cost
698                    .checked_add(gas_cost::access_list_storage_key_cost(fork))
699                    .ok_or(OutOfGas)?;
700            }
701        }
702
703        // EIP-7981 (Amsterdam+): access-list data bytes also contribute to the regular arm.
704        // access_list_cost += floor_tokens_in_access_list * total_cost_floor_per_token
705        // = access_list_bytes * STANDARD_TOKEN_COST * total_cost_floor_per_token
706        // Effective: +1280 per address, +2048 per storage key.
707        if fork >= Fork::Amsterdam {
708            let al_floor_tokens = floor_tokens_in_access_list(self.tx.access_list());
709            let al_data_cost = al_floor_tokens
710                .checked_mul(total_cost_floor_per_token(fork))
711                .ok_or(InternalError::Overflow)?;
712            access_lists_cost = access_lists_cost
713                .checked_add(al_data_cost)
714                .ok_or(InternalError::Overflow)?;
715        }
716
717        regular_gas = regular_gas.checked_add(access_lists_cost).ok_or(OutOfGas)?;
718
719        // Authorization List Cost
720        // `unwrap_or_default` will return an empty vec when the `authorization_list` field is None.
721        // If the vec is empty, the len will be 0, thus the authorization_list_cost is 0.
722        let amount_of_auth_tuples: u64 = match self.tx.authorization_list() {
723            None => 0,
724            Some(list) => list
725                .len()
726                .try_into()
727                .map_err(|_| InternalError::TypeConversion)?,
728        };
729
730        if fork >= Fork::Amsterdam {
731            // EIP-8038 (EELS `calculate_intrinsic_cost`): the per-auth intrinsic regular
732            // charge is only `REGULAR_PER_AUTH_BASE_COST` (`PER_AUTH_BASE_COST_AMSTERDAM`,
733            // 7816). The ACCOUNT_WRITE (regular) and NEW_ACCOUNT / AUTH_BASE (state)
734            // charges are state-dependent and levied in-region by
735            // `eip7702_set_access_code` (EELS `set_delegation`), not at intrinsic time.
736            // Amsterdam auth intrinsic state is 0.
737            let regular_auth_cost = PER_AUTH_BASE_COST_AMSTERDAM
738                .checked_mul(amount_of_auth_tuples)
739                .ok_or(InternalError::Overflow)?;
740            regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?;
741        } else {
742            let authorization_list_cost = PER_EMPTY_ACCOUNT_COST
743                .checked_mul(amount_of_auth_tuples)
744                .ok_or(InternalError::Overflow)?;
745            regular_gas = regular_gas
746                .checked_add(authorization_list_cost)
747                .ok_or(OutOfGas)?;
748        }
749
750        Ok(IntrinsicGas {
751            regular: regular_gas,
752            state: state_gas,
753            calldata_cost,
754        })
755    }
756
757    /// Calculates the minimum gas to be consumed in the transaction.
758    pub fn get_min_gas_used(&self) -> Result<u64, VMError> {
759        let fork = self.env.config.fork;
760
761        // If the transaction is a CREATE transaction, the calldata is emptied and the bytecode is assigned.
762        let tx_calldata;
763        let calldata = if self.is_create()? {
764            self.current_call_frame.bytecode.code()
765        } else {
766            // A transaction's own data is owned, so this shares it rather than copying.
767            tx_calldata = self.current_call_frame.calldata.to_bytes()?;
768            tx_calldata.as_ref()
769        };
770
771        // EIP-7976 floor tokens: for the floor arm, all calldata bytes count unweighted.
772        // floor_tokens_in_calldata = (zero_bytes + nonzero_bytes) * STANDARD_TOKEN_COST
773        // Pre-Amsterdam uses the weighted EIP-7623 formula: (nonzero * 16 + zero * 4) / 4
774        let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
775            // EIP-7976: floor tokens = total_bytes * STANDARD_TOKEN_COST (unweighted).
776            let total_bytes: u64 = calldata
777                .len()
778                .try_into()
779                .map_err(|_| InternalError::TypeConversion)?;
780            total_bytes
781                .checked_mul(STANDARD_TOKEN_COST)
782                .ok_or(InternalError::Overflow)?
783        } else {
784            // Pre-Amsterdam: weighted EIP-7623 token count.
785            gas_cost::tx_calldata(calldata)? / STANDARD_TOKEN_COST
786        };
787
788        // EIP-7981 (Amsterdam+): access-list data bytes fold into the floor-token count.
789        // floor_tokens_in_access_list = access_list_bytes * STANDARD_TOKEN_COST
790        // where access_list_bytes = 20 * address_count + 32 * storage_key_count.
791        if fork >= Fork::Amsterdam {
792            let al_floor_tokens = floor_tokens_in_access_list(self.tx.access_list());
793            tokens_in_calldata = tokens_in_calldata
794                .checked_add(al_floor_tokens)
795                .ok_or(InternalError::Overflow)?;
796        }
797
798        // EELS `data_floor_gas_cost = total_floor_tokens * TX_DATA_TOKEN_FLOOR + base_regular_gas`
799        // where `base_regular_gas = TX_BASE + recipient_regular_gas`. Pre-Amsterdam the floor
800        // base stays anchored on bare `tx_base_cost(fork)` (21000); Amsterdam+ (EIP-2780) folds
801        // in the recipient/value regular-gas contribution via `recipient_regular_gas`.
802        let mut min_gas_used: u64 = tokens_in_calldata
803            .checked_mul(total_cost_floor_per_token(fork))
804            .ok_or(InternalError::Overflow)?;
805
806        let floor_base = if fork >= Fork::Amsterdam {
807            tx_base_cost(fork)
808                .checked_add(gas_cost::recipient_regular_gas(
809                    &self.tx.to(),
810                    self.tx.value(),
811                    self.env.origin,
812                    fork,
813                ))
814                .ok_or(InternalError::Overflow)?
815        } else {
816            tx_base_cost(fork)
817        };
818
819        min_gas_used = min_gas_used
820            .checked_add(floor_base)
821            .ok_or(InternalError::Overflow)?;
822
823        Ok(min_gas_used)
824    }
825
826    /// Gets transaction callee, calculating create address if it's a "Create" transaction.
827    /// Bool indicates whether it is a `create` transaction or not.
828    pub fn get_tx_callee(
829        tx: &Transaction,
830        db: &mut GeneralizedDatabase,
831        env: &Environment,
832        substate: &mut Substate,
833    ) -> Result<(Address, bool), VMError> {
834        match tx.to() {
835            TxKind::Call(address_to) => {
836                substate.add_accessed_address(address_to);
837
838                Ok((address_to, false))
839            }
840
841            TxKind::Create => {
842                let sender_nonce = db.get_account(env.origin)?.info.nonce;
843
844                let created_address = calculate_create_address(env.origin, sender_nonce);
845
846                substate.add_accessed_address(created_address);
847                substate.add_created_account(created_address);
848
849                Ok((created_address, true))
850            }
851        }
852    }
853}
854
855/// Compute `(regular, state)` intrinsic gas for a transaction without needing
856/// a full VM instance. Mirrors `VM::get_intrinsic_gas` but operates on the raw
857/// transaction and fork. Amsterdam+ intrinsic state gas is 0 — CREATE/value
858/// `NEW_ACCOUNT` and all EIP-7702 auth state charges are levied in the atomic
859/// prepare region, not at intrinsic time — so this returns `(regular, 0)` for
860/// every fork.
861///
862/// Used by the block executor to perform the EIP-8037 (PR #2703) per-tx 2D
863/// inclusion check before the tx runs.
864///
865/// `sender` is the transaction's recovered sender; it is required for the
866/// EIP-2780 (Amsterdam+) self-transfer rule, which zeroes the recipient and
867/// value charges when `sender == tx.to`. The parameter is taken
868/// unconditionally so this function stays byte-identical with
869/// `VM::get_intrinsic_gas` across every tx shape (guarded by
870/// `test_intrinsic_parity_*`). A type-3/type-4 tx can never carry `to == sender`
871/// in practice, but the parameter is still threaded through for parity.
872pub fn intrinsic_gas_dimensions(
873    tx: &Transaction,
874    sender: Address,
875    fork: Fork,
876    _block_gas_limit: u64,
877) -> Result<(u64, u64), VMError> {
878    let mut regular_gas: u64 = 0;
879    let state_gas: u64 = 0;
880
881    // Calldata cost (EIP-2028 weighted)
882    let calldata_cost = gas_cost::tx_calldata(tx.data())?;
883    regular_gas = regular_gas.checked_add(calldata_cost).ok_or(OutOfGas)?;
884
885    let to = tx.to();
886    let is_create = matches!(to, TxKind::Create);
887
888    if fork >= Fork::Amsterdam {
889        // EIP-2780 (merged EIPs#11645): resource-based intrinsic gas.
890        // Mirror of `VM::get_intrinsic_gas`. These tx-level sender/to charges
891        // are ALWAYS the cold rate; access lists do NOT warm tx-level accounts.
892
893        // Sender base: always TX_BASE_COST_AMSTERDAM (12000).
894        regular_gas = regular_gas
895            .checked_add(tx_base_cost(fork))
896            .ok_or(OutOfGas)?;
897
898        // tx.to + tx.value recipient charge (EELS `calculate_intrinsic_cost`
899        // items 2-3), shared with `VM::get_intrinsic_gas` and the
900        // calldata-floor anchor via `recipient_regular_gas`.
901        regular_gas = regular_gas
902            .checked_add(gas_cost::recipient_regular_gas(
903                &to,
904                tx.value(),
905                sender,
906                fork,
907            ))
908            .ok_or(OutOfGas)?;
909
910        // Contract-creation: NEW_ACCOUNT state gas is charged in-region by
911        // `prepare_execution` (EELS `prepare_dispatch` create branch), not
912        // at intrinsic time. Amsterdam+ create intrinsic state is 0.
913    } else {
914        // Base cost
915        regular_gas = regular_gas.checked_add(TX_BASE_COST).ok_or(OutOfGas)?;
916
917        if is_create {
918            regular_gas = regular_gas.checked_add(CREATE_BASE_COST).ok_or(OutOfGas)?;
919        }
920    }
921
922    // EIP-3860 init code words (Shanghai+), unchanged by EIP-2780.
923    if is_create && fork >= Fork::Shanghai {
924        let words = tx.data().len().div_ceil(WORD_SIZE);
925        let double_words: u64 = words
926            .checked_mul(2)
927            .ok_or(OutOfGas)?
928            .try_into()
929            .map_err(|_| InternalError::TypeConversion)?;
930        regular_gas = regular_gas.checked_add(double_words).ok_or(OutOfGas)?;
931    }
932
933    // Access list cost
934    let mut access_lists_cost: u64 = 0;
935    for (_, keys) in tx.access_list() {
936        access_lists_cost = access_lists_cost
937            .checked_add(gas_cost::access_list_address_cost(fork))
938            .ok_or(OutOfGas)?;
939        for _ in keys {
940            access_lists_cost = access_lists_cost
941                .checked_add(gas_cost::access_list_storage_key_cost(fork))
942                .ok_or(OutOfGas)?;
943        }
944    }
945
946    // EIP-7981 (Amsterdam+): access-list data bytes fold into regular gas
947    if fork >= Fork::Amsterdam {
948        let al_floor_tokens = floor_tokens_in_access_list(tx.access_list());
949        let al_data_cost = al_floor_tokens
950            .checked_mul(total_cost_floor_per_token(fork))
951            .ok_or(InternalError::Overflow)?;
952        access_lists_cost = access_lists_cost
953            .checked_add(al_data_cost)
954            .ok_or(InternalError::Overflow)?;
955    }
956    regular_gas = regular_gas.checked_add(access_lists_cost).ok_or(OutOfGas)?;
957
958    // Authorization list cost
959    let amount_of_auth_tuples: u64 = match tx.authorization_list() {
960        None => 0,
961        Some(list) => list
962            .len()
963            .try_into()
964            .map_err(|_| InternalError::TypeConversion)?,
965    };
966
967    if fork >= Fork::Amsterdam {
968        // EIP-8038 (EELS `calculate_intrinsic_cost`): per-auth intrinsic regular is only
969        // `REGULAR_PER_AUTH_BASE_COST` (`PER_AUTH_BASE_COST_AMSTERDAM`, 7816). The
970        // ACCOUNT_WRITE / NEW_ACCOUNT / AUTH_BASE charges move to `set_delegation`
971        // (in-region). Amsterdam auth intrinsic state is 0. Mirrors `VM::get_intrinsic_gas`.
972        let regular_auth_cost = PER_AUTH_BASE_COST_AMSTERDAM
973            .checked_mul(amount_of_auth_tuples)
974            .ok_or(InternalError::Overflow)?;
975        regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?;
976    } else {
977        let auth_cost = PER_EMPTY_ACCOUNT_COST
978            .checked_mul(amount_of_auth_tuples)
979            .ok_or(InternalError::Overflow)?;
980        regular_gas = regular_gas.checked_add(auth_cost).ok_or(OutOfGas)?;
981    }
982
983    Ok((regular_gas, state_gas))
984}
985
986/// Standalone EIP-7623/7976/7981 floor gas for a transaction. Mirrors
987/// [`VM::get_min_gas_used`] but operates on the raw transaction + fork, so it
988/// can be called by mempool admission / the payload builder without needing a
989/// VM instance. Returns `TX_BASE_COST + floor_rate * total_floor_tokens`.
990///
991/// Amsterdam+ uses the unweighted EIP-7976 floor (16 gas/token = 64 gas/byte)
992/// and folds EIP-7981 access-list data bytes into the token count. Pre-
993/// Amsterdam uses the weighted EIP-7623 formula.
994///
995/// `sender` is the transaction's recovered sender; it feeds the Amsterdam+
996/// floor-base anchor (`tx_base_cost(fork) + recipient_regular_gas(...)`, see
997/// EIP-2780), which needs the sender for the self-transfer zero-rule. Unused
998/// pre-Amsterdam.
999///
1000/// A mismatch between this and `VM::get_min_gas_used` would cause mempool
1001/// admission to drift from VM rejection; keep the two in sync. The
1002/// `test_intrinsic_parity_*` suite also guards this.
1003pub fn intrinsic_gas_floor(tx: &Transaction, sender: Address, fork: Fork) -> Result<u64, VMError> {
1004    // EIP-7976: floor tokens count ALL calldata bytes unweighted. For CREATE
1005    // txs the calldata is the init code. Mirrors `get_min_gas_used`.
1006    let calldata = tx.data();
1007
1008    let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
1009        let total_bytes: u64 = calldata
1010            .len()
1011            .try_into()
1012            .map_err(|_| InternalError::TypeConversion)?;
1013        total_bytes
1014            .checked_mul(STANDARD_TOKEN_COST)
1015            .ok_or(InternalError::Overflow)?
1016    } else {
1017        gas_cost::tx_calldata(calldata)? / STANDARD_TOKEN_COST
1018    };
1019
1020    if fork >= Fork::Amsterdam {
1021        let al_floor_tokens = floor_tokens_in_access_list(tx.access_list());
1022        tokens_in_calldata = tokens_in_calldata
1023            .checked_add(al_floor_tokens)
1024            .ok_or(InternalError::Overflow)?;
1025    }
1026
1027    // Floor base: Amsterdam+ anchors on `tx_base_cost(fork) + recipient_regular_gas(...)`
1028    // (EIP-2780); pre-Amsterdam stays anchored on bare `tx_base_cost(fork)` (21000).
1029    // Mirrors `get_min_gas_used`.
1030    let floor_base = if fork >= Fork::Amsterdam {
1031        tx_base_cost(fork)
1032            .checked_add(gas_cost::recipient_regular_gas(
1033                &tx.to(),
1034                tx.value(),
1035                sender,
1036                fork,
1037            ))
1038            .ok_or(InternalError::Overflow)?
1039    } else {
1040        tx_base_cost(fork)
1041    };
1042
1043    tokens_in_calldata
1044        .checked_mul(total_cost_floor_per_token(fork))
1045        .ok_or(InternalError::Overflow)?
1046        .checked_add(floor_base)
1047        .ok_or(InternalError::Overflow.into())
1048}
1049
1050/// Converts Account to LevmAccount
1051/// The problem with this is that we don't have the storage root.
1052pub fn account_to_levm_account(account: Account) -> (LevmAccount, Code) {
1053    (
1054        LevmAccount {
1055            info: account.info,
1056            has_storage: !account.storage.is_empty(), // This is used in scenarios in which the storage is already all in the account. For the Levm Runner
1057            storage: account.storage.into_iter().collect(),
1058            status: AccountStatus::Unmodified,
1059            exists: true,
1060        },
1061        account.code,
1062    )
1063}
1064
1065/// Converts a U256 value into usize, returning an error if the value is over 32 bits
1066/// This is generally used for memory offsets and sizes, 32 bits is more than enough for this purpose.
1067#[expect(clippy::as_conversions)]
1068pub fn u256_to_usize(val: U256) -> Result<usize, VMError> {
1069    // OR the three high limbs into one branch instead of a 4-way `||` chain,
1070    // then a single magnitude check — fewer branches on this hot conversion
1071    // (MLOAD/MSTORE/MCOPY/CALLDATACOPY/RETURN/... offsets all pass through here).
1072    if (val.0[1] | val.0[2] | val.0[3]) != 0 || val.0[0] > u32::MAX as u64 {
1073        return Err(VMError::ExceptionalHalt(ExceptionalHalt::VeryLargeNumber));
1074    }
1075    Ok(val.0[0] as usize)
1076}
1077
1078/// Converts U256 size and offset to usize.
1079/// If the size is zero, the offset will be zero regardless of its original value as it is not relevant
1080pub fn size_offset_to_usize(size: U256, offset: U256) -> Result<(usize, usize), VMError> {
1081    if size.is_zero() {
1082        // Offset is irrelevant
1083        Ok((0, 0))
1084    } else {
1085        Ok((u256_to_usize(size)?, u256_to_usize(offset)?))
1086    }
1087}
1088
1089// ==================== EIP-7708 Helper Functions ====================
1090
1091/// Creates EIP-7708 Transfer log (LOG3) for ETH transfers.
1092/// Emitted from SYSTEM_ADDRESS when ETH is transferred.
1093#[inline]
1094pub fn create_eth_transfer_log(from: Address, to: Address, value: U256) -> Log {
1095    let mut from_topic = [0u8; 32];
1096    from_topic[12..].copy_from_slice(from.as_bytes());
1097
1098    let mut to_topic = [0u8; 32];
1099    to_topic[12..].copy_from_slice(to.as_bytes());
1100
1101    let data = value.to_big_endian();
1102
1103    Log {
1104        address: SYSTEM_ADDRESS,
1105        topics: vec![
1106            TRANSFER_EVENT_TOPIC,
1107            H256::from(from_topic),
1108            H256::from(to_topic),
1109        ],
1110        data: Bytes::from(data.to_vec()),
1111    }
1112}