ethrex-levm 17.0.0

Native EVM implementation for the ethrex Ethereum execution client
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
use crate::{
    EVMConfig, Environment,
    account::{AccountStatus, LevmAccount},
    call_frame::CallFrameBackup,
    constants::*,
    db::gen_db::GeneralizedDatabase,
    errors::{ExceptionalHalt, InternalError, TxValidationError, VMError},
    gas_cost::{
        self, ACCESS_LIST_ADDRESS_COST, ACCESS_LIST_STORAGE_KEY_COST, BLOB_GAS_PER_BLOB,
        COLD_ADDRESS_ACCESS_COST, CREATE_BASE_COST, REGULAR_GAS_CREATE, STANDARD_TOKEN_COST,
        STATE_BYTES_PER_AUTH_TOTAL, STATE_BYTES_PER_NEW_ACCOUNT, WARM_ADDRESS_ACCESS_COST,
        cost_per_state_byte, floor_tokens_in_access_list, total_cost_floor_per_token,
    },
    vm::{Substate, VM},
};
use ExceptionalHalt::OutOfGas;
use bytes::Bytes;
use ethrex_common::constants::SYSTEM_ADDRESS;
use ethrex_common::types::Log;
use ethrex_common::{
    Address, H256, U256,
    evm::calculate_create_address,
    types::{Account, Code, Fork, Transaction, fake_exponential, tx_fields::*},
    utils::{keccak, u256_to_big_endian},
};
use ethrex_common::{types::TxKind, utils::u256_from_big_endian_const};
use ethrex_rlp;
use rustc_hash::FxHashMap;
pub type Storage = FxHashMap<U256, H256>;

// ================== Address related functions ======================
/// Converts address (H160) to word (U256)
pub fn address_to_word(address: Address) -> U256 {
    let mut word = [0u8; 32];

    for (word_byte, address_byte) in word.iter_mut().skip(12).zip(address.as_bytes().iter()) {
        *word_byte = *address_byte;
    }

    u256_from_big_endian_const(word)
}

/// Calculates the address of a new contract using the CREATE2 opcode as follows
///
/// initialization_code = memory[offset:offset+size]
///
/// address = keccak256(0xff || sender_address || salt || keccak256(initialization_code))[12:]
pub fn calculate_create2_address(
    sender_address: Address,
    initialization_code: &Bytes,
    salt: U256,
) -> Result<Address, InternalError> {
    let init_code_hash = keccak(initialization_code);

    let generated_address = Address::from_slice(
        keccak(
            [
                &[0xff],
                sender_address.as_bytes(),
                &salt.to_big_endian(),
                init_code_hash.as_bytes(),
            ]
            .concat(),
        )
        .as_bytes()
        .get(12..)
        .ok_or(InternalError::Slicing)?,
    );
    Ok(generated_address)
}

// ================== Backup related functions =======================

/// Restore the state of the cache to the state it in the callframe backup.
/// Also restores BAL recorder state changes (but not touched_addresses) per EIP-7928.
pub fn restore_cache_state(
    db: &mut GeneralizedDatabase,
    callframe_backup: CallFrameBackup,
) -> Result<(), VMError> {
    for (address, account) in callframe_backup.original_accounts_info {
        if let Some(current_account) = db.current_accounts_state.get_mut(&address) {
            current_account.info = account.info;
            current_account.status = account.status;
            current_account.has_storage = account.has_storage;
            current_account.exists = account.exists;
        }
    }

    for (address, storage) in callframe_backup.original_account_storage_slots {
        // This call to `get_account_mut` should never return None, because we are looking up accounts
        // that had their storage modified, which means they should be in the cache. That's why
        // we return an internal error in case we haven't found it.
        let account = db
            .current_accounts_state
            .get_mut(&address)
            .ok_or(InternalError::AccountNotFound)?;

        for (key, value) in storage {
            account.storage.insert(key, value);
        }
    }

    // Evict codes the reverted frame(s) deployed: a stale by-hash cache entry
    // would serve a later read of the same hash (from a pre-existing account)
    // without hitting the store, hiding the read from execution-witness
    // recording (EIP-8025). Only hashes that were NOT cached before the frame
    // are tracked, so committed or store-loaded codes are never evicted.
    for code_hash in callframe_backup.inserted_code_hashes {
        db.codes.remove(&code_hash);
    }

    // Restore BAL recorder to checkpoint (but keep touched_addresses per EIP-7928)
    if let Some(checkpoint) = callframe_backup.bal_checkpoint
        && let Some(recorder) = db.bal_recorder.as_mut()
    {
        recorder.restore(checkpoint);
    }

    Ok(())
}

// ================= Blob hash related functions =====================
pub fn get_base_fee_per_blob_gas(
    block_excess_blob_gas: Option<u64>,
    evm_config: &EVMConfig,
) -> Result<U256, VMError> {
    let base_fee_update_fraction = evm_config.blob_schedule.base_fee_update_fraction;
    let excess_blob_gas = block_excess_blob_gas.unwrap_or_default();

    fake_exponential(
        MIN_BASE_FEE_PER_BLOB_GAS.into(),
        excess_blob_gas.into(),
        base_fee_update_fraction,
    )
    .map_err(|err| VMError::Internal(InternalError::FakeExponentialError(err)))
}

