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}