1pub mod db;
2mod tracing;
3
4use super::{
5 BlockExecutionResult, ExecutionGasCap, FrameValidationOutcome, TxGasBreakdown,
6 compute_burned_fees,
7};
8use crate::system_contracts::{
9 AMSTERDAM_REQUEST_PREDEPLOYS, BEACON_ROOTS_ADDRESS, BUILDER_DEPOSIT_CONTRACT_ADDRESS,
10 BUILDER_EXIT_CONTRACT_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
11 EXPIRY_VERIFIER_PREDEPLOY, EXPIRY_VERIFIER_RUNTIME_BYTECODE, HISTORY_STORAGE_ADDRESS,
12 PRAGUE_SYSTEM_CONTRACTS, SYSTEM_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
13};
14use crate::{EvmError, ExecutionResult};
15use bytes::Bytes;
16use ethrex_common::H256;
17#[cfg(feature = "rayon")]
18use ethrex_common::constants::EMPTY_KECCAK_HASH;
19#[cfg(feature = "rayon")]
20use ethrex_common::seeded_hash::{SeededHashMap, SeededHashSet};
21#[cfg(feature = "rayon")]
22use ethrex_common::types::TxType;
23use ethrex_common::types::block_access_list::BlockAccessList;
24#[cfg(feature = "rayon")]
25use ethrex_common::types::block_access_list::{
26 AccountChanges, BalAddressIndex, find_exact_change_balance, find_exact_change_code,
27 find_exact_change_nonce, find_exact_change_storage, has_exact_change_balance,
28 has_exact_change_code, has_exact_change_nonce, has_exact_change_storage,
29};
30use ethrex_common::types::fee_config::FeeConfig;
31use ethrex_common::types::{AuthorizationTuple, EIP7702Transaction};
32use ethrex_common::types::{Code, InitcodeBuffer};
33#[cfg(feature = "rayon")]
34use ethrex_common::utils::u256_from_big_endian_const;
35use ethrex_common::{
36 Address, U256,
37 types::{
38 AccessList, AccountUpdate, Block, BlockHeader, EIP1559Transaction, Fork, FrameReceipt,
39 GWEI_TO_WEI, GenericTransaction, INITIAL_BASE_FEE, Log, Receipt, Transaction, TxKind,
40 Withdrawal, requests::Requests,
41 },
42};
43#[cfg(feature = "rayon")]
44use ethrex_common::{
45 BigEndianHash, constants::AMSTERDAM_MAX_CODE_SIZE, validate_bal_code_sizes,
46 validate_block_access_list_size, validate_header_bal_indices,
47};
48use ethrex_crypto::Crypto;
49use ethrex_levm::EVMConfig;
50use ethrex_levm::StatelessValidator;
51#[cfg(feature = "rayon")]
52use ethrex_levm::account::AccountStatus;
53use ethrex_levm::call_frame::Stack;
54use ethrex_levm::constants::{
55 POST_OSAKA_GAS_LIMIT_CAP, STACK_LIMIT, SYS_CALL_GAS_LIMIT, TX_MAX_GAS_LIMIT_AMSTERDAM,
56};
57use ethrex_levm::db::gen_db::GeneralizedDatabase;
58#[cfg(feature = "rayon")]
59use ethrex_levm::db::gen_db::{
60 LazyBalCursor, code_from_bal, post_value_at_or_before, seed_one_address_info_from_bal,
61};
62#[cfg(feature = "rayon")]
63use ethrex_levm::db::{Database, gen_db::CacheDB};
64use ethrex_levm::errors::{InternalError, TxValidationError};
65use ethrex_levm::memory::Memory;
66#[cfg(feature = "perf_opcode_timings")]
67use ethrex_levm::timings::{OPCODE_TIMINGS, PRECOMPILES_TIMINGS};
68use ethrex_levm::tracing::LevmCallTracer;
69use ethrex_levm::utils::get_base_fee_per_blob_gas;
70use ethrex_levm::utils::intrinsic_gas_floor;
71use ethrex_levm::validation_observer::FrameSimViolation;
72use ethrex_levm::vm::VMType;
73use ethrex_levm::{
74 Environment,
75 errors::{ExecutionReport, TxResult, VMError},
76 vm::VM,
77};
78#[cfg(feature = "rayon")]
79use rayon::iter::{IntoParallelIterator, ParallelIterator};
80#[cfg(feature = "rayon")]
81use rayon::slice::ParallelSlice;
82#[cfg(feature = "rayon")]
83use rustc_hash::FxHashMap;
84use std::cmp::min;
85use std::sync::Arc;
86#[cfg(feature = "rayon")]
87use std::sync::atomic::AtomicBool;
88use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
89use std::sync::mpsc::Sender;
90
91#[cfg(feature = "rayon")]
95const BAL_WARM_CHUNK_ACCOUNTS: usize = 64;
96
97#[cfg(feature = "rayon")]
100const PRE_BLOCK_BAL_INDEX: u32 = 0;
101
102fn lstar_burned_fees(
110 chain_config: ðrex_common::types::ChainConfig,
111 header: ðrex_common::types::BlockHeader,
112 post_refund_gas: u64,
113) -> u64 {
114 let evm_cfg = EVMConfig::new_from_chain_config(chain_config, header);
115 let blob_base_fee = get_base_fee_per_blob_gas(header.excess_blob_gas, &evm_cfg)
116 .map(|v| u64::try_from(v).unwrap_or(u64::MAX))
117 .unwrap_or(0);
118 let blob_gas_used = header.blob_gas_used.unwrap_or(0);
119 compute_burned_fees(
120 header.base_fee_per_gas.unwrap_or(0),
121 post_refund_gas,
123 blob_base_fee,
124 blob_gas_used,
125 )
126}
127
128#[derive(Debug)]
134pub struct LEVM;
135
136fn frame_receipts_from(
140 frame_results: Option<Vec<(u8, u64, Vec<Log>)>>,
141) -> Option<Vec<FrameReceipt>> {
142 frame_results.map(|results| {
143 results
144 .into_iter()
145 .map(|(status, gas_used, logs)| FrameReceipt {
146 status,
147 gas_used,
148 logs,
149 })
150 .collect()
151 })
152}
153
154fn check_gas_limit(
156 block_gas_used: u64,
157 tx_gas_limit: u64,
158 block_gas_limit: u64,
159) -> Result<(), EvmError> {
160 if tx_gas_limit > block_gas_limit.saturating_sub(block_gas_used) {
161 return Err(EvmError::Transaction(format!(
162 "Gas allowance exceeded: \
163 used {block_gas_used} + tx limit {tx_gas_limit} > block limit {block_gas_limit}"
164 )));
165 }
166 Ok(())
167}
168
169pub fn block_work_budget(block_gas_limit: u64) -> u64 {
176 block_gas_limit.saturating_mul(2)
177}
178
179pub fn check_minimum_block_work<'a>(
192 txs_with_sender: impl IntoIterator<Item = (&'a Transaction, Address)>,
193 fork: Fork,
194 block_gas_limit: u64,
195) -> Result<(), EvmError> {
196 let budget = block_work_budget(block_gas_limit);
197 let mut floor_total = 0_u64;
198 for (tx, sender) in txs_with_sender {
199 let floor = intrinsic_gas_floor(tx, sender, fork)
203 .map_err(|e| EvmError::Transaction(format!("intrinsic gas floor: {e}")))?;
204 floor_total = floor_total.saturating_add(floor);
205 if floor_total > budget {
206 return Err(EvmError::Transaction(format!(
207 "Gas allowance exceeded: minimum gas of the block's transactions \
208 {floor_total} exceeds the block work budget {budget} \
209 (block_gas_limit={block_gas_limit})"
210 )));
211 }
212 }
213 Ok(())
214}
215
216#[derive(Default)]
227pub struct CompletedGas {
228 regular: AtomicU64,
229 state: AtomicU64,
230}
231
232impl CompletedGas {
233 pub fn check(&self, block_gas_limit: u64) -> Result<(), EvmError> {
235 let regular = self.regular.load(Ordering::Relaxed);
236 let state = self.state.load(Ordering::Relaxed);
237 if regular > block_gas_limit || state > block_gas_limit {
238 return Err(EvmError::Transaction(format!(
239 "Gas allowance exceeded: transactions completed during parallel execution \
240 used regular={regular} state={state}, over block_gas_limit={block_gas_limit}"
241 )));
242 }
243 Ok(())
244 }
245
246 pub fn record(
248 &self,
249 regular_gas: u64,
250 state_gas: u64,
251 block_gas_limit: u64,
252 ) -> Result<(), EvmError> {
253 self.regular.fetch_add(regular_gas, Ordering::Relaxed);
254 self.state.fetch_add(state_gas, Ordering::Relaxed);
255 self.check(block_gas_limit)
256 }
257}
258
259pub fn check_2d_gas_allowance(
278 tx: &Transaction,
279 block_gas_used_regular: u64,
280 block_gas_used_state: u64,
281 block_gas_limit: u64,
282) -> Result<(), EvmError> {
283 let tx_gas = tx.gas_limit();
284 let regular_available = block_gas_limit.saturating_sub(block_gas_used_regular);
285 let state_available = block_gas_limit.saturating_sub(block_gas_used_state);
286
287 let regular_contrib = tx_gas.min(TX_MAX_GAS_LIMIT_AMSTERDAM);
292 if regular_contrib > regular_available {
293 return Err(EvmError::Transaction(format!(
294 "Gas allowance exceeded: regular dim worst-case {regular_contrib} > \
295 available {regular_available} (block_gas_used_regular={block_gas_used_regular}, \
296 block_gas_limit={block_gas_limit})"
297 )));
298 }
299
300 let state_contrib = tx_gas;
302 if state_contrib > state_available {
303 return Err(EvmError::Transaction(format!(
304 "Gas allowance exceeded: state dim worst-case {state_contrib} > \
305 available {state_available} (block_gas_used_state={block_gas_used_state}, \
306 block_gas_limit={block_gas_limit})"
307 )));
308 }
309
310 Ok(())
311}
312
313#[cfg(feature = "rayon")]
319#[derive(Debug, thiserror::Error)]
320pub enum BalValidationError {
321 #[error("{0}")]
322 Mismatch(String),
323 #[error("{0}")]
324 Database(String),
325}
326
327#[cfg(feature = "rayon")]
334fn first_failure_by_key<K: Ord, V, S, E>(
335 map: &std::collections::HashMap<K, V, S>,
336 mut check: impl FnMut(&K, &V) -> Result<(), E>,
337) -> Result<(), E> {
338 if map
339 .iter()
340 .try_for_each(|(key, value)| check(key, value))
341 .is_ok()
342 {
343 return Ok(());
344 }
345 let mut entries: Vec<(&K, &V)> = map.iter().collect();
346 entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
347 entries
348 .into_iter()
349 .try_for_each(|(key, value)| check(key, value))
350}
351
352impl LEVM {
353 pub fn execute_block(
358 block: &Block,
359 db: &mut GeneralizedDatabase,
360 vm_type: VMType,
361 crypto: &dyn Crypto,
362 stateless_validator: Option<&dyn StatelessValidator>,
363 ) -> Result<(BlockExecutionResult, Option<BlockAccessList>), EvmError> {
364 let chain_config = db.store.get_chain_config()?;
365 let is_amsterdam = chain_config.is_amsterdam_activated(block.header.timestamp);
366 let is_lstar = chain_config.is_lstar_activated(block.header.timestamp);
368
369 debug_assert!(
374 block.body.transactions.len() < u32::MAX as usize,
375 "tx count overflows u32 BlockAccessIndex"
376 );
377
378 if is_amsterdam {
380 db.enable_bal_recording();
381 db.set_bal_index(0);
383 }
384
385 Self::prepare_block(block, db, vm_type, crypto)?;
386
387 let evm_config = EVMConfig::new_from_chain_config(&chain_config, &block.header);
391 let chain_id = chain_config.chain_id;
392 let base_blob_fee_per_gas =
393 get_base_fee_per_blob_gas(block.header.excess_blob_gas, &evm_config)?;
394 let mut shared_stack_pool = Vec::with_capacity(STACK_LIMIT);
397 let mut shared_memory_pool = Vec::with_capacity(1);
398 let mut shared_initcode_pool = Vec::new();
399
400 let n_txs = block.body.transactions.len();
401 let mut receipts = Vec::with_capacity(n_txs);
402 let mut tx_gas_breakdowns: Vec<TxGasBreakdown> = Vec::with_capacity(n_txs);
403 let mut cumulative_gas_used = 0_u64;
405 let mut block_gas_used = 0_u64;
407 let mut block_regular_gas_used = 0_u64;
409 let mut block_state_gas_used = 0_u64;
410 let transactions_with_sender =
411 block
412 .body
413 .get_transactions_with_sender(crypto)
414 .map_err(|error| {
415 EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
416 })?;
417
418 for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() {
419 if !is_amsterdam {
426 check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?;
427 }
428
429 if is_amsterdam {
431 check_2d_gas_allowance(
432 tx,
433 block_regular_gas_used,
434 block_state_gas_used,
435 block.header.gas_limit,
436 )?;
437 }
438
439 if is_amsterdam {
441 let bal_index = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
442 db.set_bal_index(bal_index);
443
444 if let Some(recorder) = db.bal_recorder_mut() {
448 recorder.record_touched_address(tx_sender);
449 }
450 }
451
452 let report = Self::execute_tx_in_block(
453 tx,
454 tx_sender,
455 &block.header,
456 db,
457 vm_type,
458 base_blob_fee_per_gas,
459 &mut shared_stack_pool,
460 &mut shared_memory_pool,
461 &mut shared_initcode_pool,
462 false,
463 crypto,
464 evm_config,
465 chain_id,
466 stateless_validator,
467 )?;
468
469 tx_gas_breakdowns.push(TxGasBreakdown::from_report(
470 tx_idx,
471 tx.hash(crypto),
472 &report,
473 ));
474
475 cumulative_gas_used += report.gas_spent;
477
478 let tx_state_gas = report.state_gas_used;
481 let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas);
482 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas);
483 block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas);
484
485 if is_amsterdam {
486 block_gas_used = block_regular_gas_used.max(block_state_gas_used);
488 ::tracing::debug!(
489 "EIP-8037 validate tx[{tx_idx}]: regular={tx_regular_gas} state={tx_state_gas} gas_used={} gas_spent={} block_regular={block_regular_gas_used} block_state={block_state_gas_used} block_max={block_gas_used}",
490 report.gas_used,
491 report.gas_spent,
492 );
493
494 if block_regular_gas_used > block.header.gas_limit
499 || block_state_gas_used > block.header.gas_limit
500 {
501 return Err(EvmError::Transaction(format!(
502 "Gas allowance exceeded: Block gas used overflow: \
503 block_gas_used {block_gas_used} > block_gas_limit {}",
504 block.header.gas_limit
505 )));
506 }
507 } else {
508 block_gas_used = block_gas_used.saturating_add(report.gas_used);
509 }
510
511 let mut receipt = Receipt::new(
512 tx.tx_type(),
513 matches!(report.result, TxResult::Success),
514 cumulative_gas_used,
515 report.logs,
516 );
517
518 if matches!(tx, Transaction::FrameTransaction(_)) {
520 receipt.payer = report.payer_address;
521 receipt.frame_receipts = frame_receipts_from(report.frame_results);
522 }
523
524 receipts.push(receipt);
525 }
526
527 if is_amsterdam && block_gas_used > block.header.gas_limit {
531 return Err(EvmError::Transaction(format!(
532 "Gas allowance exceeded: Block gas used overflow: \
533 block_gas_used {block_gas_used} > block_gas_limit {}",
534 block.header.gas_limit
535 )));
536 }
537
538 if is_amsterdam {
551 let post_tx_index =
552 u32::try_from(block.body.transactions.len() + 1).unwrap_or(u32::MAX);
553 db.set_bal_index(post_tx_index);
554
555 if let Some(withdrawals) = &block.body.withdrawals
559 && let Some(recorder) = db.bal_recorder_mut()
560 {
561 recorder.extend_touched_addresses(withdrawals.iter().map(|w| w.address));
562 }
563 }
564
565 if let Some(withdrawals) = &block.body.withdrawals {
566 Self::process_withdrawals(db, withdrawals)?;
567 }
568
569 let requests = match vm_type {
573 VMType::L1 => extract_all_requests_levm(&receipts, db, &block.header, vm_type, crypto)?,
574 VMType::L2(_) => Default::default(),
575 };
576
577 let bal = db.take_bal();
579
580 Ok((
581 BlockExecutionResult {
582 receipts,
583 requests,
584 block_gas_used,
585 burned_fees: is_lstar
587 .then(|| lstar_burned_fees(&chain_config, &block.header, cumulative_gas_used)),
588 tx_gas_breakdowns,
589 },
590 bal,
591 ))
592 }
593
594 #[allow(clippy::too_many_arguments)]
598 pub fn execute_block_pipeline(
599 block: &Block,
600 db: &mut GeneralizedDatabase,
601 vm_type: VMType,
602 merkleizer: Option<Sender<Vec<AccountUpdate>>>,
603 queue_length: &AtomicUsize,
604 crypto: &dyn Crypto,
605 header_bal: Option<Arc<BlockAccessList>>,
606 bal_parallel_exec_enabled: bool,
607 stateless_validator: Option<&dyn StatelessValidator>,
608 ) -> Result<(BlockExecutionResult, Option<BlockAccessList>), EvmError> {
609 let chain_config = db.store.get_chain_config()?;
610 let is_amsterdam = chain_config.is_amsterdam_activated(block.header.timestamp);
611 let is_lstar = chain_config.is_lstar_activated(block.header.timestamp);
612 let evm_config = EVMConfig::new_from_chain_config(&chain_config, &block.header);
615 let chain_id = chain_config.chain_id;
616
617 debug_assert!(
619 block.body.transactions.len() < u32::MAX as usize,
620 "tx count overflows u32 BlockAccessIndex"
621 );
622
623 let transactions_with_sender =
624 block
625 .body
626 .get_transactions_with_sender(crypto)
627 .map_err(|error| {
628 EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
629 })?;
630
631 #[cfg(not(feature = "rayon"))]
632 let _ = (header_bal, bal_parallel_exec_enabled);
635 #[cfg(feature = "rayon")]
636 if let Some(bal) = header_bal
642 && is_amsterdam
643 && bal_parallel_exec_enabled
644 {
645 validate_header_bal_indices(&bal, block.body.transactions.len())
651 .map_err(|e| EvmError::Custom(e.to_string()))?;
652 validate_bal_code_sizes(&bal, AMSTERDAM_MAX_CODE_SIZE)
655 .map_err(|e| EvmError::Custom(format!("BAL validation failed: {e}")))?;
656
657 db.enable_bal_recording();
660 db.set_bal_index(PRE_BLOCK_BAL_INDEX);
661 let prepare_result = Self::prepare_block(block, db, vm_type, crypto);
662 let pre_block_bal = db.take_bal().unwrap_or_default();
663 prepare_result?;
664
665 let validation_index = Arc::new(bal.build_validation_index());
667
668 Self::validate_bal_pre_exec_index(db, &bal, &validation_index)?;
675
676 LEVM::get_state_transitions_tx(db)?;
678 let system_seed = Arc::new(std::mem::take(&mut db.initial_accounts_state));
679
680 let parallel_result = Self::execute_block_parallel(
681 block,
682 &transactions_with_sender,
683 db,
684 vm_type,
685 Arc::clone(&bal),
686 merkleizer.as_ref(),
687 queue_length,
688 system_seed,
689 &pre_block_bal,
690 crypto,
691 Arc::clone(&validation_index),
692 stateless_validator,
693 );
694
695 let (
700 receipts,
701 block_gas_used,
702 mut unread_storage_reads,
703 mut unaccessed_pure_accounts,
704 tx_gas_breakdowns,
705 ) = match parallel_result {
706 Ok(result) => result,
707 Err(parallel_err) => {
708 let last_tx_idx =
709 u32::try_from(block.body.transactions.len()).unwrap_or(u32::MAX);
710 if Self::seed_db_from_bal(
711 db,
712 &bal,
713 last_tx_idx,
714 &validation_index.accounts_by_min_index,
715 )
716 .is_ok()
717 && let VMType::L1 = vm_type
718 && let Err(e @ EvmError::SystemContractCallFailed(_)) =
719 extract_all_requests_levm(&[], db, &block.header, vm_type, crypto)
720 {
721 return Err(e);
722 }
723 return Err(parallel_err);
724 }
725 };
726
727 let last_tx_idx = u32::try_from(block.body.transactions.len()).unwrap_or(u32::MAX);
732 Self::seed_db_from_bal(
734 db,
735 &bal,
736 last_tx_idx,
737 &validation_index.accounts_by_min_index,
738 )?;
739
740 let withdrawal_bal_idx = u32::try_from(block.body.transactions.len())
741 .map(|n| n.saturating_add(1))
742 .unwrap_or(u32::MAX);
743 db.enable_bal_recording();
744 db.set_bal_index(withdrawal_bal_idx);
745
746 let post_block_result = (|| -> Result<Vec<Requests>, EvmError> {
751 if let Some(withdrawals) = &block.body.withdrawals {
752 Self::process_withdrawals(db, withdrawals)?;
753 }
754
755 let requests = match vm_type {
756 VMType::L1 => {
757 extract_all_requests_levm(&receipts, db, &block.header, vm_type, crypto)?
758 }
759 VMType::L2(_) => Default::default(),
760 };
761 Ok(requests)
762 })();
763 let post_block_bal = db.take_bal().unwrap_or_default();
764 let requests = post_block_result?;
765 let withdrawal_idx = u32::try_from(block.body.transactions.len())
773 .map(|n| n.saturating_add(1))
774 .unwrap_or(u32::MAX);
775 Self::validate_bal_withdrawal_index(db, &bal, withdrawal_idx, &validation_index)?;
776
777 if !unread_storage_reads.is_empty() {
779 for (addr, acct) in &db.current_accounts_state {
780 for key in acct.storage.keys() {
781 unread_storage_reads.remove(&(*addr, *key));
782 }
783 }
784 }
785
786 if !unaccessed_pure_accounts.is_empty() {
791 if let Some(withdrawals) = &block.body.withdrawals {
792 for w in withdrawals {
793 unaccessed_pure_accounts.remove(&w.address);
794 }
795 }
796 for canon in post_block_bal.accounts() {
797 if canon.address == SYSTEM_ADDRESS {
799 continue;
800 }
801 unaccessed_pure_accounts.remove(&canon.address);
802 }
803 }
804
805 Self::validate_bal_covers(
806 &db.current_accounts_state,
807 &post_block_bal,
808 &bal,
809 &validation_index,
810 "the withdrawal/request phase",
811 withdrawal_bal_idx,
812 )?;
813
814 if let Some(withdrawals) = &block.body.withdrawals {
817 for w in withdrawals {
818 if !validation_index.addr_to_idx.contains_key(&w.address) {
819 return Err(EvmError::Custom(format!(
820 "BAL validation failed: withdrawal recipient {:?} is missing \
821 from BAL",
822 w.address
823 )));
824 }
825 }
826 }
827
828 if let Some((addr, key)) = unread_storage_reads.iter().min() {
832 let slot = ethrex_common::BigEndianHash::into_uint(key);
833 return Err(EvmError::Custom(format!(
834 "BAL validation failed: storage_read for account {addr:?} slot \
835 {slot} was never actually read during block execution"
836 )));
837 }
838
839 if let Some(addr) = unaccessed_pure_accounts.iter().min() {
842 return Err(EvmError::Custom(format!(
843 "BAL validation failed: account {addr:?} has no mutations \
844 and no storage reads but was never accessed during block execution"
845 )));
846 }
847
848 bal.validate_ordering()
851 .map_err(|e| EvmError::Custom(format!("BAL validation failed: {e}")))?;
852
853 validate_block_access_list_size(&block.header, &chain_config, &bal)
856 .map_err(|e| EvmError::Custom(e.to_string()))?;
857
858 let post_refund_gas = receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
862 let burned_fees_par =
863 is_lstar.then(|| lstar_burned_fees(&chain_config, &block.header, post_refund_gas));
864
865 return Ok((
866 BlockExecutionResult {
867 receipts,
868 requests,
869 block_gas_used,
870 burned_fees: burned_fees_par,
871 tx_gas_breakdowns,
872 },
873 None,
874 ));
875 }
876
877 let Some(merkleizer) = merkleizer else {
883 return Err(EvmError::Custom(
884 "sequential execution path called without a merkleizer Sender".to_string(),
885 ));
886 };
887 if is_amsterdam {
888 db.enable_bal_recording();
889 db.set_bal_index(0);
891 }
892
893 Self::prepare_block(block, db, vm_type, crypto)?;
894
895 let base_blob_fee_per_gas =
897 get_base_fee_per_blob_gas(block.header.excess_blob_gas, &evm_config)?;
898
899 let mut shared_stack_pool = Vec::with_capacity(STACK_LIMIT);
900 let mut shared_memory_pool = Vec::with_capacity(1);
902 let mut shared_initcode_pool = Vec::new();
904
905 let n_txs = block.body.transactions.len();
906 let mut receipts = Vec::with_capacity(n_txs);
907 let mut tx_gas_breakdowns: Vec<TxGasBreakdown> = Vec::with_capacity(n_txs);
908 let mut cumulative_gas_used = 0_u64;
910 let mut block_gas_used = 0_u64;
912 let mut block_regular_gas_used = 0_u64;
914 let mut block_state_gas_used = 0_u64;
915 let mut tx_since_last_flush = 2;
918
919 for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() {
920 if !is_amsterdam {
927 check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?;
928 }
929
930 if is_amsterdam {
932 check_2d_gas_allowance(
933 tx,
934 block_regular_gas_used,
935 block_state_gas_used,
936 block.header.gas_limit,
937 )?;
938 }
939
940 if is_amsterdam {
942 let bal_index = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
943 db.set_bal_index(bal_index);
944
945 if let Some(recorder) = db.bal_recorder_mut() {
949 recorder.record_touched_address(tx_sender);
950 }
951 }
952
953 let report = Self::execute_tx_in_block(
954 tx,
955 tx_sender,
956 &block.header,
957 db,
958 vm_type,
959 base_blob_fee_per_gas,
960 &mut shared_stack_pool,
961 &mut shared_memory_pool,
962 &mut shared_initcode_pool,
963 false,
964 crypto,
965 evm_config,
966 chain_id,
967 stateless_validator,
968 )?;
969
970 tx_gas_breakdowns.push(TxGasBreakdown::from_report(
971 tx_idx,
972 tx.hash(crypto),
973 &report,
974 ));
975
976 if queue_length.load(Ordering::Relaxed) == 0 && tx_since_last_flush > 5 {
977 LEVM::send_state_transitions_tx(&merkleizer, db, queue_length)?;
978 tx_since_last_flush = 0;
979 } else {
980 tx_since_last_flush += 1;
981 }
982
983 cumulative_gas_used += report.gas_spent;
985
986 let tx_state_gas = report.state_gas_used;
989 let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas);
990 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas);
991 block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas);
992
993 if is_amsterdam {
994 block_gas_used = block_regular_gas_used.max(block_state_gas_used);
996
997 if block_regular_gas_used > block.header.gas_limit
1002 || block_state_gas_used > block.header.gas_limit
1003 {
1004 return Err(EvmError::Transaction(format!(
1005 "Gas allowance exceeded: Block gas used overflow: \
1006 block_gas_used {block_gas_used} > block_gas_limit {}",
1007 block.header.gas_limit
1008 )));
1009 }
1010 } else {
1011 block_gas_used = block_gas_used.saturating_add(report.gas_used);
1012 }
1013
1014 let mut receipt = Receipt::new(
1015 tx.tx_type(),
1016 matches!(report.result, TxResult::Success),
1017 cumulative_gas_used,
1018 report.logs,
1019 );
1020
1021 if matches!(tx, Transaction::FrameTransaction(_)) {
1023 receipt.payer = report.payer_address;
1024 receipt.frame_receipts = frame_receipts_from(report.frame_results);
1025 }
1026
1027 receipts.push(receipt);
1028 }
1029
1030 if is_amsterdam && block_gas_used > block.header.gas_limit {
1034 return Err(EvmError::Transaction(format!(
1035 "Gas allowance exceeded: Block gas used overflow: \
1036 block_gas_used {block_gas_used} > block_gas_limit {}",
1037 block.header.gas_limit
1038 )));
1039 }
1040
1041 #[cfg(feature = "perf_opcode_timings")]
1042 {
1043 let mut timings = OPCODE_TIMINGS.lock().expect("poison");
1044 timings.inc_tx_count(receipts.len());
1045 timings.inc_block_count();
1046 ::tracing::info!("{}", timings.info_pretty());
1047 let precompiles_timings = PRECOMPILES_TIMINGS.lock().expect("poison");
1048 ::tracing::info!("{}", precompiles_timings.info_pretty());
1049 }
1050
1051 if queue_length.load(Ordering::Relaxed) == 0 {
1052 LEVM::send_state_transitions_tx(&merkleizer, db, queue_length)?;
1053 }
1054
1055 if is_amsterdam {
1058 let post_tx_index =
1059 u32::try_from(block.body.transactions.len() + 1).unwrap_or(u32::MAX);
1060 db.set_bal_index(post_tx_index);
1061
1062 if let Some(withdrawals) = &block.body.withdrawals
1064 && let Some(recorder) = db.bal_recorder_mut()
1065 {
1066 recorder.extend_touched_addresses(withdrawals.iter().map(|w| w.address));
1067 }
1068 }
1069
1070 if let Some(withdrawals) = &block.body.withdrawals {
1073 Self::process_withdrawals(db, withdrawals)?;
1074 }
1075
1076 let requests = match vm_type {
1080 VMType::L1 => extract_all_requests_levm(&receipts, db, &block.header, vm_type, crypto)?,
1081 VMType::L2(_) => Default::default(),
1082 };
1083 LEVM::send_state_transitions_tx(&merkleizer, db, queue_length)?;
1084
1085 let bal = db.take_bal();
1087
1088 Ok((
1089 BlockExecutionResult {
1090 receipts,
1091 requests,
1092 block_gas_used,
1093 burned_fees: is_lstar
1095 .then(|| lstar_burned_fees(&chain_config, &block.header, cumulative_gas_used)),
1096 tx_gas_breakdowns,
1097 },
1098 bal,
1099 ))
1100 }
1101
1102 #[cfg(feature = "rayon")]
1107 fn bal_to_account_updates(
1108 bal: &BlockAccessList,
1109 store: &dyn Database,
1110 ) -> Result<Vec<AccountUpdate>, EvmError> {
1111 use ethrex_common::types::AccountInfo;
1112
1113 let mut updates = Vec::new();
1114
1115 let write_addrs: Vec<Address> = bal
1117 .accounts()
1118 .iter()
1119 .filter(|ac| {
1120 !ac.balance_changes.is_empty()
1121 || !ac.nonce_changes.is_empty()
1122 || !ac.code_changes.is_empty()
1123 || !ac.storage_changes.is_empty()
1124 })
1125 .map(|ac| ac.address)
1126 .collect();
1127 let _ = store.prefetch_accounts(&write_addrs);
1131
1132 for acct_changes in bal.accounts() {
1133 let addr = acct_changes.address;
1134
1135 let has_writes = !acct_changes.balance_changes.is_empty()
1137 || !acct_changes.nonce_changes.is_empty()
1138 || !acct_changes.code_changes.is_empty()
1139 || !acct_changes.storage_changes.is_empty();
1140 if !has_writes {
1141 continue;
1142 }
1143
1144 let prestate = store
1146 .get_account_state(addr)
1147 .map_err(|e| EvmError::Custom(format!("bal_to_account_updates: {e}")))?;
1148
1149 let balance = acct_changes
1151 .balance_changes
1152 .last()
1153 .map(|c| c.post_balance)
1154 .unwrap_or(prestate.balance);
1155
1156 let nonce = acct_changes
1158 .nonce_changes
1159 .last()
1160 .map(|c| c.post_nonce)
1161 .unwrap_or(prestate.nonce);
1162
1163 let (code_hash, code) = if let Some(c) = acct_changes.code_changes.last() {
1165 code_from_bal(&c.new_code)
1166 } else {
1167 (prestate.code_hash, None)
1168 };
1169
1170 let mut added_storage = SeededHashMap::with_capacity_and_hasher(
1172 acct_changes.storage_changes.len(),
1173 Default::default(),
1174 );
1175 for slot_change in &acct_changes.storage_changes {
1176 if let Some(last) = slot_change.slot_changes.last() {
1177 let key = ethrex_common::utils::u256_to_h256(slot_change.slot);
1178 added_storage.insert(key, last.post_value);
1179 }
1180 }
1181
1182 let post_empty = balance.is_zero() && nonce == 0 && code_hash == *EMPTY_KECCAK_HASH;
1184 let pre_empty = prestate.balance.is_zero()
1185 && prestate.nonce == 0
1186 && prestate.code_hash == *EMPTY_KECCAK_HASH;
1187 let removed = post_empty && !pre_empty;
1188
1189 let balance_changed = acct_changes
1190 .balance_changes
1191 .last()
1192 .is_some_and(|c| c.post_balance != prestate.balance);
1193 let nonce_changed = acct_changes
1194 .nonce_changes
1195 .last()
1196 .is_some_and(|c| c.post_nonce != prestate.nonce);
1197 let code_changed = acct_changes.code_changes.last().is_some();
1198 let acc_info_updated = balance_changed || nonce_changed || code_changed;
1199
1200 if !removed && !acc_info_updated && added_storage.is_empty() {
1201 continue;
1202 }
1203
1204 let info = if acc_info_updated {
1205 Some(AccountInfo {
1206 code_hash,
1207 balance,
1208 nonce,
1209 })
1210 } else {
1211 None
1212 };
1213
1214 let update = AccountUpdate {
1215 address: addr,
1216 removed,
1217 info,
1218 code,
1219 added_storage,
1220 removed_storage: false,
1227 };
1228 updates.push(update);
1229 }
1230
1231 Ok(updates)
1232 }
1233
1234 #[cfg(feature = "rayon")]
1240 fn validate_bal_covers(
1241 cache: &CacheDB,
1242 phase_bal: &BlockAccessList,
1243 bal: &BlockAccessList,
1244 index: &BalAddressIndex,
1245 phase: &str,
1246 phase_idx: u32,
1247 ) -> Result<(), EvmError> {
1248 for canon in phase_bal.accounts() {
1251 let addr = canon.address;
1252 let Some(supplied) = index
1253 .addr_to_idx
1254 .get(&addr)
1255 .and_then(|i| bal.accounts().get(*i))
1256 else {
1257 return Err(EvmError::Custom(format!(
1258 "BAL validation failed: account {addr:?} was accessed during {phase} \
1259 but is missing from BAL"
1260 )));
1261 };
1262
1263 for canon_slot in &canon.storage_changes {
1265 let slot = canon_slot.slot;
1266 let supplied_changes = supplied
1267 .storage_changes
1268 .binary_search_by(|sc| sc.slot.cmp(&slot))
1269 .ok()
1270 .and_then(|i| supplied.storage_changes.get(i))
1271 .map(|sc| sc.slot_changes.as_slice())
1272 .unwrap_or_default();
1273 for canon_change in &canon_slot.slot_changes {
1274 let idx = canon_change.block_access_index;
1275 match find_exact_change_storage(supplied_changes, idx) {
1276 Some(v) if v == canon_change.post_value => {}
1277 Some(v) => {
1278 return Err(EvmError::Custom(format!(
1279 "BAL validation failed: account {addr:?} storage slot {slot} \
1280 was written during {phase} to {} but the BAL declares {v} \
1281 at index {idx}",
1282 canon_change.post_value
1283 )));
1284 }
1285 None => {
1286 return Err(EvmError::Custom(format!(
1287 "BAL validation failed: account {addr:?} storage slot {slot} \
1288 was written during {phase} to {} but the BAL declares no \
1289 change at index {idx}",
1290 canon_change.post_value
1291 )));
1292 }
1293 }
1294 }
1295 }
1296
1297 for canon_change in &canon.balance_changes {
1298 let idx = canon_change.block_access_index;
1299 let expected = canon_change.post_balance;
1300 if find_exact_change_balance(&supplied.balance_changes, idx) != Some(expected) {
1301 return Err(EvmError::Custom(format!(
1302 "BAL validation failed: account {addr:?} balance changed to {expected} \
1303 during {phase} but the BAL declares no matching change at index {idx}"
1304 )));
1305 }
1306 }
1307 for canon_change in &canon.nonce_changes {
1308 let idx = canon_change.block_access_index;
1309 let expected = canon_change.post_nonce;
1310 if find_exact_change_nonce(&supplied.nonce_changes, idx) != Some(expected) {
1311 return Err(EvmError::Custom(format!(
1312 "BAL validation failed: account {addr:?} nonce changed to {expected} \
1313 during {phase} but the BAL declares no matching change at index {idx}"
1314 )));
1315 }
1316 }
1317 for canon_change in &canon.code_changes {
1318 let idx = canon_change.block_access_index;
1319 if find_exact_change_code(&supplied.code_changes, idx)
1320 != Some(&canon_change.new_code)
1321 {
1322 return Err(EvmError::Custom(format!(
1323 "BAL validation failed: account {addr:?} code changed during {phase} \
1324 but the BAL declares no matching change at index {idx}"
1325 )));
1326 }
1327 }
1328
1329 for &slot in &canon.storage_reads {
1331 if supplied.storage_reads.binary_search(&slot).is_err()
1332 && supplied
1333 .storage_changes
1334 .binary_search_by(|sc| sc.slot.cmp(&slot))
1335 .is_err()
1336 {
1337 return Err(EvmError::Custom(format!(
1338 "BAL validation failed: storage slot {slot} of account {addr:?} was \
1339 read during {phase} but is missing from BAL"
1340 )));
1341 }
1342 }
1343 }
1344
1345 let canonical: SeededHashMap<Address, &AccountChanges> = phase_bal
1348 .accounts()
1349 .iter()
1350 .map(|a| (a.address, a))
1351 .collect();
1352 for supplied in bal.accounts() {
1353 let addr = supplied.address;
1354 let canon = canonical.get(&addr).copied();
1355 for sc in &supplied.storage_changes {
1356 if find_exact_change_storage(&sc.slot_changes, phase_idx).is_none() {
1357 continue;
1358 }
1359 let declared_canonically = canon.is_some_and(|c| {
1360 c.storage_changes
1361 .binary_search_by(|cs| cs.slot.cmp(&sc.slot))
1362 .ok()
1363 .and_then(|i| c.storage_changes.get(i))
1364 .is_some_and(|cs| {
1365 find_exact_change_storage(&cs.slot_changes, phase_idx).is_some()
1366 })
1367 });
1368 if !declared_canonically {
1369 return Err(EvmError::Custom(format!(
1370 "BAL validation failed: account {addr:?} declares a storage change \
1371 for slot {} at index {phase_idx} but {phase} did not write it",
1372 sc.slot
1373 )));
1374 }
1375 }
1376 if find_exact_change_balance(&supplied.balance_changes, phase_idx).is_some()
1377 && canon.is_none_or(|c| {
1378 find_exact_change_balance(&c.balance_changes, phase_idx).is_none()
1379 })
1380 {
1381 return Err(EvmError::Custom(format!(
1382 "BAL validation failed: account {addr:?} declares a balance change at \
1383 index {phase_idx} but {phase} did not change its balance"
1384 )));
1385 }
1386 if find_exact_change_nonce(&supplied.nonce_changes, phase_idx).is_some()
1387 && canon
1388 .is_none_or(|c| find_exact_change_nonce(&c.nonce_changes, phase_idx).is_none())
1389 {
1390 return Err(EvmError::Custom(format!(
1391 "BAL validation failed: account {addr:?} declares a nonce change at \
1392 index {phase_idx} but {phase} did not change its nonce"
1393 )));
1394 }
1395 if find_exact_change_code(&supplied.code_changes, phase_idx).is_some()
1396 && canon
1397 .is_none_or(|c| find_exact_change_code(&c.code_changes, phase_idx).is_none())
1398 {
1399 return Err(EvmError::Custom(format!(
1400 "BAL validation failed: account {addr:?} declares a code change at \
1401 index {phase_idx} but {phase} did not change its code"
1402 )));
1403 }
1404 }
1405
1406 first_failure_by_key(cache, |addr, acct| {
1409 if *addr == SYSTEM_ADDRESS {
1410 return Ok(());
1411 }
1412 let supplied = match index
1413 .addr_to_idx
1414 .get(addr)
1415 .and_then(|i| bal.accounts().get(*i))
1416 {
1417 Some(a) => a,
1418 None if !acct.storage.is_empty() => {
1420 return Err(EvmError::Custom(format!(
1421 "BAL validation failed: account {addr:?} was accessed during {phase} \
1422 but is missing from BAL"
1423 )));
1424 }
1425 None => return Ok(()),
1426 };
1427 debug_assert!(
1430 supplied.storage_reads.windows(2).all(|w| w[0] < w[1]),
1431 "storage_reads must be strictly ascending for binary_search"
1432 );
1433 first_failure_by_key(&acct.storage, |key, _| {
1434 let slot = ethrex_common::BigEndianHash::into_uint(key);
1435 if supplied.storage_reads.binary_search(&slot).is_err()
1436 && supplied
1437 .storage_changes
1438 .binary_search_by(|sc| sc.slot.cmp(&slot))
1439 .is_err()
1440 {
1441 return Err(EvmError::Custom(format!(
1442 "BAL validation failed: storage slot {slot} of account {addr:?} was \
1443 read during {phase} but is missing from BAL"
1444 )));
1445 }
1446 Ok(())
1447 })
1448 })
1449 }
1450
1451 #[cfg(feature = "rayon")]
1468 fn seed_db_from_bal(
1469 db: &mut GeneralizedDatabase,
1470 bal: &BlockAccessList,
1471 max_idx: u32,
1472 accounts_by_min_index: &[(u32, usize)],
1473 ) -> Result<(), EvmError> {
1474 let end = accounts_by_min_index.partition_point(|(min_idx, _)| *min_idx <= max_idx);
1475 let bal_accounts = bal.accounts();
1476 for &(_, acct_idx) in &accounts_by_min_index[..end] {
1477 seed_one_address_info_from_bal(db, bal, acct_idx, max_idx)
1478 .map_err(|e| EvmError::Custom(format!("seed_db_from_bal: {e}")))?;
1479
1480 let acct_changes = &bal_accounts[acct_idx];
1481 if acct_changes.storage_changes.is_empty() {
1482 continue;
1483 }
1484 let any_storage = acct_changes.storage_changes.iter().any(|sc| {
1485 sc.slot_changes
1486 .first()
1487 .is_some_and(|c| c.block_access_index <= max_idx)
1488 });
1489 if !any_storage {
1490 continue;
1491 }
1492 let addr = acct_changes.address;
1493 if !db.current_accounts_state.contains_key(&addr) {
1494 db.get_account(addr)
1495 .map_err(|e| EvmError::Custom(format!("seed storage: {e}")))?;
1496 }
1497 let acc = db
1498 .get_account_mut(addr)
1499 .map_err(|e| EvmError::Custom(format!("seed storage mut: {e}")))?;
1500 for sc in &acct_changes.storage_changes {
1501 if let Some(value) = post_value_at_or_before(sc, max_idx) {
1502 acc.storage
1503 .insert(ethrex_common::utils::u256_to_h256(sc.slot), value);
1504 }
1505 }
1506 }
1507 Ok(())
1508 }
1509
1510 #[cfg(feature = "rayon")]
1517 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1518 fn execute_block_parallel(
1519 block: &Block,
1520 txs_with_sender: &[(&Transaction, Address)],
1521 db: &mut GeneralizedDatabase,
1522 vm_type: VMType,
1523 bal: Arc<BlockAccessList>,
1524 merkleizer: Option<&Sender<Vec<AccountUpdate>>>,
1525 queue_length: &AtomicUsize,
1526 system_seed: Arc<CacheDB>,
1527 pre_block_bal: &BlockAccessList,
1528 crypto: &dyn Crypto,
1529 validation_index: Arc<BalAddressIndex>,
1530 stateless_validator: Option<&dyn StatelessValidator>,
1531 ) -> Result<
1532 (
1533 Vec<Receipt>,
1534 u64,
1535 SeededHashSet<(Address, H256)>,
1536 SeededHashSet<Address>,
1537 Vec<TxGasBreakdown>,
1538 ),
1539 EvmError,
1540 > {
1541 let store = db.store.clone();
1542 let header = &block.header;
1543 let n_txs = txs_with_sender.len();
1544 let chain_config = store.get_chain_config()?;
1549 let is_amsterdam = chain_config.is_amsterdam_activated(header.timestamp);
1550 let evm_config = EVMConfig::new_from_chain_config(&chain_config, header);
1553 let chain_id = chain_config.chain_id;
1554 let base_blob_fee_per_gas = get_base_fee_per_blob_gas(header.excess_blob_gas, &evm_config)?;
1556 debug_assert!(
1557 is_amsterdam,
1558 "execute_block_parallel invoked on non-Amsterdam block"
1559 );
1560
1561 if let Some(merkleizer) = merkleizer {
1565 let account_updates = Self::bal_to_account_updates(&bal, store.as_ref())?;
1566 merkleizer
1567 .send(account_updates)
1568 .map_err(|e| EvmError::Custom(format!("merkleizer send failed: {e}")))?;
1569 queue_length.fetch_add(1, Ordering::Relaxed);
1570 }
1571
1572 let mut unread_storage_reads: SeededHashSet<(Address, H256)> = Default::default();
1575 let mut unaccessed_pure_accounts: SeededHashSet<Address> = Default::default();
1578 for acct in bal.accounts() {
1579 for &slot in &acct.storage_reads {
1580 let key = ethrex_common::utils::u256_to_h256(slot);
1581 unread_storage_reads.insert((acct.address, key));
1582 }
1583 let is_pure = acct.storage_changes.is_empty()
1584 && acct.storage_reads.is_empty()
1585 && acct.balance_changes.is_empty()
1586 && acct.nonce_changes.is_empty()
1587 && acct.code_changes.is_empty();
1588 if is_pure {
1589 unaccessed_pure_accounts.insert(acct.address);
1590 }
1591 }
1592
1593 for canon in pre_block_bal.accounts() {
1600 if canon.address == SYSTEM_ADDRESS {
1601 continue;
1602 }
1603 unaccessed_pure_accounts.remove(&canon.address);
1604 }
1605
1606 unread_storage_reads.retain(|(addr, key)| {
1608 !system_seed
1609 .get(addr)
1610 .is_some_and(|a| a.storage.contains_key(key))
1611 });
1612
1613 let arc_bal = bal;
1615 let arc_idx = validation_index;
1616
1617 type TxExecResult = (
1631 usize,
1632 TxType,
1633 ExecutionReport,
1634 SeededHashSet<Address>, Vec<(Address, H256)>, Vec<Address>, Option<EvmError>, );
1639
1640 check_minimum_block_work(
1648 txs_with_sender.iter().map(|(tx, sender)| (*tx, *sender)),
1649 chain_config.fork(header.timestamp),
1650 header.gas_limit,
1651 )?;
1652
1653 let completed_gas = CompletedGas::default();
1656
1657 let exec_results: Result<Vec<TxExecResult>, EvmError> = (0..n_txs)
1658 .into_par_iter()
1659 .map(|tx_idx| -> Result<_, EvmError> {
1660 completed_gas.check(header.gas_limit)?;
1661 let (tx, sender) = &txs_with_sender[tx_idx];
1662 let mut tx_db = GeneralizedDatabase::new_with_shared_base_and_capacity(
1664 store.clone(),
1665 system_seed.clone(),
1666 32,
1667 );
1668 tx_db.lazy_bal = Some(LazyBalCursor {
1669 bal: arc_bal.clone(),
1670 bal_index: u32::try_from(tx_idx + 1).unwrap_or(u32::MAX),
1671 index: arc_idx.clone(),
1672 });
1673 let mut stack_pool = Vec::with_capacity(8);
1676 let mut memory_pool = Vec::with_capacity(1);
1678 let mut initcode_pool = Vec::new();
1680
1681 tx_db.accessed_accounts = Some(SeededHashSet::with_capacity_and_hasher(
1685 16,
1686 Default::default(),
1687 ));
1688
1689 tx_db.enable_bal_recording();
1694 let bal_index = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
1695 tx_db.set_bal_index(bal_index);
1696 if let Some(recorder) = tx_db.bal_recorder_mut() {
1700 recorder.record_touched_address(*sender);
1701 }
1702
1703 let report = LEVM::execute_tx_in_block(
1704 tx,
1705 *sender,
1706 header,
1707 &mut tx_db,
1708 vm_type,
1709 base_blob_fee_per_gas,
1710 &mut stack_pool,
1711 &mut memory_pool,
1712 &mut initcode_pool,
1713 false,
1714 crypto,
1715 evm_config,
1716 chain_id,
1717 stateless_validator,
1718 )?;
1719 let mut report = report;
1720
1721 report.output = Bytes::new();
1725
1726 completed_gas.record(
1728 report.gas_used.saturating_sub(report.state_gas_used),
1729 report.state_gas_used,
1730 header.gas_limit,
1731 )?;
1732
1733 let current_state = std::mem::take(&mut tx_db.current_accounts_state);
1734 let codes = std::mem::take(&mut tx_db.codes);
1735 let mut tracked = tx_db.accessed_accounts.take().unwrap_or_default();
1736 let (shadow_touched, shadow_reads, tx_initial) = tx_db
1743 .bal_recorder
1744 .take()
1745 .map(|mut r| {
1746 (
1747 r.take_touched_addresses(),
1748 r.take_storage_reads(),
1749 r.take_tx_initial_storage(),
1750 )
1751 })
1752 .unwrap_or_default();
1753 let pure_reads: SeededHashSet<(Address, U256)> = shadow_reads
1762 .iter()
1763 .copied()
1764 .filter(|k| !tx_initial.contains_key(k))
1765 .collect();
1766
1767 let mut reads_satisfied: Vec<(Address, H256)> =
1780 Vec::with_capacity(current_state.len() * 4);
1781 let mut destroyed: Vec<Address> = Vec::new();
1785 for (addr, acct) in ¤t_state {
1786 if matches!(
1787 acct.status,
1788 AccountStatus::Destroyed | AccountStatus::DestroyedModified
1789 ) {
1790 destroyed.push(*addr);
1791 } else {
1792 for key in acct.storage.keys() {
1793 reads_satisfied.push((*addr, *key));
1794 }
1795 }
1796 }
1797 for (addr, slot) in &shadow_reads {
1805 if destroyed.contains(addr) {
1806 reads_satisfied.push((*addr, ethrex_common::utils::u256_to_h256(*slot)));
1807 }
1808 }
1809
1810 let deferred_bal_err: Option<EvmError> = (|| -> Result<(), EvmError> {
1816 let bal_idx = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
1817 let seed_idx = u32::try_from(tx_idx).unwrap_or(u32::MAX);
1818 Self::validate_tx_execution(
1819 bal_idx,
1820 seed_idx,
1821 ¤t_state,
1822 &codes,
1823 &arc_bal,
1824 &arc_idx,
1825 &system_seed,
1826 &store,
1827 &pure_reads,
1828 &tx_initial,
1829 )
1830 .map_err(|e| match e {
1831 BalValidationError::Database(msg) => {
1835 EvmError::DB(format!("BAL validation for tx {tx_idx}: {msg}"))
1836 }
1837 BalValidationError::Mismatch(msg) => EvmError::Custom(format!(
1838 "BAL validation failed for tx {tx_idx}: {msg}"
1839 )),
1840 })?;
1841
1842 for addr in &shadow_touched {
1844 if !arc_idx.addr_to_idx.contains_key(addr) {
1845 return Err(EvmError::Custom(format!(
1846 "BAL validation failed for tx {tx_idx}: account {addr:?} was \
1847 accessed during execution but is missing from BAL"
1848 )));
1849 }
1850 }
1851 for (addr, slot) in &shadow_reads {
1852 let Some(&bal_acct_idx) = arc_idx.addr_to_idx.get(addr) else {
1853 continue;
1855 };
1856 let acct = &arc_bal.accounts()[bal_acct_idx];
1857 let in_changes = acct
1858 .storage_changes
1859 .binary_search_by(|sc| sc.slot.cmp(slot))
1860 .is_ok();
1861 debug_assert!(
1868 acct.storage_reads.windows(2).all(|w| w[0] < w[1]),
1869 "storage_reads must be strictly ascending for binary_search"
1870 );
1871 let in_reads = acct.storage_reads.binary_search(slot).is_ok();
1872 if !in_changes && !in_reads {
1873 return Err(EvmError::Custom(format!(
1874 "BAL validation failed for tx {tx_idx}: storage slot {slot} of \
1875 account {addr:?} was read during execution but is missing from \
1876 BAL (no storage_changes or storage_reads entry)"
1877 )));
1878 }
1879 }
1880 Ok(())
1881 })()
1882 .err();
1883
1884 drop(current_state);
1885 drop(codes);
1886
1887 tracked.extend(shadow_touched.iter().copied());
1896
1897 Ok((
1898 tx_idx,
1899 tx.tx_type(),
1900 report,
1901 tracked,
1902 reads_satisfied,
1903 destroyed,
1904 deferred_bal_err,
1905 ))
1906 })
1907 .collect();
1908
1909 let mut exec_results = exec_results?;
1910
1911 exec_results.sort_unstable_by_key(|(idx, _, _, _, _, _, _)| *idx);
1918
1919 let mut block_regular_gas_used = 0_u64;
1924 let mut block_state_gas_used = 0_u64;
1925 let mut tx_gas_breakdowns: Vec<TxGasBreakdown> = Vec::with_capacity(exec_results.len());
1926 for (tx_idx, _, report, _, _, _, _) in &exec_results {
1927 let (tx, _tx_sender) = txs_with_sender
1928 .get(*tx_idx)
1929 .ok_or_else(|| EvmError::Custom(format!("tx index {tx_idx} out of bounds")))?;
1930 if is_amsterdam {
1931 check_2d_gas_allowance(
1932 tx,
1933 block_regular_gas_used,
1934 block_state_gas_used,
1935 header.gas_limit,
1936 )?;
1937 }
1938
1939 tx_gas_breakdowns.push(TxGasBreakdown::from_report(
1940 *tx_idx,
1941 tx.hash(crypto),
1942 report,
1943 ));
1944
1945 let tx_state_gas = report.state_gas_used;
1946 let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas);
1947 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas);
1948 block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas);
1949 }
1950 let block_gas_used = block_regular_gas_used.max(block_state_gas_used);
1951 if block_gas_used > header.gas_limit {
1953 return Err(EvmError::Transaction(format!(
1954 "Gas allowance exceeded: Block gas used overflow: \
1955 block_gas_used {block_gas_used} > block_gas_limit {}",
1956 header.gas_limit
1957 )));
1958 }
1959
1960 Self::validate_bal_covers(
1963 &system_seed,
1964 pre_block_bal,
1965 &arc_bal,
1966 &arc_idx,
1967 "system calls",
1968 PRE_BLOCK_BAL_INDEX,
1969 )?;
1970
1971 for (_, _, _, _, _, _, deferred) in &mut exec_results {
1972 if let Some(err) = deferred.take() {
1973 return Err(err);
1974 }
1975 }
1976
1977 for (_, _, _, tracked_accounts, reads_satisfied, _, _) in &exec_results {
1986 if !unread_storage_reads.is_empty() {
1987 for pair in reads_satisfied {
1988 unread_storage_reads.remove(pair);
1989 }
1990 }
1991 if !unaccessed_pure_accounts.is_empty() {
1995 unaccessed_pure_accounts.remove(&header.coinbase);
1996 for addr in tracked_accounts {
1997 unaccessed_pure_accounts.remove(addr);
1998 }
1999 }
2000 }
2001
2002 let mut receipts = Vec::with_capacity(n_txs);
2004 let mut cumulative_gas_used = 0_u64;
2005 for (_, tx_type, report, _, _, _, _) in exec_results {
2006 cumulative_gas_used += report.gas_spent;
2007 let mut receipt = Receipt::new(
2008 tx_type,
2009 matches!(report.result, TxResult::Success),
2010 cumulative_gas_used,
2011 report.logs,
2012 );
2013 if tx_type == TxType::Frame {
2014 receipt.payer = report.payer_address;
2015 receipt.frame_receipts = frame_receipts_from(report.frame_results);
2016 }
2017 receipts.push(receipt);
2018 }
2019
2020 Ok((
2021 receipts,
2022 block_gas_used,
2023 unread_storage_reads,
2024 unaccessed_pure_accounts,
2025 tx_gas_breakdowns,
2026 ))
2027 }
2028
2029 #[cfg(feature = "rayon")]
2032 fn seeded_balance(
2033 seed_idx: u32,
2034 acct: ðrex_common::types::block_access_list::AccountChanges,
2035 system_seed: &CacheDB,
2036 store: &Arc<dyn Database>,
2037 ) -> Result<U256, BalValidationError> {
2038 let pos = acct
2039 .balance_changes
2040 .partition_point(|c| c.block_access_index <= seed_idx);
2041 if pos > 0 {
2042 Ok(acct.balance_changes[pos - 1].post_balance)
2043 } else if let Some(a) = system_seed.get(&acct.address) {
2044 Ok(a.info.balance)
2045 } else {
2046 store
2047 .get_account_state(acct.address)
2048 .map(|a| a.balance)
2049 .map_err(|e| {
2050 BalValidationError::Database(format!(
2051 "DB error reading balance for {:?}: {e}",
2052 acct.address
2053 ))
2054 })
2055 }
2056 }
2057
2058 #[cfg(feature = "rayon")]
2061 fn seeded_code_hash(
2062 seed_idx: u32,
2063 acct: ðrex_common::types::block_access_list::AccountChanges,
2064 system_seed: &CacheDB,
2065 store: &Arc<dyn Database>,
2066 ) -> Result<H256, BalValidationError> {
2067 let pos = acct
2068 .code_changes
2069 .partition_point(|c| c.block_access_index <= seed_idx);
2070 if pos > 0 {
2071 let seeded_code = &acct.code_changes[pos - 1].new_code;
2072 Ok(if seeded_code.is_empty() {
2073 *EMPTY_KECCAK_HASH
2074 } else {
2075 ethrex_common::utils::keccak(seeded_code)
2076 })
2077 } else if let Some(a) = system_seed.get(&acct.address) {
2078 Ok(a.info.code_hash)
2079 } else {
2080 store
2081 .get_account_state(acct.address)
2082 .map(|a| a.code_hash)
2083 .map_err(|e| {
2084 BalValidationError::Database(format!(
2085 "DB error reading account state for {:?}: {e}",
2086 acct.address
2087 ))
2088 })
2089 }
2090 }
2091
2092 #[cfg(feature = "rayon")]
2095 fn seeded_nonce(
2096 seed_idx: u32,
2097 acct: ðrex_common::types::block_access_list::AccountChanges,
2098 system_seed: &CacheDB,
2099 store: &Arc<dyn Database>,
2100 ) -> Result<u64, BalValidationError> {
2101 let pos = acct
2102 .nonce_changes
2103 .partition_point(|c| c.block_access_index <= seed_idx);
2104 if pos > 0 {
2105 Ok(acct.nonce_changes[pos - 1].post_nonce)
2106 } else if let Some(a) = system_seed.get(&acct.address) {
2107 Ok(a.info.nonce)
2108 } else {
2109 store
2110 .get_account_state(acct.address)
2111 .map(|a| a.nonce)
2112 .map_err(|e| {
2113 BalValidationError::Database(format!(
2114 "DB error reading nonce for {:?}: {e}",
2115 acct.address
2116 ))
2117 })
2118 }
2119 }
2120
2121 #[cfg(feature = "rayon")]
2129 fn seeded_account_triple(
2130 seed_idx: u32,
2131 acct: ðrex_common::types::block_access_list::AccountChanges,
2132 system_seed: &CacheDB,
2133 store: &Arc<dyn Database>,
2134 ) -> Result<(U256, u64, H256), BalValidationError> {
2135 let bpos = acct
2136 .balance_changes
2137 .partition_point(|c| c.block_access_index <= seed_idx);
2138 let npos = acct
2139 .nonce_changes
2140 .partition_point(|c| c.block_access_index <= seed_idx);
2141 let cpos = acct
2142 .code_changes
2143 .partition_point(|c| c.block_access_index <= seed_idx);
2144
2145 let mut balance = (bpos > 0).then(|| acct.balance_changes[bpos - 1].post_balance);
2146 let mut nonce = (npos > 0).then(|| acct.nonce_changes[npos - 1].post_nonce);
2147 let mut code_hash = (cpos > 0).then(|| {
2148 let c = &acct.code_changes[cpos - 1].new_code;
2149 if c.is_empty() {
2150 *EMPTY_KECCAK_HASH
2151 } else {
2152 ethrex_common::utils::keccak(c)
2153 }
2154 });
2155
2156 if let Some(a) = system_seed.get(&acct.address) {
2158 balance.get_or_insert(a.info.balance);
2159 nonce.get_or_insert(a.info.nonce);
2160 code_hash.get_or_insert(a.info.code_hash);
2161 }
2162
2163 if balance.is_none() || nonce.is_none() || code_hash.is_none() {
2165 let s = store.get_account_state(acct.address).map_err(|e| {
2166 BalValidationError::Database(format!(
2167 "DB error reading account state for {:?}: {e}",
2168 acct.address
2169 ))
2170 })?;
2171 balance.get_or_insert(s.balance);
2172 nonce.get_or_insert(s.nonce);
2173 code_hash.get_or_insert(s.code_hash);
2174 }
2175
2176 Ok((
2177 balance.unwrap_or_default(),
2178 nonce.unwrap_or_default(),
2179 code_hash.unwrap_or(*EMPTY_KECCAK_HASH),
2180 ))
2181 }
2182
2183 #[cfg(feature = "rayon")]
2188 fn seeded_storage_pre_value(
2189 addr: Address,
2190 key: H256,
2191 slot: U256,
2192 system_seed: &CacheDB,
2193 store: &Arc<dyn Database>,
2194 tx_initial: &SeededHashMap<(Address, U256), U256>,
2195 ) -> Result<U256, BalValidationError> {
2196 if let Some(v) = tx_initial.get(&(addr, slot)) {
2197 return Ok(*v);
2198 }
2199 Self::storage_from_seed_or_store(addr, key, slot, system_seed, store)
2200 }
2201
2202 #[cfg(feature = "rayon")]
2205 fn storage_from_seed_or_store(
2206 addr: Address,
2207 key: H256,
2208 slot: U256,
2209 system_seed: &CacheDB,
2210 store: &Arc<dyn Database>,
2211 ) -> Result<U256, BalValidationError> {
2212 if let Some(v) = system_seed
2213 .get(&addr)
2214 .and_then(|a| a.storage.get(&key))
2215 .copied()
2216 {
2217 Ok(v)
2218 } else {
2219 store.get_storage_value(addr, key).map_err(|e| {
2220 BalValidationError::Database(format!(
2221 "DB error reading storage for {addr:?} slot {slot}: {e}"
2222 ))
2223 })
2224 }
2225 }
2226
2227 #[cfg(feature = "rayon")]
2234 fn seeded_storage(
2235 seed_idx: u32,
2236 sc: ðrex_common::types::block_access_list::SlotChange,
2237 addr: Address,
2238 key: H256,
2239 system_seed: &CacheDB,
2240 store: &Arc<dyn Database>,
2241 tx_initial: &SeededHashMap<(Address, U256), U256>,
2242 ) -> Result<U256, BalValidationError> {
2243 if let Some(v) = tx_initial.get(&(addr, sc.slot)) {
2244 return Ok(*v);
2245 }
2246 let pos = sc
2247 .slot_changes
2248 .partition_point(|c| c.block_access_index <= seed_idx);
2249 if pos > 0 {
2250 Ok(sc.slot_changes[pos - 1].post_value)
2251 } else {
2252 Self::storage_from_seed_or_store(addr, key, sc.slot, system_seed, store)
2253 }
2254 }
2255
2256 #[cfg(feature = "rayon")]
2284 #[allow(clippy::too_many_arguments)]
2285 pub fn validate_tx_execution(
2286 bal_idx: u32,
2287 seed_idx: u32,
2288 current_state: &CacheDB,
2289 codes: &SeededHashMap<H256, Code>,
2290 bal: &BlockAccessList,
2291 index: &BalAddressIndex,
2292 system_seed: &CacheDB,
2293 store: &Arc<dyn Database>,
2294 pure_reads: &SeededHashSet<(Address, U256)>,
2295 tx_initial: &SeededHashMap<(Address, U256), U256>,
2296 ) -> Result<(), BalValidationError> {
2297 if let Some(active_accounts) = index.tx_to_accounts.get(&bal_idx) {
2301 for &acct_inner_idx in active_accounts {
2302 let acct = &bal.accounts()[acct_inner_idx];
2303 let addr = acct.address;
2304 let actual = current_state.get(&addr);
2305 let mut seeded_info: Option<(U256, u64, H256)> = None;
2309
2310 if let Some(expected) = find_exact_change_balance(&acct.balance_changes, bal_idx) {
2312 match actual {
2313 Some(a) if a.info.balance == expected => {
2314 if seeded_info.is_none() {
2318 seeded_info = Some(Self::seeded_account_triple(
2319 seed_idx,
2320 acct,
2321 system_seed,
2322 store,
2323 )?);
2324 }
2325 let seeded = seeded_info.unwrap_or_default().0;
2326 if expected == seeded {
2327 return Err(BalValidationError::Mismatch(format!(
2328 "account {addr:?} has spurious no-op BAL balance change \
2329 at index {bal_idx}: post==pre=={expected}"
2330 )));
2331 }
2332 }
2333 Some(a) => {
2334 return Err(BalValidationError::Mismatch(format!(
2335 "account {addr:?} balance mismatch at index {bal_idx}: BAL={expected}, exec={} (diff={})",
2336 a.info.balance,
2337 describe_balance_diff(expected, a.info.balance),
2338 )));
2339 }
2340 None => {
2341 let seeded = Self::seeded_balance(seed_idx, acct, system_seed, store)?;
2348 if expected == seeded {
2349 return Err(BalValidationError::Mismatch(format!(
2350 "account {addr:?} has spurious no-op BAL balance change \
2351 at index {bal_idx} for an account absent from execution \
2352 state: post==pre=={expected}"
2353 )));
2354 }
2355 let all_bal_indices: Vec<u32> = acct
2357 .balance_changes
2358 .iter()
2359 .map(|c| c.block_access_index)
2360 .collect();
2361 let all_nonce_indices: Vec<u32> = acct
2362 .nonce_changes
2363 .iter()
2364 .map(|c| c.block_access_index)
2365 .collect();
2366 let all_storage_indices: Vec<(u32, u64)> = acct
2367 .storage_changes
2368 .iter()
2369 .flat_map(|sc| {
2370 sc.slot_changes
2371 .iter()
2372 .map(|c| (c.block_access_index, sc.slot.low_u64()))
2373 })
2374 .collect();
2375 let code_indices: Vec<u32> = acct
2376 .code_changes
2377 .iter()
2378 .map(|c| c.block_access_index)
2379 .collect();
2380 return Err(BalValidationError::Mismatch(format!(
2381 "account {addr:?} has BAL balance change at {bal_idx} \
2382 but not in execution state (expected={expected}, pre={seeded}, \
2383 all_bal_idx={all_bal_indices:?}, nonce_idx={all_nonce_indices:?}, \
2384 storage_idx={all_storage_indices:?}, code_idx={code_indices:?})"
2385 )));
2386 }
2387 }
2388 }
2389
2390 if let Some(expected) = find_exact_change_nonce(&acct.nonce_changes, bal_idx) {
2392 match actual {
2393 Some(a) if a.info.nonce == expected => {
2394 if seeded_info.is_none() {
2399 seeded_info = Some(Self::seeded_account_triple(
2400 seed_idx,
2401 acct,
2402 system_seed,
2403 store,
2404 )?);
2405 }
2406 let seeded = seeded_info.unwrap_or_default().1;
2407 if expected == seeded {
2408 return Err(BalValidationError::Mismatch(format!(
2409 "account {addr:?} has spurious no-op BAL nonce change \
2410 at index {bal_idx}: post==pre=={expected}"
2411 )));
2412 }
2413 }
2414 Some(a) => {
2415 return Err(BalValidationError::Mismatch(format!(
2416 "account {addr:?} nonce mismatch at index {bal_idx}: BAL={expected}, exec={}",
2417 a.info.nonce
2418 )));
2419 }
2420 None => {
2421 let seeded = Self::seeded_nonce(seed_idx, acct, system_seed, store)?;
2425 if expected == seeded {
2426 return Err(BalValidationError::Mismatch(format!(
2427 "account {addr:?} has spurious no-op BAL nonce change \
2428 at index {bal_idx} for an account absent from execution \
2429 state: post==pre=={expected}"
2430 )));
2431 }
2432 return Err(BalValidationError::Mismatch(format!(
2433 "account {addr:?} has BAL nonce change at {bal_idx} \
2434 but not in execution state (expected={expected}, pre={seeded})"
2435 )));
2436 }
2437 }
2438 }
2439
2440 if let Some(expected_code) = find_exact_change_code(&acct.code_changes, bal_idx) {
2442 match actual {
2443 Some(a) => {
2444 let actual_code = if let Some(c) = codes.get(&a.info.code_hash) {
2445 c.code_bytes()
2446 } else {
2447 let c = store.get_account_code(a.info.code_hash).map_err(|e| {
2448 BalValidationError::Database(format!(
2449 "DB error reading account code for {addr:?}: {e}"
2450 ))
2451 })?;
2452 c.code_bytes()
2453 };
2454 if actual_code != *expected_code {
2455 return Err(BalValidationError::Mismatch(format!(
2456 "account {addr:?} code mismatch at index {bal_idx}"
2457 )));
2458 }
2459 if seeded_info.is_none() {
2462 seeded_info = Some(Self::seeded_account_triple(
2463 seed_idx,
2464 acct,
2465 system_seed,
2466 store,
2467 )?);
2468 }
2469 let seeded_hash = seeded_info.unwrap_or_default().2;
2470 if a.info.code_hash == seeded_hash {
2471 return Err(BalValidationError::Mismatch(format!(
2472 "account {addr:?} has spurious no-op BAL code change \
2473 at index {bal_idx}"
2474 )));
2475 }
2476 }
2477 None => {
2478 let code_hash = if let Some(a) = system_seed.get(&addr) {
2482 a.info.code_hash
2483 } else {
2484 store
2485 .get_account_state(addr)
2486 .map(|a| a.code_hash)
2487 .map_err(|e| {
2488 BalValidationError::Database(format!(
2489 "DB error reading account state for {addr:?}: {e}"
2490 ))
2491 })?
2492 };
2493 let pre_code = if let Some(c) = codes.get(&code_hash) {
2494 c.code_bytes()
2495 } else {
2496 let c = store.get_account_code(code_hash).map_err(|e| {
2497 BalValidationError::Database(format!(
2498 "DB error reading account code for hash \
2499 {code_hash:?}: {e}"
2500 ))
2501 })?;
2502 c.code_bytes()
2503 };
2504 if *expected_code == pre_code {
2505 return Err(BalValidationError::Mismatch(format!(
2509 "account {addr:?} has spurious no-op BAL code change \
2510 at index {bal_idx} for an account absent from execution state"
2511 )));
2512 }
2513 return Err(BalValidationError::Mismatch(format!(
2514 "account {addr:?} has BAL code change at {bal_idx} \
2515 but not in execution state"
2516 )));
2517 }
2518 }
2519 }
2520
2521 for sc in &acct.storage_changes {
2523 if let Some(expected_value) =
2524 find_exact_change_storage(&sc.slot_changes, bal_idx)
2525 {
2526 let key = ethrex_common::utils::u256_to_h256(sc.slot);
2527 let actual_value = actual.and_then(|a| a.storage.get(&key)).copied();
2528 if actual_value != Some(expected_value) {
2529 if actual.is_none() || actual_value.is_none() {
2535 let seeded = Self::seeded_storage(
2536 seed_idx,
2537 sc,
2538 addr,
2539 key,
2540 system_seed,
2541 store,
2542 tx_initial,
2543 )?;
2544 if expected_value == seeded {
2545 return Err(BalValidationError::Mismatch(format!(
2546 "account {addr:?} has spurious no-op BAL storage \
2547 change for slot {} at index {bal_idx} for a slot \
2548 absent from execution state: post==pre=={expected_value}",
2549 sc.slot
2550 )));
2551 }
2552 }
2553 return Err(BalValidationError::Mismatch(format!(
2554 "account {addr:?} storage slot {} mismatch at index {bal_idx}: \
2555 BAL={expected_value}, exec={actual_value:?}",
2556 sc.slot
2557 )));
2558 }
2559 let seeded = Self::seeded_storage(
2563 seed_idx,
2564 sc,
2565 addr,
2566 key,
2567 system_seed,
2568 store,
2569 tx_initial,
2570 )?;
2571 if expected_value == seeded {
2572 return Err(BalValidationError::Mismatch(format!(
2573 "account {addr:?} has spurious no-op BAL storage change for \
2574 slot {} at index {bal_idx}",
2575 sc.slot
2576 )));
2577 }
2578 }
2579 }
2580 }
2581 }
2582
2583 first_failure_by_key(current_state, |addr, account| {
2587 if account.is_unmodified() {
2588 return Ok(());
2589 }
2590
2591 let Some(&bal_acct_idx) = index.addr_to_idx.get(addr) else {
2592 let pre = system_seed
2596 .get(addr)
2597 .map(|a| (a.info.balance, a.info.nonce, a.info.code_hash))
2598 .or_else(|| {
2599 store
2600 .get_account_state(*addr)
2601 .ok()
2602 .map(|a| (a.balance, a.nonce, a.code_hash))
2603 })
2604 .unwrap_or_default();
2605 let post = (
2606 account.info.balance,
2607 account.info.nonce,
2608 account.info.code_hash,
2609 );
2610 if pre != post {
2611 return Err(BalValidationError::Mismatch(format!(
2612 "account {addr:?} was modified by execution but is absent from BAL"
2613 )));
2614 }
2615 return Ok(());
2616 };
2617
2618 let acct = &bal.accounts()[bal_acct_idx];
2619
2620 if !has_exact_change_balance(&acct.balance_changes, bal_idx) {
2622 let seeded = Self::seeded_balance(seed_idx, acct, system_seed, store)?;
2623 if account.info.balance != seeded {
2624 return Err(BalValidationError::Mismatch(format!(
2625 "account {addr:?} balance changed by execution ({}) but BAL has no \
2626 balance change at index {bal_idx} (seeded={seeded})",
2627 account.info.balance
2628 )));
2629 }
2630 }
2631
2632 if !has_exact_change_nonce(&acct.nonce_changes, bal_idx) {
2634 let seeded = Self::seeded_nonce(seed_idx, acct, system_seed, store)?;
2635 if account.info.nonce != seeded {
2636 return Err(BalValidationError::Mismatch(format!(
2637 "account {addr:?} nonce changed by execution ({}) but BAL has no \
2638 nonce change at index {bal_idx} (seeded={seeded})",
2639 account.info.nonce
2640 )));
2641 }
2642 }
2643
2644 if !has_exact_change_code(&acct.code_changes, bal_idx) {
2648 let seeded_hash = Self::seeded_code_hash(seed_idx, acct, system_seed, store)?;
2649 if account.info.code_hash != seeded_hash {
2650 return Err(BalValidationError::Mismatch(format!(
2651 "account {addr:?} code changed by execution but BAL has no \
2652 code change at index {bal_idx} (seeded_hash={seeded_hash:?})"
2653 )));
2654 }
2655 }
2656
2657 first_failure_by_key(&account.storage, |key_h256, &value| {
2659 let slot_u256 = u256_from_big_endian_const(key_h256.0);
2660 let pos = acct
2662 .storage_changes
2663 .partition_point(|sc| sc.slot < slot_u256);
2664 if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
2665 let sc = &acct.storage_changes[pos];
2666 if !has_exact_change_storage(&sc.slot_changes, bal_idx) {
2667 let seeded = Self::seeded_storage(
2672 seed_idx,
2673 sc,
2674 *addr,
2675 *key_h256,
2676 system_seed,
2677 store,
2678 tx_initial,
2679 )?;
2680 if value != seeded {
2681 return Err(BalValidationError::Mismatch(format!(
2682 "account {addr:?} storage slot {slot_u256} changed by \
2683 execution ({value}) but BAL has no change at index \
2684 {bal_idx} (seeded={seeded})"
2685 )));
2686 }
2687 }
2688 }
2689 else if !pure_reads.contains(&(*addr, slot_u256)) {
2703 let pre_value = Self::seeded_storage_pre_value(
2708 *addr,
2709 *key_h256,
2710 slot_u256,
2711 system_seed,
2712 store,
2713 tx_initial,
2714 )?;
2715 if value != pre_value {
2716 let declared_as_read = acct.storage_reads.contains(&slot_u256);
2717 return Err(BalValidationError::Mismatch(format!(
2718 "account {addr:?} storage slot {slot_u256} was written by \
2719 execution ({value}) but has no storage_changes entry at index \
2720 {bal_idx} (pre_value={pre_value}, declared_as_read={declared_as_read})"
2721 )));
2722 }
2723 }
2724 Ok(())
2725 })
2726 })
2727 }
2728
2729 #[cfg(feature = "rayon")]
2741 fn validate_bal_withdrawal_index(
2742 db: &GeneralizedDatabase,
2743 bal: &BlockAccessList,
2744 withdrawal_idx: u32,
2745 index: &BalAddressIndex,
2746 ) -> Result<(), EvmError> {
2747 for acct in bal.accounts() {
2750 let addr = acct.address;
2751 let actual = db.current_accounts_state.get(&addr);
2752
2753 if let Some(expected) = find_exact_change_balance(&acct.balance_changes, withdrawal_idx)
2755 {
2756 match actual {
2757 Some(a) if a.info.balance == expected => {}
2758 Some(a) => {
2759 return Err(EvmError::Custom(format!(
2760 "BAL validation failed for withdrawal: account {addr:?} balance \
2761 mismatch at index {withdrawal_idx}: BAL={expected}, actual={}",
2762 a.info.balance
2763 )));
2764 }
2765 None => {
2766 return Err(EvmError::Custom(format!(
2767 "BAL validation failed for withdrawal: account {addr:?} has \
2768 balance change at index {withdrawal_idx} but was not touched \
2769 by withdrawal/request phase"
2770 )));
2771 }
2772 }
2773 }
2774
2775 if let Some(expected) = find_exact_change_nonce(&acct.nonce_changes, withdrawal_idx) {
2777 match actual {
2778 Some(a) if a.info.nonce == expected => {}
2779 Some(a) => {
2780 return Err(EvmError::Custom(format!(
2781 "BAL validation failed for withdrawal: account {addr:?} nonce \
2782 mismatch at index {withdrawal_idx}: BAL={expected}, actual={}",
2783 a.info.nonce
2784 )));
2785 }
2786 None => {
2787 return Err(EvmError::Custom(format!(
2788 "BAL validation failed for withdrawal: account {addr:?} has \
2789 nonce change at index {withdrawal_idx} but was not touched \
2790 by withdrawal/request phase"
2791 )));
2792 }
2793 }
2794 }
2795
2796 if let Some(expected_code) = find_exact_change_code(&acct.code_changes, withdrawal_idx)
2798 {
2799 let code_hash = if expected_code.is_empty() {
2800 *EMPTY_KECCAK_HASH
2801 } else {
2802 ethrex_common::utils::keccak(expected_code)
2803 };
2804 match actual {
2805 Some(a) if a.info.code_hash == code_hash => {}
2806 Some(_) => {
2807 return Err(EvmError::Custom(format!(
2808 "BAL validation failed for withdrawal: account {addr:?} code \
2809 mismatch at index {withdrawal_idx}"
2810 )));
2811 }
2812 None => {
2813 return Err(EvmError::Custom(format!(
2814 "BAL validation failed for withdrawal: account {addr:?} has \
2815 code change at index {withdrawal_idx} but was not touched \
2816 by withdrawal/request phase"
2817 )));
2818 }
2819 }
2820 }
2821
2822 for sc in &acct.storage_changes {
2824 if let Some(expected_value) =
2825 find_exact_change_storage(&sc.slot_changes, withdrawal_idx)
2826 {
2827 let key = ethrex_common::utils::u256_to_h256(sc.slot);
2828 let actual_value = actual.and_then(|a| a.storage.get(&key)).copied();
2829 if actual_value != Some(expected_value) {
2830 return Err(EvmError::Custom(format!(
2831 "BAL validation failed for withdrawal: account {addr:?} storage \
2832 slot {} mismatch at index {withdrawal_idx}: BAL={expected_value}, \
2833 actual={actual_value:?}",
2834 sc.slot
2835 )));
2836 }
2837 }
2838 }
2839 }
2840
2841 first_failure_by_key(&db.current_accounts_state, |addr, account| {
2845 if account.is_unmodified() {
2846 return Ok(());
2847 }
2848
2849 let Some(&bal_acct_idx) = index.addr_to_idx.get(addr) else {
2850 let pre_state = db.store.get_account_state(*addr).map_err(|e| {
2854 EvmError::Custom(format!(
2855 "BAL validation failed for withdrawal: db error reading \
2856 account {addr:?}: {e}"
2857 ))
2858 })?;
2859 let pre = (pre_state.balance, pre_state.nonce, pre_state.code_hash);
2860 let post = (
2861 account.info.balance,
2862 account.info.nonce,
2863 account.info.code_hash,
2864 );
2865 if pre != post {
2866 return Err(EvmError::Custom(format!(
2867 "BAL validation failed for withdrawal: account {addr:?} was modified \
2868 during withdrawal/request phase but is absent from BAL"
2869 )));
2870 }
2871 return first_failure_by_key(&account.storage, |key_h256, &value| {
2874 let pre_value = db.store.get_storage_value(*addr, *key_h256).map_err(|e| {
2875 EvmError::Custom(format!(
2876 "BAL validation failed for withdrawal: db error reading \
2877 storage {addr:?}[{}]: {e}",
2878 u256_from_big_endian_const(key_h256.0)
2879 ))
2880 })?;
2881 if value != pre_value {
2882 return Err(EvmError::Custom(format!(
2883 "BAL validation failed for withdrawal: account {addr:?} storage \
2884 slot {} changed during withdrawal/request phase but is absent \
2885 from BAL",
2886 u256_from_big_endian_const(key_h256.0)
2887 )));
2888 }
2889 Ok(())
2890 });
2891 };
2892
2893 let acct = &bal.accounts()[bal_acct_idx];
2894
2895 if !has_exact_change_balance(&acct.balance_changes, withdrawal_idx) {
2898 let seeded = match acct.balance_changes.last() {
2899 Some(c) => c.post_balance,
2900 None => {
2901 db.store
2902 .get_account_state(*addr)
2903 .map_err(|e| {
2904 EvmError::Custom(format!(
2905 "BAL validation failed for withdrawal: db error reading \
2906 account {addr:?}: {e}"
2907 ))
2908 })?
2909 .balance
2910 }
2911 };
2912 if account.info.balance != seeded {
2913 return Err(EvmError::Custom(format!(
2914 "BAL validation failed for withdrawal: account {addr:?} balance \
2915 changed during withdrawal/request phase ({}) but BAL has no \
2916 balance change at index {withdrawal_idx} (last_bal={seeded})",
2917 account.info.balance
2918 )));
2919 }
2920 }
2921
2922 if !has_exact_change_nonce(&acct.nonce_changes, withdrawal_idx) {
2924 let seeded = match acct.nonce_changes.last() {
2925 Some(c) => c.post_nonce,
2926 None => {
2927 db.store
2928 .get_account_state(*addr)
2929 .map_err(|e| {
2930 EvmError::Custom(format!(
2931 "BAL validation failed for withdrawal: db error reading \
2932 account {addr:?}: {e}"
2933 ))
2934 })?
2935 .nonce
2936 }
2937 };
2938 if account.info.nonce != seeded {
2939 return Err(EvmError::Custom(format!(
2940 "BAL validation failed for withdrawal: account {addr:?} nonce \
2941 changed during withdrawal/request phase ({}) but BAL has no \
2942 nonce change at index {withdrawal_idx} (last_bal={seeded})",
2943 account.info.nonce
2944 )));
2945 }
2946 }
2947
2948 if !has_exact_change_code(&acct.code_changes, withdrawal_idx) {
2950 let seeded_hash = match acct.code_changes.last() {
2951 Some(c) if c.new_code.is_empty() => *EMPTY_KECCAK_HASH,
2952 Some(c) => ethrex_common::utils::keccak(&c.new_code),
2953 None => {
2954 db.store
2955 .get_account_state(*addr)
2956 .map_err(|e| {
2957 EvmError::Custom(format!(
2958 "BAL validation failed for withdrawal: db error reading \
2959 account {addr:?}: {e}"
2960 ))
2961 })?
2962 .code_hash
2963 }
2964 };
2965 if account.info.code_hash != seeded_hash {
2966 return Err(EvmError::Custom(format!(
2967 "BAL validation failed for withdrawal: account {addr:?} code \
2968 changed during withdrawal/request phase but BAL has no \
2969 code change at index {withdrawal_idx} \
2970 (actual={:?}, last_bal={seeded_hash:?})",
2971 account.info.code_hash
2972 )));
2973 }
2974 }
2975
2976 first_failure_by_key(&account.storage, |key_h256, &value| {
2979 let slot_u256 = u256_from_big_endian_const(key_h256.0);
2980 let pos = acct
2981 .storage_changes
2982 .partition_point(|sc| sc.slot < slot_u256);
2983 if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
2984 let sc = &acct.storage_changes[pos];
2985 if !has_exact_change_storage(&sc.slot_changes, withdrawal_idx) {
2986 let seeded = match sc.slot_changes.last() {
2989 Some(c) => c.post_value,
2990 None => db.store.get_storage_value(*addr, *key_h256).map_err(|e| {
2991 EvmError::Custom(format!(
2992 "BAL validation failed for withdrawal: db error reading \
2993 storage {addr:?}[{slot_u256}]: {e}"
2994 ))
2995 })?,
2996 };
2997 if value != seeded {
2998 return Err(EvmError::Custom(format!(
2999 "BAL validation failed for withdrawal: account {addr:?} \
3000 storage slot {slot_u256} changed during withdrawal/request \
3001 phase ({value}) but BAL has no change at index \
3002 {withdrawal_idx} (last_bal={seeded})"
3003 )));
3004 }
3005 }
3006 } else {
3007 let pre_value = db.store.get_storage_value(*addr, *key_h256).map_err(|e| {
3010 EvmError::Custom(format!(
3011 "BAL validation failed for withdrawal: db error reading \
3012 storage {addr:?}[{slot_u256}]: {e}"
3013 ))
3014 })?;
3015 if value != pre_value {
3016 return Err(EvmError::Custom(format!(
3017 "BAL validation failed for withdrawal: account {addr:?} \
3018 storage slot {slot_u256} changed during withdrawal/request \
3019 phase ({value}) but slot is absent from BAL storage_changes \
3020 (pre={pre_value})"
3021 )));
3022 }
3023 }
3024 Ok(())
3025 })
3026 })
3027 }
3028
3029 #[cfg(feature = "rayon")]
3047 fn validate_bal_pre_exec_index(
3048 db: &GeneralizedDatabase,
3049 bal: &BlockAccessList,
3050 index: &BalAddressIndex,
3051 ) -> Result<(), EvmError> {
3052 let Some(active_accounts) = index.tx_to_accounts.get(&PRE_BLOCK_BAL_INDEX) else {
3053 return Ok(());
3054 };
3055
3056 let pre_account = |addr: Address| -> Result<(U256, u64, H256), EvmError> {
3061 let s = db.store.get_account_state(addr).map_err(|e| {
3062 EvmError::Custom(format!(
3063 "BAL validation failed for system_tx: db error reading account {addr:?}: {e}"
3071 ))
3072 })?;
3073 Ok((s.balance, s.nonce, s.code_hash))
3074 };
3075
3076 for &acct_inner_idx in active_accounts {
3077 let acct = &bal.accounts()[acct_inner_idx];
3078 let addr = acct.address;
3079 let actual = db.current_accounts_state.get(&addr);
3080
3081 if let Some(expected) =
3083 find_exact_change_balance(&acct.balance_changes, PRE_BLOCK_BAL_INDEX)
3084 {
3085 match actual {
3086 Some(a) if a.info.balance == expected => {
3087 if expected == pre_account(addr)?.0 {
3088 return Err(EvmError::Custom(format!(
3089 "BAL validation failed for system_tx: account {addr:?} has spurious \
3090 no-op balance change at index 0: post==pre=={expected}"
3091 )));
3092 }
3093 }
3094 Some(a) => {
3095 return Err(EvmError::Custom(format!(
3096 "BAL validation failed for system_tx: account {addr:?} balance mismatch \
3097 at index 0: BAL={expected}, actual={}",
3098 a.info.balance
3099 )));
3100 }
3101 None => {
3102 return Err(EvmError::Custom(format!(
3103 "BAL validation failed for system_tx: account {addr:?} has balance \
3104 change at index 0 but was not touched by the pre-exec phase"
3105 )));
3106 }
3107 }
3108 }
3109
3110 if let Some(expected) =
3112 find_exact_change_nonce(&acct.nonce_changes, PRE_BLOCK_BAL_INDEX)
3113 {
3114 match actual {
3115 Some(a) if a.info.nonce == expected => {
3116 if expected == pre_account(addr)?.1 {
3117 return Err(EvmError::Custom(format!(
3118 "BAL validation failed for system_tx: account {addr:?} has spurious \
3119 no-op nonce change at index 0: post==pre=={expected}"
3120 )));
3121 }
3122 }
3123 Some(a) => {
3124 return Err(EvmError::Custom(format!(
3125 "BAL validation failed for system_tx: account {addr:?} nonce mismatch \
3126 at index 0: BAL={expected}, actual={}",
3127 a.info.nonce
3128 )));
3129 }
3130 None => {
3131 return Err(EvmError::Custom(format!(
3132 "BAL validation failed for system_tx: account {addr:?} has nonce \
3133 change at index 0 but was not touched by the pre-exec phase"
3134 )));
3135 }
3136 }
3137 }
3138
3139 if let Some(expected_code) =
3141 find_exact_change_code(&acct.code_changes, PRE_BLOCK_BAL_INDEX)
3142 {
3143 let expected_hash = if expected_code.is_empty() {
3144 *EMPTY_KECCAK_HASH
3145 } else {
3146 ethrex_common::utils::keccak(expected_code)
3147 };
3148 match actual {
3149 Some(a) if a.info.code_hash == expected_hash => {
3150 if expected_hash == pre_account(addr)?.2 {
3151 return Err(EvmError::Custom(format!(
3152 "BAL validation failed for system_tx: account {addr:?} has spurious \
3153 no-op code change at index 0"
3154 )));
3155 }
3156 }
3157 Some(_) => {
3158 return Err(EvmError::Custom(format!(
3159 "BAL validation failed for system_tx: account {addr:?} code mismatch \
3160 at index 0"
3161 )));
3162 }
3163 None => {
3164 return Err(EvmError::Custom(format!(
3165 "BAL validation failed for system_tx: account {addr:?} has code \
3166 change at index 0 but was not touched by the pre-exec phase"
3167 )));
3168 }
3169 }
3170 }
3171
3172 for sc in &acct.storage_changes {
3174 let Some(expected_value) =
3175 find_exact_change_storage(&sc.slot_changes, PRE_BLOCK_BAL_INDEX)
3176 else {
3177 continue;
3178 };
3179 let key = ethrex_common::utils::u256_to_h256(sc.slot);
3180 let actual_value = actual.and_then(|a| a.storage.get(&key)).copied();
3181 if actual_value != Some(expected_value) {
3182 return Err(EvmError::Custom(format!(
3183 "BAL validation failed for system_tx: account {addr:?} storage slot {} \
3184 mismatch at index 0: BAL={expected_value}, actual={actual_value:?}",
3185 sc.slot
3186 )));
3187 }
3188 let pre_value = db.store.get_storage_value(addr, key).map_err(|e| {
3189 EvmError::Custom(format!(
3190 "BAL validation failed for system_tx: db error reading storage {addr:?} \
3191 slot {}: {e}",
3192 sc.slot
3193 ))
3194 })?;
3195 if expected_value == pre_value {
3196 return Err(EvmError::Custom(format!(
3197 "BAL validation failed for system_tx: account {addr:?} has spurious no-op \
3198 storage change for slot {} at index 0: post==pre=={expected_value}",
3199 sc.slot
3200 )));
3201 }
3202 }
3203 }
3204
3205 Ok(())
3206 }
3207
3208 #[cfg(feature = "rayon")]
3214 pub fn warm_block(
3215 block: &Block,
3216 store: Arc<dyn Database>,
3217 vm_type: VMType,
3218 crypto: &dyn Crypto,
3219 cancelled: &AtomicBool,
3220 ) -> Result<(), EvmError> {
3221 let txs_with_sender = block
3222 .body
3223 .get_transactions_with_sender(crypto)
3224 .map_err(|error| {
3225 EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
3226 })?;
3227
3228 Self::warm_txs(
3229 &txs_with_sender,
3230 &block.header,
3231 store.clone(),
3232 vm_type,
3233 crypto,
3234 &|| cancelled.load(Ordering::Relaxed),
3235 )?;
3236
3237 let mut db = GeneralizedDatabase::new(store.clone());
3238
3239 if cancelled.load(Ordering::Relaxed) {
3240 return Ok(());
3241 }
3242
3243 for withdrawal in block
3244 .body
3245 .withdrawals
3246 .iter()
3247 .flatten()
3248 .filter(|withdrawal| withdrawal.amount > 0)
3249 {
3250 db.get_account_mut(withdrawal.address).map_err(|_| {
3251 EvmError::DB(format!(
3252 "Withdrawal account {} not found",
3253 withdrawal.address
3254 ))
3255 })?;
3256 }
3257 Ok(())
3258 }
3259
3260 #[cfg(feature = "rayon")]
3268 pub fn warm_txs(
3269 txs_with_sender: &[(&Transaction, Address)],
3270 header: &BlockHeader,
3271 store: Arc<dyn Database>,
3272 vm_type: VMType,
3273 crypto: &dyn Crypto,
3274 should_stop: &(dyn Fn() -> bool + Sync),
3275 ) -> Result<(), EvmError> {
3276 let mut sender_groups: FxHashMap<Address, Vec<&Transaction>> = FxHashMap::default();
3279 for (tx, sender) in txs_with_sender {
3280 sender_groups.entry(*sender).or_default().push(tx);
3281 }
3282
3283 let chain_config = store.get_chain_config()?;
3286 let evm_config = EVMConfig::new_from_chain_config(&chain_config, header);
3287 let chain_id = chain_config.chain_id;
3288 let base_blob_fee_per_gas = get_base_fee_per_blob_gas(header.excess_blob_gas, &evm_config)?;
3290
3291 sender_groups.into_par_iter().for_each_with(
3294 Vec::with_capacity(STACK_LIMIT),
3295 |stack_pool, (sender, txs)| {
3296 if should_stop() {
3297 return;
3298 }
3299 let mut memory_pool = Vec::with_capacity(1);
3303 let mut initcode_pool = Vec::new();
3305 let mut group_db = GeneralizedDatabase::new(store.clone());
3307 for tx in txs {
3308 if should_stop() {
3309 return;
3310 }
3311 let _ = Self::execute_tx_in_block(
3312 tx,
3313 sender,
3314 header,
3315 &mut group_db,
3316 vm_type,
3317 base_blob_fee_per_gas,
3318 stack_pool,
3319 &mut memory_pool,
3320 &mut initcode_pool,
3321 true,
3322 crypto,
3323 evm_config,
3324 chain_id,
3325 None,
3328 );
3329 }
3330 },
3331 );
3332 Ok(())
3333 }
3334
3335 #[cfg(feature = "rayon")]
3338 pub fn bal_storage_slots(bal: &BlockAccessList) -> Vec<(Address, H256)> {
3339 bal.accounts()
3340 .iter()
3341 .flat_map(|ac| {
3342 ac.all_storage_slots()
3343 .map(move |slot| (ac.address, H256::from_uint(&slot)))
3344 })
3345 .collect()
3346 }
3347
3348 #[cfg(feature = "rayon")]
3357 pub fn warm_block_from_bal(
3358 bal: &BlockAccessList,
3359 store: Arc<dyn Database>,
3360 cancelled: &AtomicBool,
3361 ) -> Result<(), EvmError> {
3362 let accounts = bal.accounts();
3363 if accounts.is_empty() {
3364 return Ok(());
3365 }
3366
3367 accounts
3376 .par_chunks(BAL_WARM_CHUNK_ACCOUNTS)
3377 .try_for_each(|chunk| -> Result<(), EvmError> {
3378 if cancelled.load(Ordering::Relaxed) {
3379 return Ok(());
3380 }
3381 let addresses: Vec<Address> = chunk.iter().map(|ac| ac.address).collect();
3382 store
3383 .prefetch_accounts(&addresses)
3384 .map_err(|e| EvmError::Custom(format!("prefetch_accounts: {e}")))?;
3385 for address in addresses {
3386 if cancelled.load(Ordering::Relaxed) {
3387 return Ok(());
3388 }
3389 if let Ok(state) = store.get_account_state(address)
3390 && state.code_hash != *EMPTY_KECCAK_HASH
3391 {
3392 let _ = store.get_account_code(state.code_hash);
3393 }
3394 }
3395 Ok(())
3396 })
3397 }
3398
3399 fn send_state_transitions_tx(
3400 merkleizer: &Sender<Vec<AccountUpdate>>,
3401 db: &mut GeneralizedDatabase,
3402 queue_length: &AtomicUsize,
3403 ) -> Result<(), EvmError> {
3404 let transitions = LEVM::get_state_transitions_tx(db)?;
3405 merkleizer
3406 .send(transitions)
3407 .map_err(|e| EvmError::Custom(format!("send failed: {e}")))?;
3408 queue_length.fetch_add(1, Ordering::Relaxed);
3409 Ok(())
3410 }
3411
3412 fn setup_env(
3413 tx: &Transaction,
3414 tx_sender: Address,
3415 block_header: &BlockHeader,
3416 db: &GeneralizedDatabase,
3417 vm_type: VMType,
3418 ) -> Result<Environment, EvmError> {
3419 let chain_config = db.store.get_chain_config()?;
3423 let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
3424 let base_blob_fee_per_gas =
3425 get_base_fee_per_blob_gas(block_header.excess_blob_gas, &config)?;
3426 Self::setup_env_with_config(
3427 tx,
3428 tx_sender,
3429 block_header,
3430 config,
3431 chain_config.chain_id,
3432 vm_type,
3433 base_blob_fee_per_gas,
3434 )
3435 }
3436
3437 fn setup_env_with_config(
3441 tx: &Transaction,
3442 tx_sender: Address,
3443 block_header: &BlockHeader,
3444 config: EVMConfig,
3445 chain_id: u64,
3446 vm_type: VMType,
3447 base_blob_fee_per_gas: U256,
3448 ) -> Result<Environment, EvmError> {
3449 let gas_price: U256 = calculate_gas_price_for_tx(
3450 tx,
3451 block_header.base_fee_per_gas.unwrap_or_default(),
3452 &vm_type,
3453 )?;
3454
3455 let block_excess_blob_gas = block_header.excess_blob_gas;
3456 let env = Environment {
3457 origin: tx_sender,
3458 gas_limit: tx.gas_limit(),
3459 config,
3460 block_number: block_header.number,
3461 coinbase: block_header.coinbase,
3462 timestamp: block_header.timestamp,
3463 prev_randao: Some(block_header.prev_randao),
3464 slot_number: block_header
3465 .slot_number
3466 .map(U256::from)
3467 .unwrap_or(U256::zero()),
3468 chain_id: chain_id.into(),
3469 base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(),
3470 base_blob_fee_per_gas,
3471 gas_price,
3472 block_excess_blob_gas,
3473 block_blob_gas_used: block_header.blob_gas_used,
3474 tx_blob_hashes: tx.blob_versioned_hashes(),
3475 tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from),
3476 tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from),
3477 tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas(),
3478 tx_nonce: tx.nonce(),
3479 block_gas_limit: block_header.gas_limit,
3480 difficulty: block_header.difficulty,
3481 is_privileged: matches!(tx, Transaction::PrivilegedL2Transaction(_)),
3482 fee_token: tx.fee_token(),
3483 disable_balance_check: false,
3484 disable_nonce_check: false,
3485 disable_gas_allowance_check: false,
3486 disable_sender_eoa_check: false,
3487 disable_execution_gas_cap: false,
3488 is_system_call: false,
3489 };
3490
3491 Ok(env)
3492 }
3493
3494 pub fn execute_tx(
3495 tx: &Transaction,
3497 tx_sender: Address,
3499 block_header: &BlockHeader,
3501 db: &mut GeneralizedDatabase,
3502 vm_type: VMType,
3503 crypto: &dyn Crypto,
3504 stateless_validator: Option<&dyn StatelessValidator>,
3505 ) -> Result<ExecutionReport, EvmError> {
3506 let env = Self::setup_env(tx, tx_sender, block_header, db, vm_type)?;
3507 let mut vm = VM::new(
3508 env,
3509 db,
3510 tx,
3511 LevmCallTracer::disabled(),
3512 vm_type,
3513 crypto,
3514 stateless_validator,
3515 )?;
3516
3517 vm.execute().map_err(VMError::into)
3518 }
3519
3520 #[allow(clippy::too_many_arguments)]
3524 fn execute_tx_in_block(
3525 tx: &Transaction,
3527 tx_sender: Address,
3529 block_header: &BlockHeader,
3531 db: &mut GeneralizedDatabase,
3532 vm_type: VMType,
3533 base_blob_fee_per_gas: U256,
3534 stack_pool: &mut Vec<Stack>,
3535 memory_pool: &mut Vec<Memory>,
3536 initcode_pool: &mut Vec<InitcodeBuffer>,
3537 disable_balance_check: bool,
3538 crypto: &dyn Crypto,
3539 config: EVMConfig,
3540 chain_id: u64,
3541 stateless_validator: Option<&dyn StatelessValidator>,
3542 ) -> Result<ExecutionReport, EvmError> {
3543 let mut env = Self::setup_env_with_config(
3544 tx,
3545 tx_sender,
3546 block_header,
3547 config,
3548 chain_id,
3549 vm_type,
3550 base_blob_fee_per_gas,
3551 )?;
3552 env.disable_balance_check = disable_balance_check;
3553 let mut vm = VM::new_pooled(
3558 env,
3559 db,
3560 tx,
3561 LevmCallTracer::disabled(),
3562 vm_type,
3563 crypto,
3564 stateless_validator,
3565 stack_pool,
3566 memory_pool,
3567 initcode_pool,
3568 )?;
3569 let result = vm.execute().map_err(VMError::into);
3570 vm.reclaim_into(stack_pool, memory_pool, initcode_pool);
3572 result
3573 }
3574
3575 pub fn undo_last_tx(db: &mut GeneralizedDatabase) -> Result<(), EvmError> {
3576 db.undo_last_transaction()?;
3577 Ok(())
3578 }
3579
3580 pub fn simulate_tx_from_generic(
3581 tx: &GenericTransaction,
3583 block_header: &BlockHeader,
3585 db: &mut GeneralizedDatabase,
3586 vm_type: VMType,
3587 crypto: &dyn Crypto,
3588 stateless_validator: Option<&dyn StatelessValidator>,
3589 execution_gas_cap: ExecutionGasCap,
3590 ) -> Result<ExecutionResult, EvmError> {
3591 let mut env = env_from_generic(tx, block_header, db, vm_type)?;
3596 env.disable_execution_gas_cap = execution_gas_cap == ExecutionGasCap::Lifted;
3597
3598 adjust_disabled_base_fee(&mut env);
3599
3600 let converted_tx = generic_tx_to_transaction(tx)?;
3601 let mut vm = vm_from_generic(&converted_tx, env, db, vm_type, crypto, stateless_validator)?;
3602
3603 vm.execute()
3604 .map(|value| value.into())
3605 .map_err(VMError::into)
3606 }
3607
3608 #[allow(clippy::too_many_arguments)]
3623 pub fn simulate_frame_validation_prefix(
3624 tx: &Transaction,
3625 block_header: &BlockHeader,
3626 db: &mut GeneralizedDatabase,
3627 vm_type: VMType,
3628 crypto: &dyn Crypto,
3629 prefix: ðrex_common::types::ValidationPrefix,
3630 _canonical_paymaster_code_hash: Option<H256>,
3631 ) -> Result<FrameValidationOutcome, EvmError> {
3632 use ethrex_common::types::FRAME_TX_MAX_VERIFY_GAS;
3633
3634 let frame_tx = match tx {
3635 Transaction::FrameTransaction(ft) => ft,
3636 _ => {
3637 return Err(EvmError::Custom(
3638 "simulate_frame_validation_prefix requires a frame transaction".to_string(),
3639 ));
3640 }
3641 };
3642 let sender = frame_tx.sender;
3643
3644 let env = Self::setup_env(tx, sender, block_header, db, vm_type)?;
3645 let mut vm = VM::new(
3646 env,
3647 db,
3648 tx,
3649 LevmCallTracer::disabled(),
3650 vm_type,
3651 crypto,
3652 None,
3653 )?;
3654
3655 let canonical_pay_frame: Option<usize> = None;
3658
3659 let sim = match vm.run_frame_validation_prefix(
3660 &prefix.frame_indices,
3661 prefix.deploy_index,
3662 canonical_pay_frame,
3663 ) {
3664 Ok(sim) => sim,
3665 Err(err) => {
3666 return Ok(FrameValidationOutcome {
3670 passed: false,
3671 violation: Some(EvmError::from(err).to_string()),
3672 reservation_ceiling: Self::frame_tx_reservation_ceiling(frame_tx),
3673 accessed_paymaster: None,
3674 touched_sender_slots: Vec::new(),
3675 });
3676 }
3677 };
3678
3679 let reservation_ceiling = Self::frame_tx_reservation_ceiling(frame_tx);
3680 let touched_sender_slots = vm.validation_observer.touched_sender_slots.clone();
3681 let accessed_paymaster = sim.payer_address.map(|payer| (payer, false));
3688
3689 if let Some(violation) = &vm.validation_observer.violation {
3691 return Ok(FrameValidationOutcome {
3692 passed: false,
3693 violation: Some(format!("{violation:?}")),
3694 reservation_ceiling,
3695 accessed_paymaster,
3696 touched_sender_slots,
3697 });
3698 }
3699
3700 if sim.any_revert {
3702 return Ok(FrameValidationOutcome {
3703 passed: false,
3704 violation: Some("validation prefix frame reverted".to_string()),
3705 reservation_ceiling,
3706 accessed_paymaster,
3707 touched_sender_slots,
3708 });
3709 }
3710
3711 if sim.payer_address.is_none() {
3714 return Ok(FrameValidationOutcome {
3715 passed: false,
3716 violation: Some("validation prefix did not establish a payer".to_string()),
3717 reservation_ceiling,
3718 accessed_paymaster,
3719 touched_sender_slots,
3720 });
3721 }
3722
3723 if prefix.deploy_index.is_some() {
3725 let code = vm.db.get_account_code(sender).map_err(VMError::from)?;
3726 if code.is_empty() {
3727 return Ok(FrameValidationOutcome {
3728 passed: false,
3729 violation: Some(format!("{:?}", FrameSimViolation::DeployInstalledNoCode)),
3730 reservation_ceiling,
3731 accessed_paymaster,
3732 touched_sender_slots,
3733 });
3734 }
3735 }
3736
3737 if sim.total_gas_used > FRAME_TX_MAX_VERIFY_GAS {
3739 return Ok(FrameValidationOutcome {
3740 passed: false,
3741 violation: Some(format!(
3742 "validation prefix gas {} exceeds MAX_VERIFY_GAS {}",
3743 sim.total_gas_used, FRAME_TX_MAX_VERIFY_GAS
3744 )),
3745 reservation_ceiling,
3746 accessed_paymaster,
3747 touched_sender_slots,
3748 });
3749 }
3750
3751 Ok(FrameValidationOutcome {
3752 passed: true,
3753 violation: None,
3754 reservation_ceiling,
3755 accessed_paymaster,
3756 touched_sender_slots,
3757 })
3758 }
3759
3760 fn frame_tx_reservation_ceiling(frame_tx: ðrex_common::types::FrameTransaction) -> U256 {
3779 let gas_cost =
3780 U256::from(frame_tx.max_fee_per_gas).saturating_mul(U256::from(frame_tx.max_gas()));
3781 let blob_cost = U256::from(frame_tx.blob_versioned_hashes.len())
3782 .saturating_mul(U256::from(131072u64))
3783 .saturating_mul(frame_tx.max_fee_per_blob_gas);
3784 gas_cost.saturating_add(blob_cost)
3785 }
3786
3787 pub fn get_state_transitions(
3788 db: &mut GeneralizedDatabase,
3789 ) -> Result<Vec<AccountUpdate>, EvmError> {
3790 Ok(db.get_state_transitions()?)
3791 }
3792
3793 pub fn get_state_transitions_tx(
3794 db: &mut GeneralizedDatabase,
3795 ) -> Result<Vec<AccountUpdate>, EvmError> {
3796 Ok(db.get_state_transitions_tx()?)
3797 }
3798
3799 pub fn process_withdrawals(
3800 db: &mut GeneralizedDatabase,
3801 withdrawals: &[Withdrawal],
3802 ) -> Result<(), EvmError> {
3803 for (address, increment) in withdrawals
3805 .iter()
3806 .filter(|withdrawal| withdrawal.amount > 0)
3807 .map(|w| (w.address, u128::from(w.amount) * u128::from(GWEI_TO_WEI)))
3808 {
3809 let account = db
3810 .get_account_mut(address)
3811 .map_err(|_| EvmError::DB(format!("Withdrawal account {address} not found")))?;
3812
3813 let initial_balance = account.info.balance;
3814 account.info.balance += increment.into();
3815 let new_balance = account.info.balance;
3816
3817 if let Some(recorder) = db.bal_recorder_mut() {
3819 recorder.set_initial_balance(address, initial_balance);
3820 recorder.record_balance_change(address, new_balance);
3821 }
3822 }
3823 Ok(())
3824 }
3825
3826 pub fn beacon_root_contract_call(
3828 block_header: &BlockHeader,
3829 db: &mut GeneralizedDatabase,
3830 vm_type: VMType,
3831 crypto: &dyn Crypto,
3832 ) -> Result<(), EvmError> {
3833 if let VMType::L2(_) = vm_type {
3834 return Err(EvmError::InvalidEVM(
3835 "beacon_root_contract_call should not be called for L2 VM".to_string(),
3836 ));
3837 }
3838
3839 let beacon_root = block_header.parent_beacon_block_root.ok_or_else(|| {
3840 EvmError::Header("parent_beacon_block_root field is missing".to_string())
3841 })?;
3842
3843 generic_system_contract_levm(
3844 block_header,
3845 Bytes::copy_from_slice(beacon_root.as_bytes()),
3846 db,
3847 BEACON_ROOTS_ADDRESS.address,
3848 SYSTEM_ADDRESS,
3849 vm_type,
3850 crypto,
3851 )?;
3852 Ok(())
3853 }
3854
3855 pub fn process_block_hash_history(
3856 block_header: &BlockHeader,
3857 db: &mut GeneralizedDatabase,
3858 vm_type: VMType,
3859 crypto: &dyn Crypto,
3860 ) -> Result<(), EvmError> {
3861 if let VMType::L2(_) = vm_type {
3862 return Err(EvmError::InvalidEVM(
3863 "process_block_hash_history should not be called for L2 VM".to_string(),
3864 ));
3865 }
3866
3867 generic_system_contract_levm(
3868 block_header,
3869 Bytes::copy_from_slice(block_header.parent_hash.as_bytes()),
3870 db,
3871 HISTORY_STORAGE_ADDRESS.address,
3872 SYSTEM_ADDRESS,
3873 vm_type,
3874 crypto,
3875 )?;
3876 Ok(())
3877 }
3878
3879 pub fn install_expiry_verifier_code(
3885 db: &mut GeneralizedDatabase,
3886 crypto: &dyn Crypto,
3887 ) -> Result<(), EvmError> {
3888 const PREDEPLOY_NONCE: u64 = 1;
3890
3891 let current = db.get_account_code(EXPIRY_VERIFIER_PREDEPLOY.address)?;
3892 if current.code() == EXPIRY_VERIFIER_RUNTIME_BYTECODE.as_slice() {
3893 return Ok(());
3894 }
3895 let code = Code::from_bytecode(
3896 Bytes::from_static(&EXPIRY_VERIFIER_RUNTIME_BYTECODE),
3897 crypto,
3898 );
3899 let code_hash = code.hash;
3900 if let Some(recorder) = db.bal_recorder_mut() {
3904 recorder.record_code_change(EXPIRY_VERIFIER_PREDEPLOY.address, code.code_bytes());
3905 recorder.record_nonce_change(EXPIRY_VERIFIER_PREDEPLOY.address, PREDEPLOY_NONCE);
3906 }
3907 let acc = db
3908 .get_account_mut(EXPIRY_VERIFIER_PREDEPLOY.address)
3909 .map_err(EvmError::from)?;
3910 acc.info.code_hash = code_hash;
3911 acc.info.nonce = PREDEPLOY_NONCE;
3912 db.codes.entry(code_hash).or_insert(code);
3913 Ok(())
3914 }
3915
3916 pub(crate) fn read_withdrawal_requests(
3917 block_header: &BlockHeader,
3918 db: &mut GeneralizedDatabase,
3919 vm_type: VMType,
3920 crypto: &dyn Crypto,
3921 ) -> Result<ExecutionReport, EvmError> {
3922 if let VMType::L2(_) = vm_type {
3923 return Err(EvmError::InvalidEVM(
3924 "read_withdrawal_requests should not be called for L2 VM".to_string(),
3925 ));
3926 }
3927
3928 let report = generic_system_contract_levm(
3929 block_header,
3930 Bytes::new(),
3931 db,
3932 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
3933 SYSTEM_ADDRESS,
3934 vm_type,
3935 crypto,
3936 )?;
3937
3938 match report.result {
3939 TxResult::Success => Ok(report),
3940 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
3942 "REVERT when reading withdrawal requests with error: {vm_error:?}. According to EIP-7002, the revert of this system call invalidates the block.",
3943 ))),
3944 }
3945 }
3946
3947 pub(crate) fn dequeue_consolidation_requests(
3948 block_header: &BlockHeader,
3949 db: &mut GeneralizedDatabase,
3950 vm_type: VMType,
3951 crypto: &dyn Crypto,
3952 ) -> Result<ExecutionReport, EvmError> {
3953 if let VMType::L2(_) = vm_type {
3954 return Err(EvmError::InvalidEVM(
3955 "dequeue_consolidation_requests should not be called for L2 VM".to_string(),
3956 ));
3957 }
3958
3959 let report = generic_system_contract_levm(
3960 block_header,
3961 Bytes::new(),
3962 db,
3963 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
3964 SYSTEM_ADDRESS,
3965 vm_type,
3966 crypto,
3967 )?;
3968
3969 match report.result {
3970 TxResult::Success => Ok(report),
3971 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
3973 "REVERT when dequeuing consolidation requests with error: {vm_error:?}. According to EIP-7251, the revert of this system call invalidates the block.",
3974 ))),
3975 }
3976 }
3977
3978 pub(crate) fn read_builder_deposit_requests(
3979 block_header: &BlockHeader,
3980 db: &mut GeneralizedDatabase,
3981 vm_type: VMType,
3982 crypto: &dyn Crypto,
3983 ) -> Result<ExecutionReport, EvmError> {
3984 if let VMType::L2(_) = vm_type {
3985 return Err(EvmError::InvalidEVM(
3986 "read_builder_deposit_requests should not be called for L2 VM".to_string(),
3987 ));
3988 }
3989
3990 let report = generic_system_contract_levm(
3991 block_header,
3992 Bytes::new(),
3993 db,
3994 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
3995 SYSTEM_ADDRESS,
3996 vm_type,
3997 crypto,
3998 )?;
3999
4000 match report.result {
4001 TxResult::Success => Ok(report),
4002 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
4004 "REVERT when reading builder deposit requests with error: {vm_error:?}. According to EIP-8282, the revert of this system call invalidates the block.",
4005 ))),
4006 }
4007 }
4008
4009 pub(crate) fn dequeue_builder_exit_requests(
4010 block_header: &BlockHeader,
4011 db: &mut GeneralizedDatabase,
4012 vm_type: VMType,
4013 crypto: &dyn Crypto,
4014 ) -> Result<ExecutionReport, EvmError> {
4015 if let VMType::L2(_) = vm_type {
4016 return Err(EvmError::InvalidEVM(
4017 "dequeue_builder_exit_requests should not be called for L2 VM".to_string(),
4018 ));
4019 }
4020
4021 let report = generic_system_contract_levm(
4022 block_header,
4023 Bytes::new(),
4024 db,
4025 BUILDER_EXIT_CONTRACT_ADDRESS.address,
4026 SYSTEM_ADDRESS,
4027 vm_type,
4028 crypto,
4029 )?;
4030
4031 match report.result {
4032 TxResult::Success => Ok(report),
4033 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
4035 "REVERT when dequeuing builder exit requests with error: {vm_error:?}. According to EIP-8282, the revert of this system call invalidates the block.",
4036 ))),
4037 }
4038 }
4039
4040 pub fn create_access_list(
4041 mut tx: GenericTransaction,
4042 header: &BlockHeader,
4043 db: &mut GeneralizedDatabase,
4044 vm_type: VMType,
4045 crypto: &dyn Crypto,
4046 ) -> Result<(ExecutionResult, AccessList), VMError> {
4047 let mut env = env_from_generic(&tx, header, db, vm_type)?;
4048
4049 adjust_disabled_base_fee(&mut env);
4050
4051 let converted_tx = generic_tx_to_transaction(&tx)?;
4052 let mut vm = vm_from_generic(&converted_tx, env.clone(), db, vm_type, crypto, None)?;
4053
4054 vm.stateless_execute()?;
4055
4056 tx.access_list = vm.substate.make_access_list();
4058 let converted_tx = generic_tx_to_transaction(&tx)?;
4059 let mut vm = vm_from_generic(&converted_tx, env, db, vm_type, crypto, None)?;
4060
4061 let report = vm.stateless_execute()?;
4062
4063 Ok((
4064 report.into(),
4065 tx.access_list
4066 .into_iter()
4067 .map(|x| (x.address, x.storage_keys))
4068 .collect(),
4069 ))
4070 }
4071
4072 pub fn prepare_block(
4073 block: &Block,
4074 db: &mut GeneralizedDatabase,
4075 vm_type: VMType,
4076 crypto: &dyn Crypto,
4077 ) -> Result<(), EvmError> {
4078 let chain_config = db.store.get_chain_config()?;
4079 let block_header = &block.header;
4080 let fork = chain_config.fork(block_header.timestamp);
4081
4082 if let VMType::L2(_) = vm_type {
4084 return Ok(());
4085 }
4086
4087 if fork >= Fork::Hegota {
4091 Self::install_expiry_verifier_code(db, crypto)?;
4092 }
4093
4094 if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun {
4095 Self::beacon_root_contract_call(block_header, db, vm_type, crypto)?;
4096 }
4097
4098 if fork >= Fork::Prague {
4099 Self::process_block_hash_history(block_header, db, vm_type, crypto)?;
4101 }
4102 Ok(())
4103 }
4104}
4105
4106pub fn generic_system_contract_levm(
4107 block_header: &BlockHeader,
4108 calldata: Bytes,
4109 db: &mut GeneralizedDatabase,
4110 contract_address: Address,
4111 system_address: Address,
4112 vm_type: VMType,
4113 crypto: &dyn Crypto,
4114) -> Result<ExecutionReport, EvmError> {
4115 let chain_config = db.store.get_chain_config()?;
4116 let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
4117 let env = Environment {
4118 origin: system_address,
4119 gas_limit: SYS_CALL_GAS_LIMIT,
4130 block_number: block_header.number,
4131 coinbase: block_header.coinbase,
4132 timestamp: block_header.timestamp,
4133 prev_randao: Some(block_header.prev_randao),
4134 base_fee_per_gas: U256::zero(),
4135 gas_price: U256::zero(),
4136 block_excess_blob_gas: block_header.excess_blob_gas,
4137 block_blob_gas_used: block_header.blob_gas_used,
4138 block_gas_limit: block_header.gas_limit,
4143 is_system_call: true,
4144 config,
4145 ..Default::default()
4146 };
4147
4148 debug_assert!(
4154 env.gas_price.is_zero() && env.base_fee_per_gas.is_zero(),
4155 "system calls must run with a zero gas price"
4156 );
4157
4158 if PRAGUE_SYSTEM_CONTRACTS
4168 .iter()
4169 .chain(AMSTERDAM_REQUEST_PREDEPLOYS.iter())
4170 .any(|contract| contract.address == contract_address)
4171 && db.get_account_code(contract_address)?.is_empty()
4172 {
4173 return Err(EvmError::SystemContractCallFailed(format!(
4174 "System contract: {contract_address} has no code after deployment"
4175 )));
4176 };
4177
4178 let tx = &Transaction::EIP1559Transaction(EIP1559Transaction {
4179 to: TxKind::Call(contract_address),
4180 value: U256::zero(),
4181 data: calldata,
4182 ..Default::default()
4183 });
4184 if let Some(recorder) = db.bal_recorder.as_mut() {
4186 recorder.enter_system_call();
4187 }
4188
4189 let result = VM::new(
4190 env,
4191 db,
4192 tx,
4193 LevmCallTracer::disabled(),
4194 vm_type,
4195 crypto,
4196 None,
4197 )
4198 .and_then(|mut vm| vm.execute())
4199 .map_err(EvmError::from);
4200
4201 if let Some(recorder) = db.bal_recorder.as_mut() {
4203 recorder.exit_system_call();
4204 }
4205
4206 result
4207}
4208
4209#[allow(unreachable_code)]
4210#[allow(unused_variables)]
4211pub fn extract_all_requests_levm(
4212 receipts: &[Receipt],
4213 db: &mut GeneralizedDatabase,
4214 header: &BlockHeader,
4215 vm_type: VMType,
4216 crypto: &dyn Crypto,
4217) -> Result<Vec<Requests>, EvmError> {
4218 if let VMType::L2(_) = vm_type {
4219 return Err(EvmError::InvalidEVM(
4220 "extract_all_requests_levm should not be called for L2 VM".to_string(),
4221 ));
4222 }
4223
4224 let chain_config = db.store.get_chain_config()?;
4225 let fork = chain_config.fork(header.timestamp);
4226
4227 if fork < Fork::Prague {
4228 return Ok(Default::default());
4229 }
4230
4231 let withdrawals_data: Vec<u8> = LEVM::read_withdrawal_requests(header, db, vm_type, crypto)?
4232 .output
4233 .into();
4234 let consolidation_data: Vec<u8> =
4235 LEVM::dequeue_consolidation_requests(header, db, vm_type, crypto)?
4236 .output
4237 .into();
4238
4239 let deposits = Requests::from_deposit_receipts(chain_config.deposit_contract_address, receipts)
4240 .ok_or(EvmError::InvalidDepositRequest)?;
4241 let withdrawals = Requests::from_withdrawals_data(withdrawals_data);
4242 let consolidation = Requests::from_consolidation_data(consolidation_data);
4243
4244 let mut requests = vec![deposits, withdrawals, consolidation];
4245
4246 if fork >= Fork::Amsterdam {
4250 requests.reserve_exact(2);
4252 let builder_deposit_data: Vec<u8> =
4253 LEVM::read_builder_deposit_requests(header, db, vm_type, crypto)?
4254 .output
4255 .into();
4256 let builder_exit_data: Vec<u8> =
4257 LEVM::dequeue_builder_exit_requests(header, db, vm_type, crypto)?
4258 .output
4259 .into();
4260 requests.push(Requests::from_builder_deposit_data(builder_deposit_data));
4261 requests.push(Requests::from_builder_exit_data(builder_exit_data));
4262 }
4263
4264 Ok(requests)
4265}
4266
4267pub fn calculate_gas_price_for_generic(tx: &GenericTransaction, basefee: u64) -> U256 {
4270 if !tx.gas_price.is_zero() {
4271 tx.gas_price
4273 } else {
4274 min(
4276 tx.max_priority_fee_per_gas.unwrap_or(0) + basefee,
4277 tx.max_fee_per_gas.unwrap_or(0),
4278 )
4279 .into()
4280 }
4281}
4282
4283pub fn calculate_gas_price_for_tx(
4284 tx: &Transaction,
4285 mut fee_per_gas: u64,
4286 vm_type: &VMType,
4287) -> Result<U256, VMError> {
4288 let Some(max_priority_fee) = tx.max_priority_fee() else {
4289 return Ok(tx.gas_price());
4291 };
4292
4293 let max_fee_per_gas = tx.max_fee_per_gas().ok_or(VMError::TxValidation(
4294 TxValidationError::InsufficientMaxFeePerGas,
4295 ))?;
4296
4297 if let VMType::L2(fee_config) = vm_type
4298 && let Some(operator_fee_config) = &fee_config.operator_fee_config
4299 {
4300 fee_per_gas += operator_fee_config.operator_fee_per_gas;
4301 }
4302
4303 if fee_per_gas > max_fee_per_gas {
4304 return Err(VMError::TxValidation(
4305 TxValidationError::InsufficientMaxFeePerGas,
4306 ));
4307 }
4308
4309 Ok(min(max_priority_fee + fee_per_gas, max_fee_per_gas).into())
4310}
4311
4312pub(crate) fn adjust_disabled_base_fee(env: &mut Environment) {
4316 if env.gas_price == U256::zero() {
4317 env.base_fee_per_gas = U256::zero();
4318 }
4319 if env
4328 .tx_max_fee_per_blob_gas
4329 .is_some_and(|v| v == U256::zero())
4330 {
4331 env.base_blob_fee_per_gas = U256::zero();
4332 }
4333}
4334
4335fn adjust_disabled_l2_fees(env: &Environment, vm_type: VMType) -> VMType {
4337 if env.gas_price == U256::zero()
4338 && let VMType::L2(fee_config) = vm_type
4339 {
4340 return VMType::L2(FeeConfig {
4342 operator_fee_config: None,
4343 l1_fee_config: None,
4344 ..fee_config
4345 });
4346 }
4347 vm_type
4348}
4349
4350pub(crate) fn env_from_generic(
4351 tx: &GenericTransaction,
4352 header: &BlockHeader,
4353 db: &GeneralizedDatabase,
4354 vm_type: VMType,
4355) -> Result<Environment, VMError> {
4356 let chain_config = db.store.get_chain_config()?;
4357 let gas_price =
4358 calculate_gas_price_for_generic(tx, header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE));
4359 let block_excess_blob_gas = header.excess_blob_gas;
4360 let config = EVMConfig::new_from_chain_config(&chain_config, header);
4361
4362 let slot_number = if let VMType::L2(_) = vm_type {
4376 U256::zero()
4377 } else {
4378 header.slot_number.map(U256::from).unwrap_or(U256::zero())
4379 };
4380
4381 Ok(Environment {
4382 origin: tx.from.0.into(),
4383 gas_limit: tx
4384 .gas
4385 .unwrap_or(get_max_allowed_gas_limit(header.gas_limit, config.fork)), config,
4387 block_number: header.number,
4388 coinbase: header.coinbase,
4389 timestamp: header.timestamp,
4390 prev_randao: Some(header.prev_randao),
4391 slot_number,
4392 chain_id: chain_config.chain_id.into(),
4393 base_fee_per_gas: header.base_fee_per_gas.unwrap_or_default().into(),
4394 base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,
4395 gas_price,
4396 block_excess_blob_gas,
4397 block_blob_gas_used: header.blob_gas_used,
4398 tx_blob_hashes: tx.blob_versioned_hashes.clone(),
4399 tx_max_priority_fee_per_gas: tx.max_priority_fee_per_gas.map(U256::from),
4400 tx_max_fee_per_gas: tx.max_fee_per_gas.map(U256::from),
4401 tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas,
4402 tx_nonce: tx.nonce.unwrap_or_default(),
4403 block_gas_limit: header.gas_limit,
4404 difficulty: header.difficulty,
4405 is_privileged: false,
4406 fee_token: tx.fee_token,
4407 disable_balance_check: false,
4408 disable_nonce_check: true,
4414 disable_gas_allowance_check: true,
4420 disable_sender_eoa_check: true,
4423 disable_execution_gas_cap: false,
4427 is_system_call: false,
4428 })
4429}
4430
4431fn generic_tx_to_transaction(tx: &GenericTransaction) -> Result<Transaction, VMError> {
4446 let (signature_r, signature_s) = (U256::MAX, U256::MAX);
4449 let nonce = tx.nonce.unwrap_or_default();
4450 let gas_limit = tx.gas.unwrap_or_default();
4451 let max_fee_per_gas = tx.max_fee_per_gas.unwrap_or_default();
4452 let max_priority_fee_per_gas = tx.max_priority_fee_per_gas.unwrap_or_default();
4453 let chain_id = tx.chain_id.unwrap_or_default();
4454
4455 Ok(match &tx.authorization_list {
4456 Some(authorization_list) => Transaction::EIP7702Transaction(EIP7702Transaction {
4457 to: match tx.to {
4458 TxKind::Call(to) => to,
4459 TxKind::Create => {
4460 return Err(InternalError::msg("Generic Tx cannot be create type").into());
4461 }
4462 },
4463 value: tx.value,
4464 data: tx.input.clone(),
4465 access_list: tx
4466 .access_list
4467 .iter()
4468 .map(|list| (list.address, list.storage_keys.clone()))
4469 .collect(),
4470 authorization_list: authorization_list
4471 .iter()
4472 .map(|auth| Into::<AuthorizationTuple>::into(auth.clone()))
4473 .collect(),
4474 nonce,
4475 gas_limit,
4476 max_fee_per_gas,
4477 max_priority_fee_per_gas,
4478 chain_id,
4479 signature_r,
4480 signature_s,
4481 ..Default::default()
4482 }),
4483 None => Transaction::EIP1559Transaction(EIP1559Transaction {
4484 to: tx.to.clone(),
4485 value: tx.value,
4486 data: tx.input.clone(),
4487 access_list: tx
4488 .access_list
4489 .iter()
4490 .map(|list| (list.address, list.storage_keys.clone()))
4491 .collect(),
4492 nonce,
4493 gas_limit,
4494 max_fee_per_gas,
4495 max_priority_fee_per_gas,
4496 chain_id,
4497 signature_r,
4498 signature_s,
4499 ..Default::default()
4500 }),
4501 })
4502}
4503
4504fn vm_from_generic<'a>(
4505 tx: &'a Transaction,
4506 env: Environment,
4507 db: &'a mut GeneralizedDatabase,
4508 vm_type: VMType,
4509 crypto: &'a dyn Crypto,
4510 stateless_validator: Option<&'a dyn StatelessValidator>,
4511) -> Result<VM<'a>, VMError> {
4512 let vm_type = adjust_disabled_l2_fees(&env, vm_type);
4513 VM::new(
4514 env,
4515 db,
4516 tx,
4517 LevmCallTracer::disabled(),
4518 vm_type,
4519 crypto,
4520 stateless_validator,
4521 )
4522}
4523
4524pub fn get_max_allowed_gas_limit(block_gas_limit: u64, fork: Fork) -> u64 {
4525 if fork >= Fork::Osaka && fork < Fork::Amsterdam {
4533 POST_OSAKA_GAS_LIMIT_CAP
4534 } else {
4535 block_gas_limit
4536 }
4537}
4538
4539#[allow(dead_code)]
4547fn describe_balance_diff(expected: U256, actual: U256) -> String {
4548 let (sign, mag) = if expected >= actual {
4549 ("+", expected - actual)
4550 } else {
4551 ("-", actual - expected)
4552 };
4553 let Ok(mag_u128) = u128::try_from(mag) else {
4554 return format!("{sign}{mag}");
4555 };
4556 if mag_u128 == 0 {
4557 return "0".to_string();
4558 }
4559 let cpsb: u128 = 1530;
4560 let consts = [
4562 ("NEW_ACCOUNT", 120u128),
4563 ("STORAGE_SET", 64),
4564 ("AUTH_BASE", 23),
4565 ("AUTH_TOTAL", 143),
4566 ];
4567 for &gp in &[10u128, 1, 7, 100, 1000, 1_000_000_000] {
4569 if !mag_u128.is_multiple_of(gp) {
4570 continue;
4571 }
4572 let gas = mag_u128 / gp;
4573 if !gas.is_multiple_of(cpsb) {
4574 continue;
4575 }
4576 let bytes = gas / cpsb;
4577 for (name, c) in consts {
4578 if bytes.is_multiple_of(c) {
4579 let n = bytes / c;
4580 return format!(
4581 "{sign}{mag_u128} wei (= {gas} gas at {gp} wei/gas = {n}× {name}_state_gas)"
4582 );
4583 }
4584 }
4585 }
4586 format!("{sign}{mag_u128} wei")
4587}
4588
4589#[cfg(all(test, feature = "rayon"))]
4592mod bal_tests {
4593 use super::*;
4594 use ethrex_common::H256;
4595 use ethrex_common::types::AccountState;
4596 use ethrex_common::types::block_access_list::{
4597 AccountChanges, BalanceChange, NonceChange, SlotChange, StorageChange,
4598 };
4599 use ethrex_levm::errors::DatabaseError;
4600
4601 fn addr(byte: u8) -> Address {
4602 let mut a = Address::zero();
4603 a.0[19] = byte;
4604 a
4605 }
4606
4607 struct MockStore {
4609 accounts: FxHashMap<Address, AccountState>,
4610 }
4611
4612 impl MockStore {
4613 fn new() -> Self {
4614 Self {
4615 accounts: FxHashMap::default(),
4616 }
4617 }
4618
4619 fn with_account(mut self, address: Address, state: AccountState) -> Self {
4620 self.accounts.insert(address, state);
4621 self
4622 }
4623 }
4624
4625 impl Database for MockStore {
4626 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4627 Ok(self.accounts.get(&address).copied().unwrap_or_default())
4628 }
4629 fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
4630 Ok(U256::zero())
4631 }
4632 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4633 Ok(H256::zero())
4634 }
4635 fn get_chain_config(&self) -> Result<ethrex_common::types::ChainConfig, DatabaseError> {
4636 Err(DatabaseError::Custom("not implemented".into()))
4637 }
4638 fn get_account_code(&self, _: H256) -> Result<ethrex_common::types::Code, DatabaseError> {
4639 Ok(ethrex_common::types::Code::from_bytecode(
4640 Bytes::new(),
4641 ðrex_crypto::NativeCrypto,
4642 ))
4643 }
4644 fn get_code_metadata(
4645 &self,
4646 _: H256,
4647 ) -> Result<ethrex_common::types::CodeMetadata, DatabaseError> {
4648 Ok(ethrex_common::types::CodeMetadata { length: 0 })
4649 }
4650 }
4651
4652 #[test]
4653 fn test_bal_to_account_updates_basic() {
4654 let address = addr(1);
4656 let store = MockStore::new().with_account(
4657 address,
4658 AccountState {
4659 balance: U256::from(100),
4660 nonce: 5,
4661 code_hash: *EMPTY_KECCAK_HASH,
4662 storage_root: H256::zero(),
4663 },
4664 );
4665
4666 let bal = BlockAccessList::from_accounts(vec![
4667 AccountChanges::new(address)
4668 .with_balance_changes(vec![
4669 BalanceChange::new(1, U256::from(90)),
4670 BalanceChange::new(2, U256::from(80)),
4671 ])
4672 .with_nonce_changes(vec![NonceChange::new(1, 6)])
4673 .with_storage_changes(vec![SlotChange::with_changes(
4674 U256::from(42),
4675 vec![StorageChange::new(1, U256::from(999))],
4676 )]),
4677 ]);
4678
4679 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4680 assert_eq!(updates.len(), 1);
4681 let u = &updates[0];
4682 assert_eq!(u.address, address);
4683 assert!(!u.removed);
4684 let info = u.info.as_ref().unwrap();
4685 assert_eq!(info.balance, U256::from(80));
4687 assert_eq!(info.nonce, 6);
4688 assert_eq!(info.code_hash, *EMPTY_KECCAK_HASH);
4689 let key = ethrex_common::utils::u256_to_h256(U256::from(42));
4691 assert_eq!(*u.added_storage.get(&key).unwrap(), U256::from(999));
4692 }
4693
4694 #[test]
4695 fn test_bal_to_account_updates_highest_index_wins() {
4696 let address = addr(2);
4698 let store = MockStore::new().with_account(
4699 address,
4700 AccountState {
4701 balance: U256::from(1000),
4702 nonce: 0,
4703 code_hash: *EMPTY_KECCAK_HASH,
4704 storage_root: H256::zero(),
4705 },
4706 );
4707
4708 let bal = BlockAccessList::from_accounts(vec![
4709 AccountChanges::new(address).with_balance_changes(vec![
4710 BalanceChange::new(1, U256::from(900)),
4711 BalanceChange::new(2, U256::from(800)),
4712 BalanceChange::new(3, U256::from(700)),
4713 ]),
4714 ]);
4715
4716 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4717 assert_eq!(updates.len(), 1);
4718 assert_eq!(updates[0].info.as_ref().unwrap().balance, U256::from(700));
4719 }
4720
4721 #[test]
4722 fn test_bal_to_account_updates_reads_only_skipped() {
4723 let address = addr(3);
4725 let store = MockStore::new();
4726
4727 let bal = BlockAccessList::from_accounts(vec![
4728 AccountChanges::new(address).with_storage_reads(vec![U256::from(1)]),
4729 ]);
4730
4731 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4732 assert!(updates.is_empty());
4733 }
4734
4735 #[test]
4736 fn test_bal_to_account_updates_removal() {
4737 let address = addr(4);
4739 let store = MockStore::new().with_account(
4740 address,
4741 AccountState {
4742 balance: U256::from(50),
4743 nonce: 1,
4744 code_hash: *EMPTY_KECCAK_HASH,
4745 storage_root: H256::zero(),
4746 },
4747 );
4748
4749 let bal = BlockAccessList::from_accounts(vec![
4750 AccountChanges::new(address)
4751 .with_balance_changes(vec![BalanceChange::new(1, U256::zero())])
4752 .with_nonce_changes(vec![NonceChange::new(1, 0)]),
4753 ]);
4754
4755 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4756 assert_eq!(updates.len(), 1);
4757 assert!(updates[0].removed);
4758 }
4759
4760 #[test]
4761 fn test_bal_to_account_updates_storage_zero() {
4762 let address = addr(5);
4764 let store = MockStore::new();
4765
4766 let bal = BlockAccessList::from_accounts(vec![
4767 AccountChanges::new(address).with_storage_changes(vec![SlotChange::with_changes(
4768 U256::from(7),
4769 vec![StorageChange::new(1, U256::zero())],
4770 )]),
4771 ]);
4772
4773 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4774 assert_eq!(updates.len(), 1);
4775 let key = ethrex_common::utils::u256_to_h256(U256::from(7));
4776 assert_eq!(*updates[0].added_storage.get(&key).unwrap(), U256::zero());
4777 }
4778
4779 #[test]
4780 fn test_bal_to_account_updates_code_deployment() {
4781 let address = addr(6);
4783 let store = MockStore::new();
4784 let code = Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xf3]); let expected_hash =
4786 ethrex_common::types::Code::from_bytecode(code.clone(), ðrex_crypto::NativeCrypto)
4787 .hash;
4788
4789 let bal = BlockAccessList::from_accounts(vec![
4790 AccountChanges::new(address)
4791 .with_code_changes(vec![
4792 ethrex_common::types::block_access_list::CodeChange::new(1, code.clone()),
4793 ])
4794 .with_nonce_changes(vec![NonceChange::new(1, 1)]),
4795 ]);
4796
4797 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4798 assert_eq!(updates.len(), 1);
4799 let u = &updates[0];
4800 assert_eq!(u.info.as_ref().unwrap().code_hash, expected_hash);
4801 assert_eq!(u.code.as_ref().unwrap().code(), &code[..]);
4802 }
4803}
4804
4805#[cfg(test)]
4806mod system_call_coinbase_tests {
4807 use super::*;
4813 use ethrex_common::types::{AccountState, AccountUpdate, ChainConfig, Code, CodeMetadata};
4814 use ethrex_crypto::NativeCrypto;
4815 use ethrex_levm::db::Database;
4816 use ethrex_levm::errors::DatabaseError;
4817 use std::sync::Arc;
4818
4819 const HISTORY_RUNTIME_CODE: &str = concat!(
4821 "3373fffffffffffffffffffffffffffffffffffffffe1460465760203603604257",
4822 "5f35600143038111604257611fff81430311604257611fff900654",
4823 "5f5260205ff35b5f5ffd5b5f35611fff60014303065500",
4824 );
4825
4826 struct Store {
4827 chain_config: ChainConfig,
4828 history_code: Code,
4829 }
4830
4831 impl Database for Store {
4832 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4833 if address == HISTORY_STORAGE_ADDRESS.address {
4834 return Ok(AccountState {
4835 nonce: 1,
4836 code_hash: self.history_code.hash,
4837 ..Default::default()
4838 });
4839 }
4840 Ok(AccountState::default())
4841 }
4842 fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
4843 Ok(U256::zero())
4844 }
4845 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4846 Ok(H256::zero())
4847 }
4848 fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
4849 Ok(self.chain_config)
4850 }
4851 fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
4852 if code_hash == self.history_code.hash {
4853 return Ok(self.history_code.clone());
4854 }
4855 Ok(Code::default())
4856 }
4857 fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
4858 let length = if code_hash == self.history_code.hash {
4859 self.history_code.len() as u64
4860 } else {
4861 0
4862 };
4863 Ok(CodeMetadata { length })
4864 }
4865 }
4866
4867 fn history_code() -> Code {
4868 let bytes = hex::decode(HISTORY_RUNTIME_CODE).expect("history runtime code is valid hex");
4869 Code::from_bytecode(Bytes::from(bytes), &NativeCrypto)
4870 }
4871
4872 fn prague_db() -> GeneralizedDatabase {
4873 GeneralizedDatabase::new(Arc::new(Store {
4874 chain_config: ChainConfig {
4875 prague_time: Some(0),
4876 ..Default::default()
4877 },
4878 history_code: history_code(),
4879 }))
4880 }
4881
4882 fn parent_hash_value(parent_hash: H256) -> U256 {
4883 U256::from_big_endian(parent_hash.as_bytes())
4884 }
4885
4886 fn history_slot(block_number: u64) -> H256 {
4887 H256::from_low_u64_be((block_number - 1) % 8191)
4888 }
4889
4890 fn run_history_update(coinbase: Address) -> (Option<U256>, Vec<AccountUpdate>, H256) {
4894 let mut db = prague_db();
4895 let parent_hash = H256::from_low_u64_be(0x2935);
4896 let block_number = 42;
4897 let header = BlockHeader {
4898 parent_hash,
4899 coinbase,
4900 number: block_number,
4901 timestamp: 1,
4902 ..Default::default()
4903 };
4904
4905 LEVM::process_block_hash_history(&header, &mut db, VMType::L1, &NativeCrypto)
4906 .expect("history system call executes");
4907
4908 let slot = history_slot(block_number);
4909 let stored_value = db
4910 .current_accounts_state
4911 .get(&HISTORY_STORAGE_ADDRESS.address)
4912 .and_then(|account| account.storage.get(&slot).copied());
4913 let updates =
4914 LEVM::get_state_transitions(&mut db).expect("state transitions are generated");
4915
4916 (stored_value, updates, parent_hash)
4917 }
4918
4919 fn assert_history_write_emitted(coinbase: Address) {
4920 let (stored_value, updates, parent_hash) = run_history_update(coinbase);
4921 let slot = history_slot(42);
4922 assert_eq!(
4923 stored_value,
4924 Some(parent_hash_value(parent_hash)),
4925 "history storage must hold the parent hash after the system call"
4926 );
4927 assert!(
4928 updates.iter().any(|update| {
4929 update.address == HISTORY_STORAGE_ADDRESS.address
4930 && update.added_storage.get(&slot) == Some(&parent_hash_value(parent_hash))
4931 }),
4932 "the history-contract storage write must be emitted as a state update"
4933 );
4934 }
4935
4936 #[test]
4937 fn ordinary_coinbase_preserves_history_storage_write() {
4938 assert_history_write_emitted(Address::from_low_u64_be(0xbeef));
4939 }
4940
4941 #[test]
4944 fn history_address_coinbase_preserves_history_storage_write() {
4945 assert_history_write_emitted(HISTORY_STORAGE_ADDRESS.address);
4946 }
4947}
4948
4949#[cfg(all(test, feature = "rayon"))]
4954mod burned_fees_tests {
4955 use super::*;
4956 use ethrex_common::{
4957 constants::{DEFAULT_OMMERS_HASH, DEFAULT_REQUESTS_HASH},
4958 types::{AccountState, Block, BlockBody, BlockHeader, ChainConfig, Code, CodeMetadata},
4959 utils::keccak,
4960 };
4961 use ethrex_crypto::NativeCrypto;
4962 use ethrex_levm::errors::DatabaseError;
4963 use ethrex_rlp::encode::PayloadRLPEncode;
4964
4965 fn lstar_chain_config() -> ChainConfig {
4968 ChainConfig {
4969 chain_id: 1,
4970 homestead_block: Some(0),
4971 eip150_block: Some(0),
4972 eip155_block: Some(0),
4973 eip158_block: Some(0),
4974 byzantium_block: Some(0),
4975 constantinople_block: Some(0),
4976 petersburg_block: Some(0),
4977 istanbul_block: Some(0),
4978 berlin_block: Some(0),
4979 london_block: Some(0),
4980 terminal_total_difficulty: Some(0),
4981 terminal_total_difficulty_passed: true,
4982 shanghai_time: Some(0),
4983 cancun_time: Some(0),
4984 prague_time: Some(0),
4985 amsterdam_time: Some(0),
4986 lstar_time: Some(0),
4987 ..Default::default()
4988 }
4989 }
4990
4991 fn amsterdam_chain_config() -> ChainConfig {
4992 ChainConfig {
4993 chain_id: 1,
4994 homestead_block: Some(0),
4995 eip150_block: Some(0),
4996 eip155_block: Some(0),
4997 eip158_block: Some(0),
4998 byzantium_block: Some(0),
4999 constantinople_block: Some(0),
5000 petersburg_block: Some(0),
5001 istanbul_block: Some(0),
5002 berlin_block: Some(0),
5003 london_block: Some(0),
5004 terminal_total_difficulty: Some(0),
5005 terminal_total_difficulty_passed: true,
5006 shanghai_time: Some(0),
5007 cancun_time: Some(0),
5008 prague_time: Some(0),
5009 amsterdam_time: Some(0),
5010 lstar_time: None, ..Default::default()
5012 }
5013 }
5014
5015 struct MockDB {
5018 chain_config: ChainConfig,
5019 accounts: FxHashMap<Address, (AccountState, Bytes)>,
5020 storage: FxHashMap<(Address, H256), U256>,
5022 }
5023
5024 impl MockDB {
5025 fn new(chain_config: ChainConfig) -> Self {
5026 Self {
5027 chain_config,
5028 accounts: FxHashMap::default(),
5029 storage: FxHashMap::default(),
5030 }
5031 }
5032
5033 fn with_account(mut self, address: Address, balance: U256, code: Option<Bytes>) -> Self {
5034 let code_bytes = code.unwrap_or_default();
5035 let code_hash = if code_bytes.is_empty() {
5036 *EMPTY_KECCAK_HASH
5037 } else {
5038 keccak(&code_bytes)
5039 };
5040 let state = AccountState {
5041 nonce: 0,
5042 balance,
5043 storage_root: H256::zero(),
5044 code_hash,
5045 };
5046 self.accounts.insert(address, (state, code_bytes));
5047 self
5048 }
5049
5050 fn with_storage(mut self, address: Address, slot: H256, value: U256) -> Self {
5051 self.storage.insert((address, slot), value);
5052 self
5053 }
5054 }
5055
5056 impl Database for MockDB {
5057 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
5058 Ok(self
5059 .accounts
5060 .get(&address)
5061 .map(|(s, _)| *s)
5062 .unwrap_or_default())
5063 }
5064
5065 fn get_storage_value(&self, address: Address, slot: H256) -> Result<U256, DatabaseError> {
5066 Ok(self
5067 .storage
5068 .get(&(address, slot))
5069 .copied()
5070 .unwrap_or_default())
5071 }
5072
5073 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
5074 Ok(H256::zero())
5075 }
5076
5077 fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
5078 Ok(self.chain_config)
5079 }
5080
5081 fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
5082 let crypto = NativeCrypto;
5083 for (state, bytecode) in self.accounts.values() {
5084 if state.code_hash == code_hash && !bytecode.is_empty() {
5085 return Ok(Code::from_bytecode(bytecode.clone(), &crypto));
5086 }
5087 }
5088 Ok(Code::from_bytecode(Bytes::new(), &crypto))
5089 }
5090
5091 fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
5092 for (state, bytecode) in self.accounts.values() {
5093 if state.code_hash == code_hash {
5094 return Ok(CodeMetadata {
5095 length: bytecode.len() as u64,
5096 });
5097 }
5098 }
5099 Ok(CodeMetadata { length: 0 })
5100 }
5101 }
5102
5103 const PRIVATE_KEY: [u8; 32] = [
5106 0x4c, 0x08, 0x83, 0xa6, 0x91, 0x02, 0x93, 0x7d, 0x62, 0x31, 0x47, 0x1b, 0x5d, 0xbb, 0x62,
5107 0x04, 0xfe, 0x51, 0x29, 0x61, 0x70, 0x82, 0x79, 0x2a, 0xe4, 0x68, 0xd0, 0x1a, 0x3f, 0x36,
5108 0x23, 0x18,
5109 ];
5110
5111 fn sender_address() -> Address {
5112 use k256::ecdsa::SigningKey;
5113 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
5114 let uncompressed_pub = signing_key.verifying_key().to_encoded_point(false);
5115 let pub_hash = keccak(&uncompressed_pub.as_bytes()[1..]);
5116 Address::from_slice(&pub_hash.0[12..])
5117 }
5118
5119 fn signed_eth_transfer(to: Address, nonce: u64, base_fee: u64) -> Transaction {
5122 use k256::ecdsa::SigningKey;
5123
5124 let tx = EIP1559Transaction {
5125 chain_id: 1,
5126 nonce,
5127 max_priority_fee_per_gas: 1,
5128 max_fee_per_gas: base_fee + 10,
5129 gas_limit: 21_000,
5130 to: TxKind::Call(to),
5131 value: U256::zero(),
5132 ..Default::default()
5133 };
5134
5135 let mut buf = vec![0x02u8];
5137 tx.encode_payload(&mut buf);
5138 let hash = keccak(&buf);
5139
5140 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
5141 let (sig, rid) = signing_key
5142 .sign_prehash_recoverable(&hash.0)
5143 .expect("sign ok");
5144 let sig_bytes = sig.to_bytes();
5145
5146 Transaction::EIP1559Transaction(EIP1559Transaction {
5147 signature_r: U256::from_big_endian(&sig_bytes[..32]),
5148 signature_s: U256::from_big_endian(&sig_bytes[32..]),
5149 signature_y_parity: rid.to_byte() != 0,
5150 ..tx
5151 })
5152 }
5153
5154 const STOP_BYTECODE: &[u8] = &[0x00];
5156
5157 fn make_block_with_tx(tx: Transaction, base_fee: u64) -> Block {
5158 let header = BlockHeader {
5159 parent_hash: H256::zero(),
5160 ommers_hash: *DEFAULT_OMMERS_HASH,
5161 number: 1,
5162 gas_limit: 1_000_000,
5163 timestamp: 1,
5164 base_fee_per_gas: Some(base_fee),
5165 withdrawals_root: Some(H256::zero()),
5166 requests_hash: Some(*DEFAULT_REQUESTS_HASH),
5167 parent_beacon_block_root: None,
5169 ..Default::default()
5170 };
5171 let body = BlockBody {
5172 transactions: vec![tx],
5173 ommers: vec![],
5174 withdrawals: Some(vec![]),
5175 };
5176 Block::new(header, body)
5177 }
5178
5179 fn make_gen_db(chain_config: ChainConfig) -> GeneralizedDatabase {
5180 let sender = sender_address();
5181 let stop = Bytes::from_static(STOP_BYTECODE);
5182 let mock = MockDB::new(chain_config)
5183 .with_account(sender, U256::from(1_000_000_000u64), None)
5185 .with_account(
5187 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
5188 U256::zero(),
5189 Some(stop.clone()),
5190 )
5191 .with_account(
5192 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
5193 U256::zero(),
5194 Some(stop.clone()),
5195 )
5196 .with_account(
5198 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
5199 U256::zero(),
5200 Some(stop.clone()),
5201 )
5202 .with_account(
5203 BUILDER_EXIT_CONTRACT_ADDRESS.address,
5204 U256::zero(),
5205 Some(stop),
5206 );
5207 GeneralizedDatabase::new(Arc::new(mock))
5208 }
5209
5210 const SSTORE_CLEAR_BYTECODE: &[u8] = &[0x60, 0x00, 0x60, 0x00, 0x55, 0x00];
5215
5216 fn signed_call_tx(to: Address, nonce: u64, base_fee: u64, gas_limit: u64) -> Transaction {
5218 use k256::ecdsa::SigningKey;
5219
5220 let tx = EIP1559Transaction {
5221 chain_id: 1,
5222 nonce,
5223 max_priority_fee_per_gas: 1,
5224 max_fee_per_gas: base_fee + 10,
5225 gas_limit,
5226 to: TxKind::Call(to),
5227 value: U256::zero(),
5228 ..Default::default()
5229 };
5230
5231 let mut buf = vec![0x02u8];
5232 tx.encode_payload(&mut buf);
5233 let hash = keccak(&buf);
5234
5235 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
5236 let (sig, rid) = signing_key
5237 .sign_prehash_recoverable(&hash.0)
5238 .expect("sign ok");
5239 let sig_bytes = sig.to_bytes();
5240
5241 Transaction::EIP1559Transaction(EIP1559Transaction {
5242 signature_r: U256::from_big_endian(&sig_bytes[..32]),
5243 signature_s: U256::from_big_endian(&sig_bytes[32..]),
5244 signature_y_parity: rid.to_byte() != 0,
5245 ..tx
5246 })
5247 }
5248
5249 fn make_gen_db_sstore_clear(
5252 chain_config: ChainConfig,
5253 contract_addr: Address,
5254 ) -> GeneralizedDatabase {
5255 let sender = sender_address();
5256 let stop = Bytes::from_static(STOP_BYTECODE);
5257 let mock = MockDB::new(chain_config)
5258 .with_account(sender, U256::from(100_000_000_000u64), None)
5259 .with_account(
5260 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
5261 U256::zero(),
5262 Some(stop.clone()),
5263 )
5264 .with_account(
5265 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
5266 U256::zero(),
5267 Some(stop.clone()),
5268 )
5269 .with_account(
5271 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
5272 U256::zero(),
5273 Some(stop.clone()),
5274 )
5275 .with_account(
5276 BUILDER_EXIT_CONTRACT_ADDRESS.address,
5277 U256::zero(),
5278 Some(stop),
5279 )
5280 .with_account(
5281 contract_addr,
5282 U256::zero(),
5283 Some(Bytes::from_static(SSTORE_CLEAR_BYTECODE)),
5284 )
5285 .with_storage(contract_addr, H256::zero(), U256::one());
5287 GeneralizedDatabase::new(Arc::new(mock))
5288 }
5289
5290 #[test]
5293 fn test_burned_fees_block_aggregate() {
5294 use std::sync::atomic::AtomicUsize;
5295 use std::sync::mpsc;
5296
5297 const BASE_FEE: u64 = 10;
5298 let recipient = Address::from_low_u64_be(0xBEEF);
5306 let tx = signed_eth_transfer(recipient, 0, BASE_FEE);
5307 let block = make_block_with_tx(tx, BASE_FEE);
5308 let crypto = NativeCrypto;
5309
5310 let mut db1 = make_gen_db(lstar_chain_config());
5312 let (result, _bal) = LEVM::execute_block(&block, &mut db1, VMType::L1, &crypto, None)
5313 .expect("execute_block succeeded");
5314 let cumulative = result
5315 .receipts
5316 .last()
5317 .map(|r| r.cumulative_gas_used)
5318 .unwrap_or(0);
5319 let expected = compute_burned_fees(BASE_FEE, cumulative, 0, 0);
5320 assert_eq!(
5321 result.burned_fees,
5322 Some(expected),
5323 "execute_block: burned_fees must equal base_fee * post-refund gas ({cumulative})"
5324 );
5325
5326 let mut db2 = make_gen_db(lstar_chain_config());
5328 let (tx_sender, _rx) = mpsc::channel();
5329 let queue_len = AtomicUsize::new(0);
5330 let tx2 = signed_eth_transfer(recipient, 0, BASE_FEE);
5331 let block2 = make_block_with_tx(tx2, BASE_FEE);
5332 let (result_pipeline, _bal) = LEVM::execute_block_pipeline(
5333 &block2,
5334 &mut db2,
5335 VMType::L1,
5336 Some(tx_sender),
5337 &queue_len,
5338 &crypto,
5339 None, false,
5341 None,
5342 )
5343 .expect("execute_block_pipeline succeeded");
5344 assert_eq!(
5345 result_pipeline.burned_fees, result.burned_fees,
5346 "execute_block_pipeline: burned_fees must match execute_block (production/verification consistency)"
5347 );
5348
5349 let tx3 = signed_eth_transfer(recipient, 0, BASE_FEE);
5351 let block3 = make_block_with_tx(tx3, BASE_FEE);
5352 let mut db3 = make_gen_db(amsterdam_chain_config());
5353 let (result_pre_lstar, _) =
5354 LEVM::execute_block(&block3, &mut db3, VMType::L1, &crypto, None)
5355 .expect("pre-LStar execute_block succeeded");
5356 assert_eq!(
5357 result_pre_lstar.burned_fees, None,
5358 "pre-LStar: burned_fees must be None"
5359 );
5360 }
5361
5362 #[test]
5379 fn test_burned_fees_post_refund_discriminating() {
5380 use std::sync::atomic::AtomicUsize;
5381 use std::sync::mpsc;
5382
5383 const BASE_FEE: u64 = 10;
5384 const TX_GAS_LIMIT: u64 = 100_000;
5386
5387 let contract_addr = Address::from_low_u64_be(0xC0DEBEEF);
5388 let crypto = NativeCrypto;
5389
5390 let tx_a = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
5392 let block_a = make_block_with_tx(tx_a, BASE_FEE);
5393 let mut db_a = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
5394 let (result_a, _) = LEVM::execute_block(&block_a, &mut db_a, VMType::L1, &crypto, None)
5395 .expect("execute_block with SSTORE clear succeeded");
5396
5397 let cumulative_a = result_a.receipts[0].cumulative_gas_used;
5398 assert!(
5400 result_a.block_gas_used > cumulative_a,
5401 "SSTORE refund must cause block_gas_used ({}) > cumulative_gas_used ({})",
5402 result_a.block_gas_used,
5403 cumulative_a,
5404 );
5405 assert_eq!(
5407 result_a.burned_fees,
5408 Some(BASE_FEE * cumulative_a),
5409 "execute_block: burned_fees must use post-refund gas (cumulative={}, block_gas_used={})",
5410 cumulative_a,
5411 result_a.block_gas_used,
5412 );
5413
5414 let tx_b = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
5416 let block_b = make_block_with_tx(tx_b, BASE_FEE);
5417 let mut db_b = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
5418 let (tx_sender_b, _rx_b) = mpsc::channel();
5419 let queue_len_b = AtomicUsize::new(0);
5420 let (result_b, _) = LEVM::execute_block_pipeline(
5421 &block_b,
5422 &mut db_b,
5423 VMType::L1,
5424 Some(tx_sender_b),
5425 &queue_len_b,
5426 &crypto,
5427 None, false,
5429 None,
5430 )
5431 .expect("execute_block_pipeline sequential with SSTORE clear succeeded");
5432
5433 let cumulative_b = result_b.receipts[0].cumulative_gas_used;
5434 assert!(
5435 result_b.block_gas_used > cumulative_b,
5436 "sequential pipeline: SSTORE refund must be present",
5437 );
5438 assert_eq!(
5439 result_b.burned_fees,
5440 Some(BASE_FEE * cumulative_b),
5441 "execute_block_pipeline (sequential): burned_fees must use post-refund gas",
5442 );
5443
5444 let tx_c = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
5447 let block_c = make_block_with_tx(tx_c, BASE_FEE);
5448 let mut db_c1 = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
5449 let (_, bal_c) = LEVM::execute_block(&block_c, &mut db_c1, VMType::L1, &crypto, None)
5450 .expect("BAL generation succeeded");
5451 let bal_c = bal_c.expect("LStar must produce a BAL");
5452
5453 let mut db_c2 = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
5454 let (tx_sender_c, _rx_c) = mpsc::channel();
5455 let queue_len_c = AtomicUsize::new(0);
5456 let (result_c, _) = LEVM::execute_block_pipeline(
5457 &block_c,
5458 &mut db_c2,
5459 VMType::L1,
5460 Some(tx_sender_c),
5461 &queue_len_c,
5462 &crypto,
5463 Some(std::sync::Arc::new(bal_c)), true,
5465 None,
5466 )
5467 .expect("execute_block_pipeline parallel with SSTORE clear succeeded");
5468
5469 let cumulative_c = result_c.receipts[0].cumulative_gas_used;
5470 assert!(
5471 result_c.block_gas_used > cumulative_c,
5472 "parallel pipeline: SSTORE refund must be present",
5473 );
5474 assert_eq!(
5475 result_c.burned_fees,
5476 Some(BASE_FEE * cumulative_c),
5477 "execute_block_pipeline (parallel): burned_fees must use post-refund gas",
5478 );
5479
5480 assert_eq!(
5482 result_a.burned_fees, result_b.burned_fees,
5483 "execute_block vs sequential pipeline: burned_fees must match",
5484 );
5485 assert_eq!(
5486 result_a.burned_fees, result_c.burned_fees,
5487 "execute_block vs parallel pipeline: burned_fees must match",
5488 );
5489
5490 let tx_d = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
5492 let block_d = make_block_with_tx(tx_d, BASE_FEE);
5493 let mut db_d = make_gen_db_sstore_clear(amsterdam_chain_config(), contract_addr);
5494 let (result_d, _) = LEVM::execute_block(&block_d, &mut db_d, VMType::L1, &crypto, None)
5495 .expect("pre-LStar with SSTORE clear succeeded");
5496 assert_eq!(
5497 result_d.burned_fees, None,
5498 "pre-LStar: burned_fees must be None"
5499 );
5500 }
5501}
5502
5503#[cfg(test)]
5504mod simulated_tx_encoding_tests {
5505 use super::*;
5515 use ethrex_common::types::GenericTransaction;
5516 use ethrex_rlp::encode::RLPEncode;
5517
5518 fn signed_equivalent(gas_limit: u64, data: Bytes) -> Transaction {
5521 Transaction::EIP1559Transaction(EIP1559Transaction {
5522 chain_id: 9_999,
5523 nonce: 42,
5524 max_priority_fee_per_gas: 1_000_000_000,
5525 max_fee_per_gas: 2_000_000_000,
5526 gas_limit,
5527 to: TxKind::Create,
5528 value: U256::from(1u64),
5529 data,
5530 access_list: vec![],
5531 signature_y_parity: true,
5532 signature_r: U256::MAX,
5533 signature_s: U256::MAX,
5534 ..Default::default()
5535 })
5536 }
5537
5538 fn generic(gas_limit: u64, data: Bytes) -> GenericTransaction {
5539 GenericTransaction {
5540 to: TxKind::Create,
5541 value: U256::from(1u64),
5542 input: data,
5543 nonce: Some(42),
5544 gas: Some(gas_limit),
5545 max_fee_per_gas: Some(2_000_000_000),
5546 max_priority_fee_per_gas: Some(1_000_000_000),
5547 chain_id: Some(9_999),
5548 ..Default::default()
5549 }
5550 }
5551
5552 #[test]
5553 fn simulated_tx_never_encodes_shorter_than_the_signed_one() {
5554 for (gas_limit, data) in [
5555 (21_000u64, Bytes::new()),
5556 (150_000, Bytes::from_static(&[0x60, 0x00, 0x60, 0x00])),
5557 (5_000_000, Bytes::from(vec![0xab; 4_096])),
5558 ] {
5559 let simulated = generic_tx_to_transaction(&generic(gas_limit, data.clone()))
5560 .expect("conversion should succeed");
5561 let signed = signed_equivalent(gas_limit, data.clone());
5562 assert!(
5563 simulated.length() >= signed.length(),
5564 "simulated tx encodes {} bytes, signed one {} — the L2 would under-reserve \
5565 the L1 fee by the difference (gas_limit {gas_limit}, {} data bytes)",
5566 simulated.length(),
5567 signed.length(),
5568 data.len(),
5569 );
5570 }
5571 }
5572}
5573
5574#[cfg(all(test, feature = "rayon"))]
5575mod first_failure_by_key_tests {
5576 use super::first_failure_by_key;
5577 use ethrex_common::seeded_hash::SeededHashMap;
5578
5579 const FAILING: u64 = 256;
5582
5583 #[test]
5584 fn names_the_lowest_failing_key() {
5585 let map: SeededHashMap<u64, ()> = (0..2 * FAILING).map(|key| (key, ())).collect();
5587 let result = first_failure_by_key(&map, |key, _| match key % 2 {
5588 0 => Ok(()),
5589 _ => Err(*key),
5590 });
5591 assert_eq!(result, Err(1));
5592 }
5593
5594 #[test]
5595 fn walks_a_passing_map_once() {
5596 let map: SeededHashMap<u64, ()> = (0..FAILING).map(|key| (key, ())).collect();
5597 let mut checked = 0;
5598 let result: Result<(), ()> = first_failure_by_key(&map, |_, _| {
5599 checked += 1;
5600 Ok(())
5601 });
5602 assert_eq!(result, Ok(()));
5603 assert_eq!(checked, FAILING);
5604 }
5605
5606 #[test]
5607 fn names_the_lowest_failure_of_a_nested_map() {
5608 let map: SeededHashMap<u64, SeededHashMap<u64, ()>> = (0..FAILING)
5610 .map(|outer| {
5611 let inner = (0..FAILING).map(|key| (key, ())).collect();
5612 (outer, inner)
5613 })
5614 .collect();
5615 let result = first_failure_by_key(&map, |outer, inner| {
5616 first_failure_by_key(inner, |key, _| match (*outer >= 7, *key >= 3) {
5617 (true, true) => Err((*outer, *key)),
5618 _ => Ok(()),
5619 })
5620 });
5621 assert_eq!(result, Err((7, 3)));
5622 }
5623}