/// Gets the max blob gas cost for a transaction that a user is
/// willing to pay.
pub fn get_max_blob_gas_price(
    tx_blob_hashes: &[H256],
    tx_max_fee_per_blob_gas: Option<U256>,
) -> Result<U256, VMError> {
    let blobhash_amount: u64 = tx_blob_hashes
        .len()
        .try_into()
        .map_err(|_| InternalError::TypeConversion)?;

    let blob_gas_used: u64 = blobhash_amount
        .checked_mul(BLOB_GAS_PER_BLOB)
        .unwrap_or_default();

    let max_blob_gas_cost = tx_max_fee_per_blob_gas
        .unwrap_or_default()
        .checked_mul(blob_gas_used.into())
        .ok_or(InternalError::Overflow)?;

    Ok(max_blob_gas_cost)
}
/// Calculate the actual blob gas cost.
pub fn calculate_blob_gas_cost(
    tx_blob_hashes: &[H256],
    base_blob_fee_per_gas: U256,
) -> Result<U256, VMError> {
    let blobhash_amount: u64 = tx_blob_hashes
        .len()
        .try_into()
        .map_err(|_| InternalError::TypeConversion)?;

    let blob_gas_used: u64 = blobhash_amount
        .checked_mul(BLOB_GAS_PER_BLOB)
        .unwrap_or_default();

    let blob_gas_used: U256 = blob_gas_used.into();
    let blob_fee: U256 = blob_gas_used
        .checked_mul(base_blob_fee_per_gas)
        .ok_or(InternalError::Overflow)?;

    Ok(blob_fee)
}

// ==================== Word related functions =======================
pub fn word_to_address(word: U256) -> Address {
    Address::from_slice(&u256_to_big_endian(word)[12..])
}

// ================== EIP-7702 related functions =====================

pub fn code_has_delegation(code: &[u8]) -> Result<bool, VMError> {
    if code.len() == EIP7702_DELEGATED_CODE_LEN {
        let first_3_bytes = &code.get(..3).ok_or(InternalError::Slicing)?;
        return Ok(*first_3_bytes == SET_CODE_DELEGATION_BYTES);
    }
    Ok(false)
}

/// Gets the address inside the bytecode if it has been
/// delegated as the EIP7702 determines.
pub fn get_authorized_address_from_code(code: &[u8]) -> Result<Address, VMError> {
    if code_has_delegation(code)? {
        let address_bytes = &code
            .get(SET_CODE_DELEGATION_BYTES.len()..)
            .ok_or(InternalError::Slicing)?;
        // It shouldn't panic when doing Address::from_slice()
        // because the length is checked inside the code_has_delegation() function
        let address = Address::from_slice(address_bytes);
        Ok(address)
    } else {
        // if we end up here, it means that the address wasn't previously delegated.
        Err(InternalError::AccountNotDelegated.into())
    }
}

pub fn eip7702_recover_address(
    auth_tuple: &AuthorizationTuple,
    crypto: &dyn ethrex_crypto::Crypto,
) -> Result<Option<Address>, VMError> {
    use ethrex_rlp::encode::RLPEncode;

    if auth_tuple.s_signature > *SECP256K1_ORDER_OVER2 || U256::zero() >= auth_tuple.s_signature {
        return Ok(None);
    }
    if auth_tuple.r_signature > *SECP256K1_ORDER || U256::zero() >= auth_tuple.r_signature {
        return Ok(None);
    }
    if auth_tuple.y_parity != U256::one() && auth_tuple.y_parity != U256::zero() {
        return Ok(None);
    }

    let mut rlp_buf = Vec::with_capacity(128);
    rlp_buf.push(MAGIC);
    (auth_tuple.chain_id, auth_tuple.address, auth_tuple.nonce).encode(&mut rlp_buf);
    let msg = crypto.keccak256(&rlp_buf);

    let y_parity: u8 =
        TryInto::<u8>::try_into(auth_tuple.y_parity).map_err(|_| InternalError::TypeConversion)?;

    let mut sig = [0u8; 65];
    sig[..32].copy_from_slice(&auth_tuple.r_signature.to_big_endian());
    sig[32..64].copy_from_slice(&auth_tuple.s_signature.to_big_endian());
    sig[64] = y_parity;

    match crypto.recover_signer(&sig, &msg) {
        Ok(address) => Ok(Some(address)),
        Err(_) => Ok(None),
    }
}

/// Gets code of an account, returning early if it's not a delegated account, otherwise
/// Returns tuple (is_delegated, eip7702_cost, code_address, code).
/// Notice that it also inserts the delegated account to the "accessed accounts" set.
///
/// Where:
/// - `is_delegated`: True if account is a delegated account.
/// - `eip7702_cost`: Cost of accessing the delegated account (if any)
/// - `code_address`: Code address (if delegated, returns the delegated address)
/// - `code`: Bytecode of the code_address, what the EVM will execute.
pub fn eip7702_get_code(
    db: &mut GeneralizedDatabase,
    accrued_substate: &mut Substate,
    address: Address,
) -> Result<(bool, u64, Address, Code), VMError> {
    let (bytecode, delegation) = eip7702_peek_delegation(db, accrued_substate, address)?;
    let Some((auth_address, access_cost)) = delegation else {
        return Ok((false, 0, address, bytecode));
    };

    accrued_substate.add_accessed_address(auth_address);
    let authorized_bytecode = db.get_account_code(auth_address)?.clone();

    Ok((true, access_cost, auth_address, authorized_bytecode))
}

/// First half of [`eip7702_get_code`]: read `address`'s code and detect a
/// delegation designation WITHOUT touching the delegate account.
///
/// Returns `address`'s code and, when delegated, the delegate address with
/// its warm/cold access cost (computed from the current substate, not
/// recorded). CALL-family opcodes use this to gas-check the delegation
/// access cost before reading the delegate (EELS order); reading it earlier
/// would leak the delegate account into execution witnesses on OOG.
pub fn eip7702_peek_delegation(
    db: &mut GeneralizedDatabase,
    substate: &Substate,
    address: Address,
) -> Result<(Code, Option<(Address, u64)>), VMError> {
    let bytecode = db.get_account_code(address)?.clone();
    if !code_has_delegation(bytecode.code())? {
        return Ok((bytecode, None));
    }
    let auth_address = get_authorized_address_from_code(bytecode.code())?;
    let access_cost = if substate.is_address_accessed(&auth_address) {
        WARM_ADDRESS_ACCESS_COST
    } else {
        COLD_ADDRESS_ACCESS_COST
    };
    Ok((bytecode, Some((auth_address, access_cost))))
}

/// Precomputed intrinsic-gas components for a transaction.
///
/// Computed once per tx in the prepare-execution hook and reused by
/// [`VM::validate_min_gas_limit`](crate::hooks::default_hook::validate_min_gas_limit)
/// and [`VM::add_intrinsic_gas`]. Previously the full calldata / access-list /
/// auth-list walk ran 2-3x per tx (once in each function, plus the pre-Amsterdam
/// floor's own `tx_calldata`).
#[derive(Clone, Copy, Debug)]
pub struct IntrinsicGas {
    /// Regular (EIP-8037) intrinsic-gas arm.
    pub regular: u64,
    /// State (EIP-8037, Amsterdam+) intrinsic-gas arm; always 0 pre-Amsterdam.
    pub state: u64,
    /// `gas_cost::tx_calldata` over `current_call_frame.calldata`. Reused by the
    /// pre-Amsterdam floor check (same byte string, same point in execution).
    pub calldata_cost: u64,
}

impl<'a> VM<'a> {
    /// Sets the account code as the EIP7702 determines.
    pub fn eip7702_set_access_code(&mut self) -> Result<(), VMError> {
        let mut refunded_gas: u64 = 0;
        // IMPORTANT:
        // 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.
        // If transaction execution results in failure (any exceptional condition or code reverting), setting delegation designations is not rolled back.
        for auth_tuple in self.tx.authorization_list().cloned().unwrap_or_default() {
            let chain_id_not_equals_this_chain_id = auth_tuple.chain_id != self.env.chain_id;
            let chain_id_not_zero = !auth_tuple.chain_id.is_zero();

            // 1. Verify the chain id is either 0 or the chain’s current ID.
            if chain_id_not_zero && chain_id_not_equals_this_chain_id {
                continue;
            }

            // 2. Verify the nonce is less than 2**64 - 1.
            // NOTE: nonce is a u64, it's always less than or equal to u64::MAX
            if auth_tuple.nonce == u64::MAX {
                continue;
            }

            // 3. authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s)
            //      s value must be less than or equal to secp256k1n/2, as specified in EIP-2.
            let Some(authority_address) = eip7702_recover_address(&auth_tuple, self.crypto)? else {
                continue;
            };

            // 4. Add authority to accessed_addresses (as defined in EIP-2929).
            let authority_account = self.db.get_account(authority_address)?;
            let authority_exists = authority_account.exists;
            let authority_info = authority_account.info.clone();
            let authority_code = self.db.get_code(authority_info.code_hash)?;
            self.substate.add_accessed_address(authority_address);

            // 5. Verify the code of authority is either empty or already delegated.
            // Check this BEFORE recording to BAL so we can release the borrow on authority_code.
            let authority_code_is_empty = authority_code.is_empty();
            let empty_or_delegated =
                authority_code_is_empty || code_has_delegation(authority_code.code())?;

            // Record authority as touched for BAL per EIP-7928, even if validation fails later.
            // This ensures authority appears in BAL with empty change set when:
            // - Authority was loaded (above)
            // - But validation fails (checks below)
            if let Some(recorder) = self.db.bal_recorder.as_mut() {
                recorder.record_touched_address(authority_address);
            }

            if !empty_or_delegated {
                continue;
            }

            // 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.
            // If it doesn't exist, it means the nonce is zero. The get_account() function will return Account::default()
            // If it has nonce, the account.info.nonce should equal auth_tuple.nonce
            if authority_info.nonce != auth_tuple.nonce {
                continue;
            }

            // 7. Refund if authority exists in the trie.
            // EIP-8037 (Amsterdam+): return STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte
            // to the state gas reservoir (the new-account portion of the auth state charge).
            // Pre-Amsterdam: add REFUND_AUTH_PER_EXISTING_ACCOUNT (12500) to global refund counter.
            // NOTE: Uses `exists` (account_exists in EELS / Exist in geth), NOT `!is_empty()`.
            // An account can exist in the trie but be empty (e.g., has non-empty storage root).
            if authority_exists {
                if self.env.config.fork >= Fork::Amsterdam {
                    // EIP-7702: refund
                    // `STATE_BYTES_PER_NEW_ACCOUNT * cpsb` for each existing authority via
                    // two independent channels:
                    //   1. `state_gas_reservoir += refund` — sender gets the gas back via
                    //      receipt refund at tx finalize.
                    //   2. `state_refund += refund` — block-level state-gas accounting
                    //      subtracts this at refund_sender (mirrors EELS
                    //      `MessageCallOutput.state_refund`).
                    // `state_gas_used` is NOT decremented here: the refund goes through
                    // `state_refund` (tx-level channel) so block-level accounting subtracts it.
                    let refund = self.state_gas_new_account;
                    self.state_gas_reservoir = self
                        .state_gas_reservoir
                        .checked_add(refund)
                        .ok_or(InternalError::Overflow)?;
                    self.state_refund = self
                        .state_refund
                        .checked_add(refund)
                        .ok_or(InternalError::Overflow)?;
                } else {
                    refunded_gas = refunded_gas
                        .checked_add(REFUND_AUTH_PER_EXISTING_ACCOUNT)
                        .ok_or(InternalError::Overflow)?;
                }
            }

            // EIP-7702: refill the
            // `STATE_BYTES_PER_AUTH_BASE * cpsb` portion of intrinsic state gas
            // when no new delegation indicator bytes are written. That covers
            // two cases:
            //   1. Authority's code slot already holds a delegation indicator
            //      (overwrite or clear in place — PR #2836).
            //   2. The auth is a clear (`auth.address == 0x00`) against an
            //      authority with no prior code — also writes zero bytes
            //      (PR #2848).
            // Step 5 already restricts non-empty pre-state code to a valid
            // delegation indicator, so checking `!authority_code_is_empty` is
            // equivalent to EELS's `code_hash != EMPTY_CODE_HASH`.
            let writes_no_new_indicator =
                !authority_code_is_empty || auth_tuple.address == Address::zero();
            if self.env.config.fork >= Fork::Amsterdam && writes_no_new_indicator {
                let refund = self.state_gas_auth_base;
                self.state_gas_reservoir = self
                    .state_gas_reservoir
                    .checked_add(refund)
                    .ok_or(InternalError::Overflow)?;
                self.state_refund = self
                    .state_refund
                    .checked_add(refund)
                    .ok_or(InternalError::Overflow)?;
            }

            // 8. Set the code of authority to be 0xef0100 || address. This is a delegation designation.
            let delegation_bytes = [
                &SET_CODE_DELEGATION_BYTES[..],
                auth_tuple.address.as_bytes(),
            ]
            .concat();

            // As a special case, if address is 0x0000000000000000000000000000000000000000 do not write the designation.
            // Clear the account’s code and reset the account’s code hash to the empty hash.
            let code = if auth_tuple.address != Address::zero() {
                delegation_bytes.into()
            } else {
                Bytes::new()
            };
            self.update_account_bytecode(
                authority_address,
                Code::from_bytecode(code, self.crypto),
            )?;

            // 9. Increase the nonce of authority by one.
            self.increment_account_nonce(authority_address)
                .map_err(|_| TxValidationError::NonceIsMax)?;
        }

        self.substate.refunded_gas = self
            .substate
            .refunded_gas
            .checked_add(refunded_gas)
            .ok_or(InternalError::Overflow)?;

        Ok(())
    }

    pub fn add_intrinsic_gas(&mut self, intrinsic: &IntrinsicGas) -> Result<(), VMError> {
        // Intrinsic gas is the gas consumed by the transaction before the execution of the opcodes. Section 6.2 in the Yellow Paper.

        let regular_gas = intrinsic.regular;
        let state_gas = intrinsic.state;

        let total_gas = regular_gas.checked_add(state_gas).ok_or(OutOfGas)?;

        self.current_call_frame
            .increase_consumed_gas(total_gas)
            .map_err(|_| TxValidationError::IntrinsicGasTooLow)?;

        // state_gas_used is i64; intrinsic state gas is bounded by tx gas limit (< i64::MAX).
        self.state_gas_used = self
            .state_gas_used
            .checked_add(i64::try_from(state_gas).map_err(|_| InternalError::Overflow)?)
            .ok_or(InternalError::Overflow)?;
        // Remember the intrinsic split so we can leave it in state_gas_used on top-level
        // error (matches EELS `tx_env.intrinsic_state_gas`, which is kept separate from
        // `tx_output.state_gas_used` and never refunded).
        debug_assert_eq!(self.intrinsic_state_gas, 0, "intrinsic_state_gas set twice");
        self.intrinsic_state_gas = state_gas;

        // EIP-8037 (Amsterdam+): compute state gas reservoir from excess gas_limit.
        // execution_gas = what remains after all intrinsic gas; regular_gas_budget = how much
        // regular execution gas is allowed (capped at TX_MAX_GAS_LIMIT_AMSTERDAM); the difference becomes
        // the reservoir for drawing state gas without consuming regular gas_remaining.
        if self.env.config.fork >= Fork::Amsterdam {
            if self.env.is_system_call {
                // EIP-8037: system
                // transactions get a dedicated state-gas reservoir of
                // `state_gas_storage_set * SYSTEM_MAX_SSTORES_PER_CALL` ON TOP of
                // the full SYS_CALL_GAS_LIMIT regular budget — so SSTORE-heavy
                // system contracts (EIP-2935, EIP-4788) cannot OOG on state-gas
                // growth alone. Skip the regular reservoir computation so we don't
                // pre-consume `gas_remaining`; EELS sets `intrinsic_regular_gas=0`
                // and `gas=SYSTEM_TRANSACTION_GAS` for the message
                // (amsterdam/fork.py::process_unchecked_system_transaction).
                let sys_reservoir = self
                    .state_gas_storage_set
                    .saturating_mul(SYSTEM_MAX_SSTORES_PER_CALL);
                self.state_gas_reservoir = sys_reservoir;
                self.state_gas_reservoir_initial = sys_reservoir;
            } else {
                let gas_limit = self.tx.gas_limit();
                let execution_gas = gas_limit.saturating_sub(total_gas);
                let regular_gas_budget = TX_MAX_GAS_LIMIT_AMSTERDAM.saturating_sub(regular_gas);
                let gas_left = regular_gas_budget.min(execution_gas);
                let reservoir = execution_gas.saturating_sub(gas_left);
                if reservoir > 0 {
                    // Pre-consume reservoir from gas_remaining so GAS opcode returns <= TX_MAX_GAS_LIMIT_AMSTERDAM
                    let reservoir_i64 =
                        i64::try_from(reservoir).map_err(|_| InternalError::Overflow)?;
                    self.current_call_frame.gas_remaining = self
                        .current_call_frame
                        .gas_remaining
                        .checked_sub(reservoir_i64)
                        .ok_or(InternalError::Overflow)?;
                    self.state_gas_reservoir = reservoir;
                }
                // Capture initial reservoir for block-dimensional regular gas computation.
                self.state_gas_reservoir_initial = reservoir;
            }
        }

        Ok(())
    }

    // ==================== Gas related functions =======================
    /// Returns `(regular_gas, state_gas)` intrinsic gas for the transaction.
    /// For Amsterdam+, state_gas is the EIP-8037 state portion.
    /// For pre-Amsterdam, state_gas is always 0.
    pub fn get_intrinsic_gas(&self) -> Result<IntrinsicGas, VMError> {
        // Intrinsic Gas = Calldata cost + Create cost + Base cost + Access list cost
        let mut regular_gas: u64 = 0;
        let mut state_gas: u64 = 0;
        let fork = self.env.config.fork;

        // Calldata Cost
        // 4 gas for each zero byte in the transaction data 16 gas for each non-zero byte in the transaction.
        let calldata_cost = gas_cost::tx_calldata(&self.current_call_frame.calldata)?;

        regular_gas = regular_gas.checked_add(calldata_cost).ok_or(OutOfGas)?;

        // Base Cost
        regular_gas = regular_gas.checked_add(TX_BASE_COST).ok_or(OutOfGas)?;

        // Create Cost
        if self.is_create()? {
            if fork >= Fork::Amsterdam {
                // EIP-8037: reduced regular cost + state gas for new account
                regular_gas = regular_gas
                    .checked_add(REGULAR_GAS_CREATE)
                    .ok_or(OutOfGas)?;
                state_gas = state_gas
                    .checked_add(self.state_gas_new_account)
                    .ok_or(OutOfGas)?;
            } else {
                // https://eips.ethereum.org/EIPS/eip-2#specification
                regular_gas = regular_gas.checked_add(CREATE_BASE_COST).ok_or(OutOfGas)?;
            }

            // https://eips.ethereum.org/EIPS/eip-3860
            if fork >= Fork::Shanghai {
                let number_of_words = &self.current_call_frame.calldata.len().div_ceil(WORD_SIZE);
                let double_number_of_words: u64 = number_of_words
                    .checked_mul(2)
                    .ok_or(OutOfGas)?
                    .try_into()
                    .map_err(|_| InternalError::TypeConversion)?;

                regular_gas = regular_gas
                    .checked_add(double_number_of_words)
                    .ok_or(OutOfGas)?;
            }
        }

        // Access List Cost
        let mut access_lists_cost: u64 = 0;
        for (_, keys) in self.tx.access_list() {
            access_lists_cost = access_lists_cost
                .checked_add(ACCESS_LIST_ADDRESS_COST)
                .ok_or(OutOfGas)?;
            for _ in keys {
                access_lists_cost = access_lists_cost
                    .checked_add(ACCESS_LIST_STORAGE_KEY_COST)
                    .ok_or(OutOfGas)?;
            }
        }

        // EIP-7981 (Amsterdam+): access-list data bytes also contribute to the regular arm.
        // access_list_cost += floor_tokens_in_access_list * total_cost_floor_per_token
        // = access_list_bytes * STANDARD_TOKEN_COST * total_cost_floor_per_token
        // Effective: +1280 per address, +2048 per storage key.
        if fork >= Fork::Amsterdam {
            let al_floor_tokens = floor_tokens_in_access_list(self.tx.access_list());
            let al_data_cost = al_floor_tokens
                .checked_mul(total_cost_floor_per_token(fork))
                .ok_or(InternalError::Overflow)?;
            access_lists_cost = access_lists_cost
                .checked_add(al_data_cost)
                .ok_or(InternalError::Overflow)?;
        }

        regular_gas = regular_gas.checked_add(access_lists_cost).ok_or(OutOfGas)?;

        // Authorization List Cost
        // `unwrap_or_default` will return an empty vec when the `authorization_list` field is None.
        // If the vec is empty, the len will be 0, thus the authorization_list_cost is 0.
        let amount_of_auth_tuples: u64 = match self.tx.authorization_list() {
            None => 0,
            Some(list) => list
                .len()
                .try_into()
                .map_err(|_| InternalError::TypeConversion)?,
        };

        if fork >= Fork::Amsterdam {
            // EIP-8037: per-auth regular cost is PER_AUTH_BASE_COST, state is STATE_BYTES_PER_AUTH_TOTAL * cost_per_state_byte
            let regular_auth_cost = PER_AUTH_BASE_COST
                .checked_mul(amount_of_auth_tuples)
                .ok_or(InternalError::Overflow)?;
            regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?;
            let state_auth_cost = self
                .state_gas_auth_total
                .checked_mul(amount_of_auth_tuples)
                .ok_or(InternalError::Overflow)?;
            state_gas = state_gas.checked_add(state_auth_cost).ok_or(OutOfGas)?;
        } else {
            let authorization_list_cost = PER_EMPTY_ACCOUNT_COST
                .checked_mul(amount_of_auth_tuples)
                .ok_or(InternalError::Overflow)?;
            regular_gas = regular_gas
                .checked_add(authorization_list_cost)
                .ok_or(OutOfGas)?;
        }

        Ok(IntrinsicGas {
            regular: regular_gas,
            state: state_gas,
            calldata_cost,
        })
    }

    /// Calculates the minimum gas to be consumed in the transaction.
    pub fn get_min_gas_used(&self) -> Result<u64, VMError> {
        let fork = self.env.config.fork;

        // If the transaction is a CREATE transaction, the calldata is emptied and the bytecode is assigned.
        let calldata = if self.is_create()? {
            self.current_call_frame.bytecode.code()
        } else {
            self.current_call_frame.calldata.as_ref()
        };

        // EIP-7976 floor tokens: for the floor arm, all calldata bytes count unweighted.
        // floor_tokens_in_calldata = (zero_bytes + nonzero_bytes) * STANDARD_TOKEN_COST
        // Pre-Amsterdam uses the weighted EIP-7623 formula: (nonzero * 16 + zero * 4) / 4
        let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
            // EIP-7976: floor tokens = total_bytes * STANDARD_TOKEN_COST (unweighted).
            let total_bytes: u64 = calldata
                .len()
                .try_into()
                .map_err(|_| InternalError::TypeConversion)?;
            total_bytes
                .checked_mul(STANDARD_TOKEN_COST)
                .ok_or(InternalError::Overflow)?
        } else {
            // Pre-Amsterdam: weighted EIP-7623 token count.
            gas_cost::tx_calldata(calldata)? / STANDARD_TOKEN_COST
        };

        // EIP-7981 (Amsterdam+): access-list data bytes fold into the floor-token count.
        // floor_tokens_in_access_list = access_list_bytes * STANDARD_TOKEN_COST
        // where access_list_bytes = 20 * address_count + 32 * storage_key_count.
        if fork >= Fork::Amsterdam {
            let al_floor_tokens = floor_tokens_in_access_list(self.tx.access_list());
            tokens_in_calldata = tokens_in_calldata
                .checked_add(al_floor_tokens)
                .ok_or(InternalError::Overflow)?;
        }

        // min_gas_used = TX_BASE_COST + total_cost_floor_per_token(fork) * tokens
        // EIP-7976 (Amsterdam+) raises TOTAL_COST_FLOOR_PER_TOKEN from 10 to 16.
        let mut min_gas_used: u64 = tokens_in_calldata
            .checked_mul(total_cost_floor_per_token(fork))
            .ok_or(InternalError::Overflow)?;

        min_gas_used = min_gas_used
            .checked_add(TX_BASE_COST)
            .ok_or(InternalError::Overflow)?;

        Ok(min_gas_used)
    }

    /// Gets transaction callee, calculating create address if it's a "Create" transaction.
    /// Bool indicates whether it is a `create` transaction or not.
    pub fn get_tx_callee(
        tx: &Transaction,
        db: &mut GeneralizedDatabase,
        env: &Environment,
        substate: &mut Substate,
    ) -> Result<(Address, bool), VMError> {
        match tx.to() {
            TxKind::Call(address_to) => {
                substate.add_accessed_address(address_to);

                Ok((address_to, false))
            }

            TxKind::Create => {
                let sender_nonce = db.get_account(env.origin)?.info.nonce;

                let created_address = calculate_create_address(env.origin, sender_nonce);

                substate.add_accessed_address(created_address);
                substate.add_created_account(created_address);

                Ok((created_address, true))
            }
        }
    }
}

/// Compute `(regular, state)` intrinsic gas for a transaction without needing
/// a full VM instance. Mirrors `VM::get_intrinsic_gas` but operates on the raw
/// transaction, fork, and block gas limit (for cpsb derivation). Pre-Amsterdam
/// returns `(regular, 0)`.
///
/// Used by the block executor to perform the EIP-8037 (PR #2703) per-tx 2D
/// inclusion check before the tx runs.
pub fn intrinsic_gas_dimensions(
    tx: &Transaction,
    fork: Fork,
    block_gas_limit: u64,
) -> Result<(u64, u64), VMError> {
    let mut regular_gas: u64 = 0;
    let mut state_gas: u64 = 0;

    let (state_gas_new_account, state_gas_auth_total) = if fork >= Fork::Amsterdam {
        let cpsb = cost_per_state_byte(block_gas_limit);
        (
            STATE_BYTES_PER_NEW_ACCOUNT
                .checked_mul(cpsb)
                .ok_or(InternalError::Overflow)?,
            STATE_BYTES_PER_AUTH_TOTAL
                .checked_mul(cpsb)
                .ok_or(InternalError::Overflow)?,
        )
    } else {
        (0, 0)
    };

    // Calldata cost (EIP-2028 weighted)
    let calldata_cost = gas_cost::tx_calldata(tx.data())?;
    regular_gas = regular_gas.checked_add(calldata_cost).ok_or(OutOfGas)?;

    // Base cost
    regular_gas = regular_gas.checked_add(TX_BASE_COST).ok_or(OutOfGas)?;

    let is_create = matches!(tx.to(), TxKind::Create);
    if is_create {
        if fork >= Fork::Amsterdam {
            regular_gas = regular_gas
                .checked_add(REGULAR_GAS_CREATE)
                .ok_or(OutOfGas)?;
            state_gas = state_gas
                .checked_add(state_gas_new_account)
                .ok_or(OutOfGas)?;
        } else {
            regular_gas = regular_gas.checked_add(CREATE_BASE_COST).ok_or(OutOfGas)?;
        }

        // EIP-3860 init code words (Shanghai+)
        if fork >= Fork::Shanghai {
            let words = tx.data().len().div_ceil(WORD_SIZE);
            let double_words: u64 = words
                .checked_mul(2)
                .ok_or(OutOfGas)?
                .try_into()
                .map_err(|_| InternalError::TypeConversion)?;
            regular_gas = regular_gas.checked_add(double_words).ok_or(OutOfGas)?;
        }
    }

    // Access list cost
    let mut access_lists_cost: u64 = 0;
    for (_, keys) in tx.access_list() {
        access_lists_cost = access_lists_cost
            .checked_add(ACCESS_LIST_ADDRESS_COST)
            .ok_or(OutOfGas)?;
        for _ in keys {
            access_lists_cost = access_lists_cost
                .checked_add(ACCESS_LIST_STORAGE_KEY_COST)
                .ok_or(OutOfGas)?;
        }
    }

    // EIP-7981 (Amsterdam+): access-list data bytes fold into regular gas
    if fork >= Fork::Amsterdam {
        let al_floor_tokens = floor_tokens_in_access_list(tx.access_list());
        let al_data_cost = al_floor_tokens
            .checked_mul(total_cost_floor_per_token(fork))
            .ok_or(InternalError::Overflow)?;
        access_lists_cost = access_lists_cost
            .checked_add(al_data_cost)
            .ok_or(InternalError::Overflow)?;
    }
    regular_gas = regular_gas.checked_add(access_lists_cost).ok_or(OutOfGas)?;

    // Authorization list cost
    let amount_of_auth_tuples: u64 = match tx.authorization_list() {
        None => 0,
        Some(list) => list
            .len()
            .try_into()
            .map_err(|_| InternalError::TypeConversion)?,
    };

    if fork >= Fork::Amsterdam {
        let regular_auth_cost = PER_AUTH_BASE_COST
            .checked_mul(amount_of_auth_tuples)
            .ok_or(InternalError::Overflow)?;
        regular_gas = regular_gas.checked_add(regular_auth_cost).ok_or(OutOfGas)?;
        let state_auth_cost = state_gas_auth_total
            .checked_mul(amount_of_auth_tuples)
            .ok_or(InternalError::Overflow)?;
        state_gas = state_gas.checked_add(state_auth_cost).ok_or(OutOfGas)?;
    } else {
        let auth_cost = PER_EMPTY_ACCOUNT_COST
            .checked_mul(amount_of_auth_tuples)
            .ok_or(InternalError::Overflow)?;
        regular_gas = regular_gas.checked_add(auth_cost).ok_or(OutOfGas)?;
    }

    Ok((regular_gas, state_gas))
}

/// Standalone EIP-7623/7976/7981 floor gas for a transaction. Mirrors
/// [`VM::get_min_gas_used`] but operates on the raw transaction + fork, so it
/// can be called by mempool admission / the payload builder without needing a
/// VM instance. Returns `TX_BASE_COST + floor_rate * total_floor_tokens`.
///
/// Amsterdam+ uses the unweighted EIP-7976 floor (16 gas/token = 64 gas/byte)
/// and folds EIP-7981 access-list data bytes into the token count. Pre-
/// Amsterdam uses the weighted EIP-7623 formula.
///
/// A mismatch between this and `VM::get_min_gas_used` would cause mempool
/// admission to drift from VM rejection; keep the two in sync. The
/// `test_intrinsic_parity_*` suite also guards this.
pub fn intrinsic_gas_floor(tx: &Transaction, fork: Fork) -> Result<u64, VMError> {
    // EIP-7976: floor tokens count ALL calldata bytes unweighted. For CREATE
    // txs the calldata is the init code. Mirrors `get_min_gas_used`.
    let calldata = tx.data();

    let mut tokens_in_calldata: u64 = if fork >= Fork::Amsterdam {
        let total_bytes: u64 = calldata
            .len()
            .try_into()
            .map_err(|_| InternalError::TypeConversion)?;
        total_bytes
            .checked_mul(STANDARD_TOKEN_COST)
            .ok_or(InternalError::Overflow)?
    } else {
        gas_cost::tx_calldata(calldata)? / STANDARD_TOKEN_COST
    };

    if fork >= Fork::Amsterdam {
        let al_floor_tokens = floor_tokens_in_access_list(tx.access_list());
        tokens_in_calldata = tokens_in_calldata
            .checked_add(al_floor_tokens)
            .ok_or(InternalError::Overflow)?;
    }

    tokens_in_calldata
        .checked_mul(total_cost_floor_per_token(fork))
        .ok_or(InternalError::Overflow)?
        .checked_add(TX_BASE_COST)
        .ok_or(InternalError::Overflow.into())
}

/// Converts Account to LevmAccount
/// The problem with this is that we don't have the storage root.
pub fn account_to_levm_account(account: Account) -> (LevmAccount, Code) {
    (
        LevmAccount {
            info: account.info,
            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
            storage: account.storage,
            status: AccountStatus::Unmodified,
            exists: true,
        },
        account.code,
    )
}

/// Converts a U256 value into usize, returning an error if the value is over 32 bits
/// This is generally used for memory offsets and sizes, 32 bits is more than enough for this purpose.
#[expect(clippy::as_conversions)]
pub fn u256_to_usize(val: U256) -> Result<usize, VMError> {
    if val.0[0] > u32::MAX as u64 || val.0[1] != 0 || val.0[2] != 0 || val.0[3] != 0 {
        return Err(VMError::ExceptionalHalt(ExceptionalHalt::VeryLargeNumber));
    }
    Ok(val.0[0] as usize)
}

/// Converts U256 size and offset to usize.
/// If the size is zero, the offset will be zero regardless of its original value as it is not relevant
pub fn size_offset_to_usize(size: U256, offset: U256) -> Result<(usize, usize), VMError> {
    if size.is_zero() {
        // Offset is irrelevant
        Ok((0, 0))
    } else {
        Ok((u256_to_usize(size)?, u256_to_usize(offset)?))
    }
}

// ==================== EIP-7708 Helper Functions ====================

/// Creates EIP-7708 Transfer log (LOG3) for ETH transfers.
/// Emitted from SYSTEM_ADDRESS when ETH is transferred.
#[inline]
pub fn create_eth_transfer_log(from: Address, to: Address, value: U256) -> Log {
    let mut from_topic = [0u8; 32];
    from_topic[12..].copy_from_slice(from.as_bytes());

    let mut to_topic = [0u8; 32];
    to_topic[12..].copy_from_slice(to.as_bytes());

    let data = value.to_big_endian();

    Log {
        address: SYSTEM_ADDRESS,
        topics: vec![
            TRANSFER_EVENT_TOPIC,
            H256::from(from_topic),
            H256::from(to_topic),
        ],
        data: Bytes::from(data.to_vec()),
    }
}

/// Creates EIP-7708 Burn log (LOG2) for ETH burns.
/// Emitted from SYSTEM_ADDRESS when ETH is burned (e.g. via SELFDESTRUCT).
#[inline]
pub fn create_burn_log(address: Address, amount: U256) -> Log {
    let mut address_topic = [0u8; 32];
    address_topic[12..].copy_from_slice(address.as_bytes());

    let data = amount.to_big_endian();

    Log {
        address: SYSTEM_ADDRESS,
        topics: vec![BURN_EVENT_TOPIC, H256::from(address_topic)],
        data: Bytes::from(data.to_vec()),
    }
}