1pub mod db;
2mod tracing;
3
4use super::{BlockExecutionResult, FrameValidationOutcome, TxGasBreakdown, compute_burned_fees};
5use crate::system_contracts::{
6 AMSTERDAM_REQUEST_PREDEPLOYS, BEACON_ROOTS_ADDRESS, BUILDER_DEPOSIT_CONTRACT_ADDRESS,
7 BUILDER_EXIT_CONTRACT_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
8 EXPIRY_VERIFIER_PREDEPLOY, EXPIRY_VERIFIER_RUNTIME_BYTECODE, HISTORY_STORAGE_ADDRESS,
9 PRAGUE_SYSTEM_CONTRACTS, SYSTEM_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
10};
11use crate::{EvmError, ExecutionResult};
12use bytes::Bytes;
13use ethrex_common::H256;
14#[cfg(feature = "rayon")]
15use ethrex_common::constants::EMPTY_KECCAK_HASH;
16use ethrex_common::types::Code;
17#[cfg(feature = "rayon")]
18use ethrex_common::types::TxType;
19use ethrex_common::types::block_access_list::BlockAccessList;
20#[cfg(feature = "rayon")]
21use ethrex_common::types::block_access_list::{
22 BalAddressIndex, find_exact_change_balance, find_exact_change_code, find_exact_change_nonce,
23 find_exact_change_storage, has_exact_change_balance, has_exact_change_code,
24 has_exact_change_nonce, has_exact_change_storage,
25};
26use ethrex_common::types::fee_config::FeeConfig;
27use ethrex_common::types::{AuthorizationTuple, EIP7702Transaction};
28#[cfg(feature = "rayon")]
29use ethrex_common::utils::u256_from_big_endian_const;
30use ethrex_common::{
31 Address, U256,
32 types::{
33 AccessList, AccountUpdate, Block, BlockHeader, EIP1559Transaction, Fork, FrameReceipt,
34 GWEI_TO_WEI, GenericTransaction, INITIAL_BASE_FEE, Log, Receipt, Transaction, TxKind,
35 Withdrawal, requests::Requests,
36 },
37};
38#[cfg(feature = "rayon")]
39use ethrex_common::{BigEndianHash, validate_block_access_list_size, validate_header_bal_indices};
40use ethrex_crypto::Crypto;
41use ethrex_levm::EVMConfig;
42use ethrex_levm::StatelessValidator;
43#[cfg(feature = "rayon")]
44use ethrex_levm::account::{AccountStatus, LevmAccount};
45use ethrex_levm::call_frame::Stack;
46use ethrex_levm::constants::{
47 POST_OSAKA_GAS_LIMIT_CAP, STACK_LIMIT, SYS_CALL_GAS_LIMIT, TX_MAX_GAS_LIMIT_AMSTERDAM,
48};
49use ethrex_levm::db::gen_db::GeneralizedDatabase;
50#[cfg(feature = "rayon")]
51use ethrex_levm::db::gen_db::{
52 LazyBalCursor, code_from_bal, post_value_at_or_before, seed_one_address_info_from_bal,
53};
54#[cfg(feature = "rayon")]
55use ethrex_levm::db::{Database, gen_db::CacheDB};
56use ethrex_levm::errors::{InternalError, TxValidationError};
57use ethrex_levm::memory::Memory;
58#[cfg(feature = "perf_opcode_timings")]
59use ethrex_levm::timings::{OPCODE_TIMINGS, PRECOMPILES_TIMINGS};
60use ethrex_levm::tracing::LevmCallTracer;
61use ethrex_levm::utils::get_base_fee_per_blob_gas;
62use ethrex_levm::validation_observer::FrameSimViolation;
63use ethrex_levm::vm::VMType;
64use ethrex_levm::{
65 Environment,
66 errors::{ExecutionReport, TxResult, VMError},
67 vm::VM,
68};
69#[cfg(feature = "rayon")]
70use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
71#[cfg(feature = "rayon")]
72use rustc_hash::{FxHashMap, FxHashSet};
73use std::cmp::min;
74use std::sync::Arc;
75#[cfg(feature = "rayon")]
76use std::sync::atomic::AtomicBool;
77use std::sync::atomic::{AtomicUsize, Ordering};
78use std::sync::mpsc::Sender;
79
80fn lstar_burned_fees(
88 chain_config: ðrex_common::types::ChainConfig,
89 header: ðrex_common::types::BlockHeader,
90 post_refund_gas: u64,
91) -> u64 {
92 let evm_cfg = EVMConfig::new_from_chain_config(chain_config, header);
93 let blob_base_fee = get_base_fee_per_blob_gas(header.excess_blob_gas, &evm_cfg)
94 .map(|v| u64::try_from(v).unwrap_or(u64::MAX))
95 .unwrap_or(0);
96 let blob_gas_used = header.blob_gas_used.unwrap_or(0);
97 compute_burned_fees(
98 header.base_fee_per_gas.unwrap_or(0),
99 post_refund_gas,
101 blob_base_fee,
102 blob_gas_used,
103 )
104}
105
106#[derive(Debug)]
112pub struct LEVM;
113
114fn frame_receipts_from(
118 frame_results: Option<Vec<(u8, u64, Vec<Log>)>>,
119) -> Option<Vec<FrameReceipt>> {
120 frame_results.map(|results| {
121 results
122 .into_iter()
123 .map(|(status, gas_used, logs)| FrameReceipt {
124 status,
125 gas_used,
126 logs,
127 })
128 .collect()
129 })
130}
131
132fn check_gas_limit(
134 block_gas_used: u64,
135 tx_gas_limit: u64,
136 block_gas_limit: u64,
137) -> Result<(), EvmError> {
138 if tx_gas_limit > block_gas_limit.saturating_sub(block_gas_used) {
139 return Err(EvmError::Transaction(format!(
140 "Gas allowance exceeded: \
141 used {block_gas_used} + tx limit {tx_gas_limit} > block limit {block_gas_limit}"
142 )));
143 }
144 Ok(())
145}
146
147pub fn check_2d_gas_allowance(
166 tx: &Transaction,
167 block_gas_used_regular: u64,
168 block_gas_used_state: u64,
169 block_gas_limit: u64,
170) -> Result<(), EvmError> {
171 let tx_gas = tx.gas_limit();
172 let regular_available = block_gas_limit.saturating_sub(block_gas_used_regular);
173 let state_available = block_gas_limit.saturating_sub(block_gas_used_state);
174
175 let regular_contrib = tx_gas.min(TX_MAX_GAS_LIMIT_AMSTERDAM);
180 if regular_contrib > regular_available {
181 return Err(EvmError::Transaction(format!(
182 "Gas allowance exceeded: regular dim worst-case {regular_contrib} > \
183 available {regular_available} (block_gas_used_regular={block_gas_used_regular}, \
184 block_gas_limit={block_gas_limit})"
185 )));
186 }
187
188 let state_contrib = tx_gas;
190 if state_contrib > state_available {
191 return Err(EvmError::Transaction(format!(
192 "Gas allowance exceeded: state dim worst-case {state_contrib} > \
193 available {state_available} (block_gas_used_state={block_gas_used_state}, \
194 block_gas_limit={block_gas_limit})"
195 )));
196 }
197
198 Ok(())
199}
200
201#[cfg(feature = "rayon")]
207#[derive(Debug, thiserror::Error)]
208pub enum BalValidationError {
209 #[error("{0}")]
210 Mismatch(String),
211 #[error("{0}")]
212 Database(String),
213}
214
215impl LEVM {
216 pub fn execute_block(
221 block: &Block,
222 db: &mut GeneralizedDatabase,
223 vm_type: VMType,
224 crypto: &dyn Crypto,
225 stateless_validator: Option<&dyn StatelessValidator>,
226 ) -> Result<(BlockExecutionResult, Option<BlockAccessList>), EvmError> {
227 let chain_config = db.store.get_chain_config()?;
228 let is_amsterdam = chain_config.is_amsterdam_activated(block.header.timestamp);
229 let is_lstar = chain_config.is_lstar_activated(block.header.timestamp);
231
232 debug_assert!(
237 block.body.transactions.len() < u32::MAX as usize,
238 "tx count overflows u32 BlockAccessIndex"
239 );
240
241 if is_amsterdam {
243 db.enable_bal_recording();
244 db.set_bal_index(0);
246 }
247
248 Self::prepare_block(block, db, vm_type, crypto)?;
249
250 let evm_config = EVMConfig::new_from_chain_config(&chain_config, &block.header);
254 let chain_id = chain_config.chain_id;
255 let base_blob_fee_per_gas =
256 get_base_fee_per_blob_gas(block.header.excess_blob_gas, &evm_config)?;
257 let mut shared_stack_pool = Vec::with_capacity(STACK_LIMIT);
259 let mut shared_memory_pool = Vec::with_capacity(1);
260
261 let n_txs = block.body.transactions.len();
262 let mut receipts = Vec::with_capacity(n_txs);
263 let mut tx_gas_breakdowns: Vec<TxGasBreakdown> = Vec::with_capacity(n_txs);
264 let mut cumulative_gas_used = 0_u64;
266 let mut block_gas_used = 0_u64;
268 let mut block_regular_gas_used = 0_u64;
270 let mut block_state_gas_used = 0_u64;
271 let transactions_with_sender =
272 block
273 .body
274 .get_transactions_with_sender(crypto)
275 .map_err(|error| {
276 EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
277 })?;
278
279 for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() {
280 if !is_amsterdam {
287 check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?;
288 }
289
290 if is_amsterdam {
292 check_2d_gas_allowance(
293 tx,
294 block_regular_gas_used,
295 block_state_gas_used,
296 block.header.gas_limit,
297 )?;
298 }
299
300 if is_amsterdam {
302 let bal_index = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
303 db.set_bal_index(bal_index);
304
305 if let Some(recorder) = db.bal_recorder_mut() {
309 recorder.record_touched_address(tx_sender);
310 }
311 }
312
313 let report = Self::execute_tx_in_block(
314 tx,
315 tx_sender,
316 &block.header,
317 db,
318 vm_type,
319 base_blob_fee_per_gas,
320 &mut shared_stack_pool,
321 &mut shared_memory_pool,
322 false,
323 crypto,
324 evm_config,
325 chain_id,
326 stateless_validator,
327 )?;
328
329 tx_gas_breakdowns.push(TxGasBreakdown::from_report(
330 tx_idx,
331 tx.hash(crypto),
332 &report,
333 ));
334
335 cumulative_gas_used += report.gas_spent;
337
338 let tx_state_gas = report.state_gas_used;
341 let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas);
342 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas);
343 block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas);
344
345 if is_amsterdam {
346 block_gas_used = block_regular_gas_used.max(block_state_gas_used);
348 ::tracing::debug!(
349 "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}",
350 report.gas_used,
351 report.gas_spent,
352 );
353
354 if block_regular_gas_used > block.header.gas_limit
359 || block_state_gas_used > block.header.gas_limit
360 {
361 return Err(EvmError::Transaction(format!(
362 "Gas allowance exceeded: Block gas used overflow: \
363 block_gas_used {block_gas_used} > block_gas_limit {}",
364 block.header.gas_limit
365 )));
366 }
367 } else {
368 block_gas_used = block_gas_used.saturating_add(report.gas_used);
369 }
370
371 let mut receipt = Receipt::new(
372 tx.tx_type(),
373 matches!(report.result, TxResult::Success),
374 cumulative_gas_used,
375 report.logs,
376 );
377
378 if matches!(tx, Transaction::FrameTransaction(_)) {
380 receipt.payer = report.payer_address;
381 receipt.frame_receipts = frame_receipts_from(report.frame_results);
382 }
383
384 receipts.push(receipt);
385 }
386
387 if is_amsterdam && block_gas_used > block.header.gas_limit {
391 return Err(EvmError::Transaction(format!(
392 "Gas allowance exceeded: Block gas used overflow: \
393 block_gas_used {block_gas_used} > block_gas_limit {}",
394 block.header.gas_limit
395 )));
396 }
397
398 if is_amsterdam {
401 let post_tx_index =
402 u32::try_from(block.body.transactions.len() + 1).unwrap_or(u32::MAX);
403 db.set_bal_index(post_tx_index);
404
405 if let Some(withdrawals) = &block.body.withdrawals
409 && let Some(recorder) = db.bal_recorder_mut()
410 {
411 recorder.extend_touched_addresses(withdrawals.iter().map(|w| w.address));
412 }
413 }
414
415 let requests = match vm_type {
419 VMType::L1 => extract_all_requests_levm(&receipts, db, &block.header, vm_type, crypto)?,
420 VMType::L2(_) => Default::default(),
421 };
422
423 if let Some(withdrawals) = &block.body.withdrawals {
424 Self::process_withdrawals(db, withdrawals)?;
425 }
426
427 let bal = db.take_bal();
429
430 Ok((
431 BlockExecutionResult {
432 receipts,
433 requests,
434 block_gas_used,
435 burned_fees: is_lstar
437 .then(|| lstar_burned_fees(&chain_config, &block.header, cumulative_gas_used)),
438 tx_gas_breakdowns,
439 },
440 bal,
441 ))
442 }
443
444 #[allow(clippy::too_many_arguments)]
448 pub fn execute_block_pipeline(
449 block: &Block,
450 db: &mut GeneralizedDatabase,
451 vm_type: VMType,
452 merkleizer: Option<Sender<Vec<AccountUpdate>>>,
453 queue_length: &AtomicUsize,
454 crypto: &dyn Crypto,
455 header_bal: Option<Arc<BlockAccessList>>,
456 bal_parallel_exec_enabled: bool,
457 stateless_validator: Option<&dyn StatelessValidator>,
458 ) -> Result<(BlockExecutionResult, Option<BlockAccessList>), EvmError> {
459 let chain_config = db.store.get_chain_config()?;
460 let is_amsterdam = chain_config.is_amsterdam_activated(block.header.timestamp);
461 let is_lstar = chain_config.is_lstar_activated(block.header.timestamp);
462 let evm_config = EVMConfig::new_from_chain_config(&chain_config, &block.header);
465 let chain_id = chain_config.chain_id;
466
467 debug_assert!(
469 block.body.transactions.len() < u32::MAX as usize,
470 "tx count overflows u32 BlockAccessIndex"
471 );
472
473 let transactions_with_sender =
474 block
475 .body
476 .get_transactions_with_sender(crypto)
477 .map_err(|error| {
478 EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
479 })?;
480
481 #[cfg(not(feature = "rayon"))]
482 let _ = (header_bal, bal_parallel_exec_enabled);
485 #[cfg(feature = "rayon")]
486 if let Some(bal) = header_bal
492 && is_amsterdam
493 && bal_parallel_exec_enabled
494 {
495 validate_header_bal_indices(&bal, block.body.transactions.len())
501 .map_err(|e| EvmError::Custom(e.to_string()))?;
502
503 Self::prepare_block(block, db, vm_type, crypto)?;
506
507 let validation_index = Arc::new(bal.build_validation_index());
509
510 Self::validate_bal_pre_exec_index(db, &bal, &validation_index)?;
517
518 LEVM::get_state_transitions_tx(db)?;
520 let system_seed = Arc::new(std::mem::take(&mut db.initial_accounts_state));
521
522 let parallel_result = Self::execute_block_parallel(
523 block,
524 &transactions_with_sender,
525 db,
526 vm_type,
527 Arc::clone(&bal),
528 merkleizer.as_ref(),
529 queue_length,
530 system_seed,
531 crypto,
532 Arc::clone(&validation_index),
533 stateless_validator,
534 );
535
536 let (
541 receipts,
542 block_gas_used,
543 mut unread_storage_reads,
544 mut unaccessed_pure_accounts,
545 tx_gas_breakdowns,
546 ) = match parallel_result {
547 Ok(result) => result,
548 Err(parallel_err) => {
549 let last_tx_idx =
550 u32::try_from(block.body.transactions.len()).unwrap_or(u32::MAX);
551 if Self::seed_db_from_bal(
552 db,
553 &bal,
554 last_tx_idx,
555 &validation_index.accounts_by_min_index,
556 )
557 .is_ok()
558 && let VMType::L1 = vm_type
559 && let Err(e @ EvmError::SystemContractCallFailed(_)) =
560 extract_all_requests_levm(&[], db, &block.header, vm_type, crypto)
561 {
562 return Err(e);
563 }
564 return Err(parallel_err);
565 }
566 };
567
568 let last_tx_idx = u32::try_from(block.body.transactions.len()).unwrap_or(u32::MAX);
573 Self::seed_db_from_bal(
575 db,
576 &bal,
577 last_tx_idx,
578 &validation_index.accounts_by_min_index,
579 )?;
580
581 let requests = match vm_type {
583 VMType::L1 => {
584 extract_all_requests_levm(&receipts, db, &block.header, vm_type, crypto)?
585 }
586 VMType::L2(_) => Default::default(),
587 };
588
589 if let Some(withdrawals) = &block.body.withdrawals {
590 Self::process_withdrawals(db, withdrawals)?;
591 }
592 let withdrawal_idx = u32::try_from(block.body.transactions.len())
600 .map(|n| n.saturating_add(1))
601 .unwrap_or(u32::MAX);
602 Self::validate_bal_withdrawal_index(db, &bal, withdrawal_idx, &validation_index)?;
603
604 if !unread_storage_reads.is_empty() {
606 for (addr, acct) in &db.current_accounts_state {
607 for key in acct.storage.keys() {
608 unread_storage_reads.remove(&(*addr, *key));
609 }
610 }
611 }
612
613 if !unaccessed_pure_accounts.is_empty() {
618 if let Some(withdrawals) = &block.body.withdrawals {
619 for w in withdrawals {
620 unaccessed_pure_accounts.remove(&w.address);
621 }
622 }
623 for addr in db.current_accounts_state.keys() {
624 if *addr == SYSTEM_ADDRESS {
628 continue;
629 }
630 unaccessed_pure_accounts.remove(addr);
631 }
632 }
633
634 if let Some((addr, key)) = unread_storage_reads.iter().next() {
636 let slot = ethrex_common::BigEndianHash::into_uint(key);
637 return Err(EvmError::Custom(format!(
638 "BAL validation failed: storage_read for account {addr:?} slot \
639 {slot} was never actually read during block execution"
640 )));
641 }
642
643 if let Some(addr) = unaccessed_pure_accounts.iter().next() {
645 return Err(EvmError::Custom(format!(
646 "BAL validation failed: account {addr:?} has no mutations \
647 and no storage reads but was never accessed during block execution"
648 )));
649 }
650
651 validate_block_access_list_size(&block.header, &chain_config, &bal)
654 .map_err(|e| EvmError::Custom(e.to_string()))?;
655
656 let post_refund_gas = receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
660 let burned_fees_par =
661 is_lstar.then(|| lstar_burned_fees(&chain_config, &block.header, post_refund_gas));
662
663 return Ok((
664 BlockExecutionResult {
665 receipts,
666 requests,
667 block_gas_used,
668 burned_fees: burned_fees_par,
669 tx_gas_breakdowns,
670 },
671 None,
672 ));
673 }
674
675 let Some(merkleizer) = merkleizer else {
681 return Err(EvmError::Custom(
682 "sequential execution path called without a merkleizer Sender".to_string(),
683 ));
684 };
685 if is_amsterdam {
686 db.enable_bal_recording();
687 db.set_bal_index(0);
689 }
690
691 Self::prepare_block(block, db, vm_type, crypto)?;
692
693 let base_blob_fee_per_gas =
695 get_base_fee_per_blob_gas(block.header.excess_blob_gas, &evm_config)?;
696
697 let mut shared_stack_pool = Vec::with_capacity(STACK_LIMIT);
698 let mut shared_memory_pool = Vec::with_capacity(1);
700
701 let n_txs = block.body.transactions.len();
702 let mut receipts = Vec::with_capacity(n_txs);
703 let mut tx_gas_breakdowns: Vec<TxGasBreakdown> = Vec::with_capacity(n_txs);
704 let mut cumulative_gas_used = 0_u64;
706 let mut block_gas_used = 0_u64;
708 let mut block_regular_gas_used = 0_u64;
710 let mut block_state_gas_used = 0_u64;
711 let mut tx_since_last_flush = 2;
714
715 for (tx_idx, (tx, tx_sender)) in transactions_with_sender.into_iter().enumerate() {
716 if !is_amsterdam {
723 check_gas_limit(cumulative_gas_used, tx.gas_limit(), block.header.gas_limit)?;
724 }
725
726 if is_amsterdam {
728 check_2d_gas_allowance(
729 tx,
730 block_regular_gas_used,
731 block_state_gas_used,
732 block.header.gas_limit,
733 )?;
734 }
735
736 if is_amsterdam {
738 let bal_index = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
739 db.set_bal_index(bal_index);
740
741 if let Some(recorder) = db.bal_recorder_mut() {
745 recorder.record_touched_address(tx_sender);
746 }
747 }
748
749 let report = Self::execute_tx_in_block(
750 tx,
751 tx_sender,
752 &block.header,
753 db,
754 vm_type,
755 base_blob_fee_per_gas,
756 &mut shared_stack_pool,
757 &mut shared_memory_pool,
758 false,
759 crypto,
760 evm_config,
761 chain_id,
762 stateless_validator,
763 )?;
764
765 tx_gas_breakdowns.push(TxGasBreakdown::from_report(
766 tx_idx,
767 tx.hash(crypto),
768 &report,
769 ));
770
771 if queue_length.load(Ordering::Relaxed) == 0 && tx_since_last_flush > 5 {
772 LEVM::send_state_transitions_tx(&merkleizer, db, queue_length)?;
773 tx_since_last_flush = 0;
774 } else {
775 tx_since_last_flush += 1;
776 }
777
778 cumulative_gas_used += report.gas_spent;
780
781 let tx_state_gas = report.state_gas_used;
784 let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas);
785 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas);
786 block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas);
787
788 if is_amsterdam {
789 block_gas_used = block_regular_gas_used.max(block_state_gas_used);
791
792 if block_regular_gas_used > block.header.gas_limit
797 || block_state_gas_used > block.header.gas_limit
798 {
799 return Err(EvmError::Transaction(format!(
800 "Gas allowance exceeded: Block gas used overflow: \
801 block_gas_used {block_gas_used} > block_gas_limit {}",
802 block.header.gas_limit
803 )));
804 }
805 } else {
806 block_gas_used = block_gas_used.saturating_add(report.gas_used);
807 }
808
809 let mut receipt = Receipt::new(
810 tx.tx_type(),
811 matches!(report.result, TxResult::Success),
812 cumulative_gas_used,
813 report.logs,
814 );
815
816 if matches!(tx, Transaction::FrameTransaction(_)) {
818 receipt.payer = report.payer_address;
819 receipt.frame_receipts = frame_receipts_from(report.frame_results);
820 }
821
822 receipts.push(receipt);
823 }
824
825 if is_amsterdam && block_gas_used > block.header.gas_limit {
829 return Err(EvmError::Transaction(format!(
830 "Gas allowance exceeded: Block gas used overflow: \
831 block_gas_used {block_gas_used} > block_gas_limit {}",
832 block.header.gas_limit
833 )));
834 }
835
836 #[cfg(feature = "perf_opcode_timings")]
837 {
838 let mut timings = OPCODE_TIMINGS.lock().expect("poison");
839 timings.inc_tx_count(receipts.len());
840 timings.inc_block_count();
841 ::tracing::info!("{}", timings.info_pretty());
842 let precompiles_timings = PRECOMPILES_TIMINGS.lock().expect("poison");
843 ::tracing::info!("{}", precompiles_timings.info_pretty());
844 }
845
846 if queue_length.load(Ordering::Relaxed) == 0 {
847 LEVM::send_state_transitions_tx(&merkleizer, db, queue_length)?;
848 }
849
850 if is_amsterdam {
853 let post_tx_index =
854 u32::try_from(block.body.transactions.len() + 1).unwrap_or(u32::MAX);
855 db.set_bal_index(post_tx_index);
856
857 if let Some(withdrawals) = &block.body.withdrawals
859 && let Some(recorder) = db.bal_recorder_mut()
860 {
861 recorder.extend_touched_addresses(withdrawals.iter().map(|w| w.address));
862 }
863 }
864
865 let requests = match vm_type {
869 VMType::L1 => extract_all_requests_levm(&receipts, db, &block.header, vm_type, crypto)?,
870 VMType::L2(_) => Default::default(),
871 };
872
873 if let Some(withdrawals) = &block.body.withdrawals {
874 Self::process_withdrawals(db, withdrawals)?;
875 }
876 LEVM::send_state_transitions_tx(&merkleizer, db, queue_length)?;
877
878 let bal = db.take_bal();
880
881 Ok((
882 BlockExecutionResult {
883 receipts,
884 requests,
885 block_gas_used,
886 burned_fees: is_lstar
888 .then(|| lstar_burned_fees(&chain_config, &block.header, cumulative_gas_used)),
889 tx_gas_breakdowns,
890 },
891 bal,
892 ))
893 }
894
895 #[cfg(feature = "rayon")]
900 fn bal_to_account_updates(
901 bal: &BlockAccessList,
902 store: &dyn Database,
903 ) -> Result<Vec<AccountUpdate>, EvmError> {
904 use ethrex_common::types::AccountInfo;
905
906 let mut updates = Vec::new();
907
908 let write_addrs: Vec<Address> = bal
910 .accounts()
911 .iter()
912 .filter(|ac| {
913 !ac.balance_changes.is_empty()
914 || !ac.nonce_changes.is_empty()
915 || !ac.code_changes.is_empty()
916 || !ac.storage_changes.is_empty()
917 })
918 .map(|ac| ac.address)
919 .collect();
920 let _ = store.prefetch_accounts(&write_addrs);
924
925 for acct_changes in bal.accounts() {
926 let addr = acct_changes.address;
927
928 let has_writes = !acct_changes.balance_changes.is_empty()
930 || !acct_changes.nonce_changes.is_empty()
931 || !acct_changes.code_changes.is_empty()
932 || !acct_changes.storage_changes.is_empty();
933 if !has_writes {
934 continue;
935 }
936
937 let prestate = store
939 .get_account_state(addr)
940 .map_err(|e| EvmError::Custom(format!("bal_to_account_updates: {e}")))?;
941
942 let balance = acct_changes
944 .balance_changes
945 .last()
946 .map(|c| c.post_balance)
947 .unwrap_or(prestate.balance);
948
949 let nonce = acct_changes
951 .nonce_changes
952 .last()
953 .map(|c| c.post_nonce)
954 .unwrap_or(prestate.nonce);
955
956 let (code_hash, code) = if let Some(c) = acct_changes.code_changes.last() {
958 code_from_bal(&c.new_code)
959 } else {
960 (prestate.code_hash, None)
961 };
962
963 let mut added_storage = FxHashMap::with_capacity_and_hasher(
965 acct_changes.storage_changes.len(),
966 Default::default(),
967 );
968 for slot_change in &acct_changes.storage_changes {
969 if let Some(last) = slot_change.slot_changes.last() {
970 let key = ethrex_common::utils::u256_to_h256(slot_change.slot);
971 added_storage.insert(key, last.post_value);
972 }
973 }
974
975 let post_empty = balance.is_zero() && nonce == 0 && code_hash == *EMPTY_KECCAK_HASH;
977 let pre_empty = prestate.balance.is_zero()
978 && prestate.nonce == 0
979 && prestate.code_hash == *EMPTY_KECCAK_HASH;
980 let removed = post_empty && !pre_empty;
981
982 let balance_changed = acct_changes
983 .balance_changes
984 .last()
985 .is_some_and(|c| c.post_balance != prestate.balance);
986 let nonce_changed = acct_changes
987 .nonce_changes
988 .last()
989 .is_some_and(|c| c.post_nonce != prestate.nonce);
990 let code_changed = acct_changes.code_changes.last().is_some();
991 let acc_info_updated = balance_changed || nonce_changed || code_changed;
992
993 if !removed && !acc_info_updated && added_storage.is_empty() {
994 continue;
995 }
996
997 let info = if acc_info_updated {
998 Some(AccountInfo {
999 code_hash,
1000 balance,
1001 nonce,
1002 })
1003 } else {
1004 None
1005 };
1006
1007 let update = AccountUpdate {
1008 address: addr,
1009 removed,
1010 info,
1011 code,
1012 added_storage,
1013 removed_storage: false,
1020 };
1021 updates.push(update);
1022 }
1023
1024 Ok(updates)
1025 }
1026
1027 #[cfg(feature = "rayon")]
1044 fn seed_db_from_bal(
1045 db: &mut GeneralizedDatabase,
1046 bal: &BlockAccessList,
1047 max_idx: u32,
1048 accounts_by_min_index: &[(u32, usize)],
1049 ) -> Result<(), EvmError> {
1050 let end = accounts_by_min_index.partition_point(|(min_idx, _)| *min_idx <= max_idx);
1051 let bal_accounts = bal.accounts();
1052 for &(_, acct_idx) in &accounts_by_min_index[..end] {
1053 seed_one_address_info_from_bal(db, bal, acct_idx, max_idx)
1054 .map_err(|e| EvmError::Custom(format!("seed_db_from_bal: {e}")))?;
1055
1056 let acct_changes = &bal_accounts[acct_idx];
1057 if acct_changes.storage_changes.is_empty() {
1058 continue;
1059 }
1060 let any_storage = acct_changes.storage_changes.iter().any(|sc| {
1061 sc.slot_changes
1062 .first()
1063 .is_some_and(|c| c.block_access_index <= max_idx)
1064 });
1065 if !any_storage {
1066 continue;
1067 }
1068 let addr = acct_changes.address;
1069 if !db.current_accounts_state.contains_key(&addr) {
1070 db.get_account(addr)
1071 .map_err(|e| EvmError::Custom(format!("seed storage: {e}")))?;
1072 }
1073 let acc = db
1074 .get_account_mut(addr)
1075 .map_err(|e| EvmError::Custom(format!("seed storage mut: {e}")))?;
1076 for sc in &acct_changes.storage_changes {
1077 if let Some(value) = post_value_at_or_before(sc, max_idx) {
1078 acc.storage
1079 .insert(ethrex_common::utils::u256_to_h256(sc.slot), value);
1080 }
1081 }
1082 }
1083 Ok(())
1084 }
1085
1086 #[cfg(feature = "rayon")]
1093 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1094 fn execute_block_parallel(
1095 block: &Block,
1096 txs_with_sender: &[(&Transaction, Address)],
1097 db: &mut GeneralizedDatabase,
1098 vm_type: VMType,
1099 bal: Arc<BlockAccessList>,
1100 merkleizer: Option<&Sender<Vec<AccountUpdate>>>,
1101 queue_length: &AtomicUsize,
1102 system_seed: Arc<CacheDB>,
1103 crypto: &dyn Crypto,
1104 validation_index: Arc<BalAddressIndex>,
1105 stateless_validator: Option<&dyn StatelessValidator>,
1106 ) -> Result<
1107 (
1108 Vec<Receipt>,
1109 u64,
1110 FxHashSet<(Address, H256)>,
1111 FxHashSet<Address>,
1112 Vec<TxGasBreakdown>,
1113 ),
1114 EvmError,
1115 > {
1116 let store = db.store.clone();
1117 let header = &block.header;
1118 let n_txs = txs_with_sender.len();
1119 let chain_config = store.get_chain_config()?;
1124 let is_amsterdam = chain_config.is_amsterdam_activated(header.timestamp);
1125 let evm_config = EVMConfig::new_from_chain_config(&chain_config, header);
1128 let chain_id = chain_config.chain_id;
1129 let base_blob_fee_per_gas = get_base_fee_per_blob_gas(header.excess_blob_gas, &evm_config)?;
1131 debug_assert!(
1132 is_amsterdam,
1133 "execute_block_parallel invoked on non-Amsterdam block"
1134 );
1135
1136 if let Some(merkleizer) = merkleizer {
1140 let account_updates = Self::bal_to_account_updates(&bal, store.as_ref())?;
1141 merkleizer
1142 .send(account_updates)
1143 .map_err(|e| EvmError::Custom(format!("merkleizer send failed: {e}")))?;
1144 queue_length.fetch_add(1, Ordering::Relaxed);
1145 }
1146
1147 let mut unread_storage_reads: FxHashSet<(Address, H256)> = FxHashSet::default();
1150 let mut unaccessed_pure_accounts: FxHashSet<Address> = FxHashSet::default();
1153 for acct in bal.accounts() {
1154 for &slot in &acct.storage_reads {
1155 let key = ethrex_common::utils::u256_to_h256(slot);
1156 unread_storage_reads.insert((acct.address, key));
1157 }
1158 let is_pure = acct.storage_changes.is_empty()
1159 && acct.storage_reads.is_empty()
1160 && acct.balance_changes.is_empty()
1161 && acct.nonce_changes.is_empty()
1162 && acct.code_changes.is_empty();
1163 if is_pure {
1164 unaccessed_pure_accounts.insert(acct.address);
1165 }
1166 }
1167
1168 for addr in system_seed.keys() {
1174 if *addr == SYSTEM_ADDRESS {
1175 continue;
1176 }
1177 unaccessed_pure_accounts.remove(addr);
1178 }
1179
1180 unread_storage_reads.retain(|(addr, key)| {
1182 !system_seed
1183 .get(addr)
1184 .is_some_and(|a| a.storage.contains_key(key))
1185 });
1186
1187 let arc_bal = bal;
1189 let arc_idx = validation_index;
1190
1191 type TxExecResult = (
1205 usize,
1206 TxType,
1207 ExecutionReport,
1208 FxHashSet<Address>, Vec<(Address, H256)>, Vec<Address>, Option<EvmError>, );
1213
1214 let exec_results: Result<Vec<TxExecResult>, EvmError> = (0..n_txs)
1215 .into_par_iter()
1216 .map(|tx_idx| -> Result<_, EvmError> {
1217 let (tx, sender) = &txs_with_sender[tx_idx];
1218 let mut tx_db = GeneralizedDatabase::new_with_shared_base_and_capacity(
1220 store.clone(),
1221 system_seed.clone(),
1222 32,
1223 );
1224 tx_db.lazy_bal = Some(LazyBalCursor {
1225 bal: arc_bal.clone(),
1226 bal_index: u32::try_from(tx_idx + 1).unwrap_or(u32::MAX),
1227 index: arc_idx.clone(),
1228 });
1229 let mut stack_pool = Vec::with_capacity(8);
1232 let mut memory_pool = Vec::with_capacity(1);
1234
1235 tx_db.accessed_accounts =
1239 Some(FxHashSet::with_capacity_and_hasher(16, Default::default()));
1240
1241 tx_db.enable_bal_recording();
1246 let bal_index = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
1247 tx_db.set_bal_index(bal_index);
1248 if let Some(recorder) = tx_db.bal_recorder_mut() {
1252 recorder.record_touched_address(*sender);
1253 }
1254
1255 let report = LEVM::execute_tx_in_block(
1256 tx,
1257 *sender,
1258 header,
1259 &mut tx_db,
1260 vm_type,
1261 base_blob_fee_per_gas,
1262 &mut stack_pool,
1263 &mut memory_pool,
1264 false,
1265 crypto,
1266 evm_config,
1267 chain_id,
1268 stateless_validator,
1269 )?;
1270
1271 let current_state = std::mem::take(&mut tx_db.current_accounts_state);
1272 let codes = std::mem::take(&mut tx_db.codes);
1273 let mut tracked = tx_db.accessed_accounts.take().unwrap_or_default();
1274 let (shadow_touched, shadow_reads, tx_initial) = tx_db
1281 .bal_recorder
1282 .take()
1283 .map(|mut r| {
1284 (
1285 r.take_touched_addresses(),
1286 r.take_storage_reads(),
1287 r.take_tx_initial_storage(),
1288 )
1289 })
1290 .unwrap_or_default();
1291 let pure_reads: FxHashSet<(Address, U256)> = shadow_reads
1300 .iter()
1301 .copied()
1302 .filter(|k| !tx_initial.contains_key(k))
1303 .collect();
1304
1305 let mut reads_satisfied: Vec<(Address, H256)> =
1318 Vec::with_capacity(current_state.len() * 4);
1319 let mut destroyed: Vec<Address> = Vec::new();
1323 for (addr, acct) in ¤t_state {
1324 if matches!(
1325 acct.status,
1326 AccountStatus::Destroyed | AccountStatus::DestroyedModified
1327 ) {
1328 destroyed.push(*addr);
1329 } else {
1330 for key in acct.storage.keys() {
1331 reads_satisfied.push((*addr, *key));
1332 }
1333 }
1334 }
1335 for (addr, slot) in &shadow_reads {
1343 if destroyed.contains(addr) {
1344 reads_satisfied.push((*addr, ethrex_common::utils::u256_to_h256(*slot)));
1345 }
1346 }
1347
1348 let deferred_bal_err: Option<EvmError> = (|| -> Result<(), EvmError> {
1354 let bal_idx = u32::try_from(tx_idx + 1).unwrap_or(u32::MAX);
1355 let seed_idx = u32::try_from(tx_idx).unwrap_or(u32::MAX);
1356 Self::validate_tx_execution(
1357 bal_idx,
1358 seed_idx,
1359 ¤t_state,
1360 &codes,
1361 &arc_bal,
1362 &arc_idx,
1363 &system_seed,
1364 &store,
1365 &pure_reads,
1366 &tx_initial,
1367 )
1368 .map_err(|e| match e {
1369 BalValidationError::Database(msg) => {
1373 EvmError::DB(format!("BAL validation for tx {tx_idx}: {msg}"))
1374 }
1375 BalValidationError::Mismatch(msg) => EvmError::Custom(format!(
1376 "BAL validation failed for tx {tx_idx}: {msg}"
1377 )),
1378 })?;
1379
1380 for addr in &shadow_touched {
1382 if !arc_idx.addr_to_idx.contains_key(addr) {
1383 return Err(EvmError::Custom(format!(
1384 "BAL validation failed for tx {tx_idx}: account {addr:?} was \
1385 accessed during execution but is missing from BAL"
1386 )));
1387 }
1388 }
1389 for (addr, slot) in &shadow_reads {
1390 let Some(&bal_acct_idx) = arc_idx.addr_to_idx.get(addr) else {
1391 continue;
1393 };
1394 let acct = &arc_bal.accounts()[bal_acct_idx];
1395 let in_changes = acct
1396 .storage_changes
1397 .binary_search_by(|sc| sc.slot.cmp(slot))
1398 .is_ok();
1399 debug_assert!(
1406 acct.storage_reads.windows(2).all(|w| w[0] < w[1]),
1407 "storage_reads must be strictly ascending for binary_search"
1408 );
1409 let in_reads = acct.storage_reads.binary_search(slot).is_ok();
1410 if !in_changes && !in_reads {
1411 return Err(EvmError::Custom(format!(
1412 "BAL validation failed for tx {tx_idx}: storage slot {slot} of \
1413 account {addr:?} was read during execution but is missing from \
1414 BAL (no storage_changes or storage_reads entry)"
1415 )));
1416 }
1417 }
1418 Ok(())
1419 })()
1420 .err();
1421
1422 drop(current_state);
1423 drop(codes);
1424
1425 tracked.extend(shadow_touched.iter().copied());
1434
1435 Ok((
1436 tx_idx,
1437 tx.tx_type(),
1438 report,
1439 tracked,
1440 reads_satisfied,
1441 destroyed,
1442 deferred_bal_err,
1443 ))
1444 })
1445 .collect();
1446
1447 let mut exec_results = exec_results?;
1448
1449 exec_results.sort_unstable_by_key(|(idx, _, _, _, _, _, _)| *idx);
1456
1457 let mut block_regular_gas_used = 0_u64;
1462 let mut block_state_gas_used = 0_u64;
1463 let mut tx_gas_breakdowns: Vec<TxGasBreakdown> = Vec::with_capacity(exec_results.len());
1464 for (tx_idx, _, report, _, _, _, _) in &exec_results {
1465 let (tx, _tx_sender) = txs_with_sender
1466 .get(*tx_idx)
1467 .ok_or_else(|| EvmError::Custom(format!("tx index {tx_idx} out of bounds")))?;
1468 if is_amsterdam {
1469 check_2d_gas_allowance(
1470 tx,
1471 block_regular_gas_used,
1472 block_state_gas_used,
1473 header.gas_limit,
1474 )?;
1475 }
1476
1477 tx_gas_breakdowns.push(TxGasBreakdown::from_report(
1478 *tx_idx,
1479 tx.hash(crypto),
1480 report,
1481 ));
1482
1483 let tx_state_gas = report.state_gas_used;
1484 let tx_regular_gas = report.gas_used.saturating_sub(tx_state_gas);
1485 block_regular_gas_used = block_regular_gas_used.saturating_add(tx_regular_gas);
1486 block_state_gas_used = block_state_gas_used.saturating_add(tx_state_gas);
1487 }
1488 let block_gas_used = block_regular_gas_used.max(block_state_gas_used);
1489 if block_gas_used > header.gas_limit {
1491 return Err(EvmError::Transaction(format!(
1492 "Gas allowance exceeded: Block gas used overflow: \
1493 block_gas_used {block_gas_used} > block_gas_limit {}",
1494 header.gas_limit
1495 )));
1496 }
1497
1498 for (_, _, _, _, _, _, deferred) in &mut exec_results {
1501 if let Some(err) = deferred.take() {
1502 return Err(err);
1503 }
1504 }
1505
1506 for (_, _, _, tracked_accounts, reads_satisfied, _, _) in &exec_results {
1515 if !unread_storage_reads.is_empty() {
1516 for pair in reads_satisfied {
1517 unread_storage_reads.remove(pair);
1518 }
1519 }
1520 if !unaccessed_pure_accounts.is_empty() {
1524 unaccessed_pure_accounts.remove(&header.coinbase);
1525 for addr in tracked_accounts {
1526 unaccessed_pure_accounts.remove(addr);
1527 }
1528 }
1529 }
1530
1531 let mut receipts = Vec::with_capacity(n_txs);
1533 let mut cumulative_gas_used = 0_u64;
1534 for (_, tx_type, report, _, _, _, _) in exec_results {
1535 cumulative_gas_used += report.gas_spent;
1536 let mut receipt = Receipt::new(
1537 tx_type,
1538 matches!(report.result, TxResult::Success),
1539 cumulative_gas_used,
1540 report.logs,
1541 );
1542 if tx_type == TxType::Frame {
1543 receipt.payer = report.payer_address;
1544 receipt.frame_receipts = frame_receipts_from(report.frame_results);
1545 }
1546 receipts.push(receipt);
1547 }
1548
1549 Ok((
1550 receipts,
1551 block_gas_used,
1552 unread_storage_reads,
1553 unaccessed_pure_accounts,
1554 tx_gas_breakdowns,
1555 ))
1556 }
1557
1558 #[cfg(feature = "rayon")]
1561 fn seeded_balance(
1562 seed_idx: u32,
1563 acct: ðrex_common::types::block_access_list::AccountChanges,
1564 system_seed: &CacheDB,
1565 store: &Arc<dyn Database>,
1566 ) -> Result<U256, BalValidationError> {
1567 let pos = acct
1568 .balance_changes
1569 .partition_point(|c| c.block_access_index <= seed_idx);
1570 if pos > 0 {
1571 Ok(acct.balance_changes[pos - 1].post_balance)
1572 } else if let Some(a) = system_seed.get(&acct.address) {
1573 Ok(a.info.balance)
1574 } else {
1575 store
1576 .get_account_state(acct.address)
1577 .map(|a| a.balance)
1578 .map_err(|e| {
1579 BalValidationError::Database(format!(
1580 "DB error reading balance for {:?}: {e}",
1581 acct.address
1582 ))
1583 })
1584 }
1585 }
1586
1587 #[cfg(feature = "rayon")]
1590 fn seeded_code_hash(
1591 seed_idx: u32,
1592 acct: ðrex_common::types::block_access_list::AccountChanges,
1593 system_seed: &CacheDB,
1594 store: &Arc<dyn Database>,
1595 ) -> Result<H256, BalValidationError> {
1596 let pos = acct
1597 .code_changes
1598 .partition_point(|c| c.block_access_index <= seed_idx);
1599 if pos > 0 {
1600 let seeded_code = &acct.code_changes[pos - 1].new_code;
1601 Ok(if seeded_code.is_empty() {
1602 *EMPTY_KECCAK_HASH
1603 } else {
1604 ethrex_common::utils::keccak(seeded_code)
1605 })
1606 } else if let Some(a) = system_seed.get(&acct.address) {
1607 Ok(a.info.code_hash)
1608 } else {
1609 store
1610 .get_account_state(acct.address)
1611 .map(|a| a.code_hash)
1612 .map_err(|e| {
1613 BalValidationError::Database(format!(
1614 "DB error reading account state for {:?}: {e}",
1615 acct.address
1616 ))
1617 })
1618 }
1619 }
1620
1621 #[cfg(feature = "rayon")]
1624 fn seeded_nonce(
1625 seed_idx: u32,
1626 acct: ðrex_common::types::block_access_list::AccountChanges,
1627 system_seed: &CacheDB,
1628 store: &Arc<dyn Database>,
1629 ) -> Result<u64, BalValidationError> {
1630 let pos = acct
1631 .nonce_changes
1632 .partition_point(|c| c.block_access_index <= seed_idx);
1633 if pos > 0 {
1634 Ok(acct.nonce_changes[pos - 1].post_nonce)
1635 } else if let Some(a) = system_seed.get(&acct.address) {
1636 Ok(a.info.nonce)
1637 } else {
1638 store
1639 .get_account_state(acct.address)
1640 .map(|a| a.nonce)
1641 .map_err(|e| {
1642 BalValidationError::Database(format!(
1643 "DB error reading nonce for {:?}: {e}",
1644 acct.address
1645 ))
1646 })
1647 }
1648 }
1649
1650 #[cfg(feature = "rayon")]
1658 fn seeded_account_triple(
1659 seed_idx: u32,
1660 acct: ðrex_common::types::block_access_list::AccountChanges,
1661 system_seed: &CacheDB,
1662 store: &Arc<dyn Database>,
1663 ) -> Result<(U256, u64, H256), BalValidationError> {
1664 let bpos = acct
1665 .balance_changes
1666 .partition_point(|c| c.block_access_index <= seed_idx);
1667 let npos = acct
1668 .nonce_changes
1669 .partition_point(|c| c.block_access_index <= seed_idx);
1670 let cpos = acct
1671 .code_changes
1672 .partition_point(|c| c.block_access_index <= seed_idx);
1673
1674 let mut balance = (bpos > 0).then(|| acct.balance_changes[bpos - 1].post_balance);
1675 let mut nonce = (npos > 0).then(|| acct.nonce_changes[npos - 1].post_nonce);
1676 let mut code_hash = (cpos > 0).then(|| {
1677 let c = &acct.code_changes[cpos - 1].new_code;
1678 if c.is_empty() {
1679 *EMPTY_KECCAK_HASH
1680 } else {
1681 ethrex_common::utils::keccak(c)
1682 }
1683 });
1684
1685 if let Some(a) = system_seed.get(&acct.address) {
1687 balance.get_or_insert(a.info.balance);
1688 nonce.get_or_insert(a.info.nonce);
1689 code_hash.get_or_insert(a.info.code_hash);
1690 }
1691
1692 if balance.is_none() || nonce.is_none() || code_hash.is_none() {
1694 let s = store.get_account_state(acct.address).map_err(|e| {
1695 BalValidationError::Database(format!(
1696 "DB error reading account state for {:?}: {e}",
1697 acct.address
1698 ))
1699 })?;
1700 balance.get_or_insert(s.balance);
1701 nonce.get_or_insert(s.nonce);
1702 code_hash.get_or_insert(s.code_hash);
1703 }
1704
1705 Ok((
1706 balance.unwrap_or_default(),
1707 nonce.unwrap_or_default(),
1708 code_hash.unwrap_or(*EMPTY_KECCAK_HASH),
1709 ))
1710 }
1711
1712 #[cfg(feature = "rayon")]
1717 fn seeded_storage_pre_value(
1718 addr: Address,
1719 key: H256,
1720 slot: U256,
1721 system_seed: &CacheDB,
1722 store: &Arc<dyn Database>,
1723 tx_initial: &FxHashMap<(Address, U256), U256>,
1724 ) -> Result<U256, BalValidationError> {
1725 if let Some(v) = tx_initial.get(&(addr, slot)) {
1726 return Ok(*v);
1727 }
1728 Self::storage_from_seed_or_store(addr, key, slot, system_seed, store)
1729 }
1730
1731 #[cfg(feature = "rayon")]
1734 fn storage_from_seed_or_store(
1735 addr: Address,
1736 key: H256,
1737 slot: U256,
1738 system_seed: &CacheDB,
1739 store: &Arc<dyn Database>,
1740 ) -> Result<U256, BalValidationError> {
1741 if let Some(v) = system_seed
1742 .get(&addr)
1743 .and_then(|a| a.storage.get(&key))
1744 .copied()
1745 {
1746 Ok(v)
1747 } else {
1748 store.get_storage_value(addr, key).map_err(|e| {
1749 BalValidationError::Database(format!(
1750 "DB error reading storage for {addr:?} slot {slot}: {e}"
1751 ))
1752 })
1753 }
1754 }
1755
1756 #[cfg(feature = "rayon")]
1763 fn seeded_storage(
1764 seed_idx: u32,
1765 sc: ðrex_common::types::block_access_list::SlotChange,
1766 addr: Address,
1767 key: H256,
1768 system_seed: &CacheDB,
1769 store: &Arc<dyn Database>,
1770 tx_initial: &FxHashMap<(Address, U256), U256>,
1771 ) -> Result<U256, BalValidationError> {
1772 if let Some(v) = tx_initial.get(&(addr, sc.slot)) {
1773 return Ok(*v);
1774 }
1775 let pos = sc
1776 .slot_changes
1777 .partition_point(|c| c.block_access_index <= seed_idx);
1778 if pos > 0 {
1779 Ok(sc.slot_changes[pos - 1].post_value)
1780 } else {
1781 Self::storage_from_seed_or_store(addr, key, sc.slot, system_seed, store)
1782 }
1783 }
1784
1785 #[cfg(feature = "rayon")]
1813 #[allow(clippy::too_many_arguments)]
1814 pub fn validate_tx_execution(
1815 bal_idx: u32,
1816 seed_idx: u32,
1817 current_state: &FxHashMap<Address, LevmAccount>,
1818 codes: &FxHashMap<H256, Code>,
1819 bal: &BlockAccessList,
1820 index: &BalAddressIndex,
1821 system_seed: &CacheDB,
1822 store: &Arc<dyn Database>,
1823 pure_reads: &FxHashSet<(Address, U256)>,
1824 tx_initial: &FxHashMap<(Address, U256), U256>,
1825 ) -> Result<(), BalValidationError> {
1826 if let Some(active_accounts) = index.tx_to_accounts.get(&bal_idx) {
1830 for &acct_inner_idx in active_accounts {
1831 let acct = &bal.accounts()[acct_inner_idx];
1832 let addr = acct.address;
1833 let actual = current_state.get(&addr);
1834 let mut seeded_info: Option<(U256, u64, H256)> = None;
1838
1839 if let Some(expected) = find_exact_change_balance(&acct.balance_changes, bal_idx) {
1841 match actual {
1842 Some(a) if a.info.balance == expected => {
1843 if seeded_info.is_none() {
1847 seeded_info = Some(Self::seeded_account_triple(
1848 seed_idx,
1849 acct,
1850 system_seed,
1851 store,
1852 )?);
1853 }
1854 let seeded = seeded_info.unwrap_or_default().0;
1855 if expected == seeded {
1856 return Err(BalValidationError::Mismatch(format!(
1857 "account {addr:?} has spurious no-op BAL balance change \
1858 at index {bal_idx}: post==pre=={expected}"
1859 )));
1860 }
1861 }
1862 Some(a) => {
1863 return Err(BalValidationError::Mismatch(format!(
1864 "account {addr:?} balance mismatch at index {bal_idx}: BAL={expected}, exec={} (diff={})",
1865 a.info.balance,
1866 describe_balance_diff(expected, a.info.balance),
1867 )));
1868 }
1869 None => {
1870 let seeded = Self::seeded_balance(seed_idx, acct, system_seed, store)?;
1877 if expected == seeded {
1878 return Err(BalValidationError::Mismatch(format!(
1879 "account {addr:?} has spurious no-op BAL balance change \
1880 at index {bal_idx} for an account absent from execution \
1881 state: post==pre=={expected}"
1882 )));
1883 }
1884 let all_bal_indices: Vec<u32> = acct
1886 .balance_changes
1887 .iter()
1888 .map(|c| c.block_access_index)
1889 .collect();
1890 let all_nonce_indices: Vec<u32> = acct
1891 .nonce_changes
1892 .iter()
1893 .map(|c| c.block_access_index)
1894 .collect();
1895 let all_storage_indices: Vec<(u32, u64)> = acct
1896 .storage_changes
1897 .iter()
1898 .flat_map(|sc| {
1899 sc.slot_changes
1900 .iter()
1901 .map(|c| (c.block_access_index, sc.slot.low_u64()))
1902 })
1903 .collect();
1904 let code_indices: Vec<u32> = acct
1905 .code_changes
1906 .iter()
1907 .map(|c| c.block_access_index)
1908 .collect();
1909 return Err(BalValidationError::Mismatch(format!(
1910 "account {addr:?} has BAL balance change at {bal_idx} \
1911 but not in execution state (expected={expected}, pre={seeded}, \
1912 all_bal_idx={all_bal_indices:?}, nonce_idx={all_nonce_indices:?}, \
1913 storage_idx={all_storage_indices:?}, code_idx={code_indices:?})"
1914 )));
1915 }
1916 }
1917 }
1918
1919 if let Some(expected) = find_exact_change_nonce(&acct.nonce_changes, bal_idx) {
1921 match actual {
1922 Some(a) if a.info.nonce == expected => {
1923 if seeded_info.is_none() {
1928 seeded_info = Some(Self::seeded_account_triple(
1929 seed_idx,
1930 acct,
1931 system_seed,
1932 store,
1933 )?);
1934 }
1935 let seeded = seeded_info.unwrap_or_default().1;
1936 if expected == seeded {
1937 return Err(BalValidationError::Mismatch(format!(
1938 "account {addr:?} has spurious no-op BAL nonce change \
1939 at index {bal_idx}: post==pre=={expected}"
1940 )));
1941 }
1942 }
1943 Some(a) => {
1944 return Err(BalValidationError::Mismatch(format!(
1945 "account {addr:?} nonce mismatch at index {bal_idx}: BAL={expected}, exec={}",
1946 a.info.nonce
1947 )));
1948 }
1949 None => {
1950 let seeded = Self::seeded_nonce(seed_idx, acct, system_seed, store)?;
1954 if expected == seeded {
1955 return Err(BalValidationError::Mismatch(format!(
1956 "account {addr:?} has spurious no-op BAL nonce change \
1957 at index {bal_idx} for an account absent from execution \
1958 state: post==pre=={expected}"
1959 )));
1960 }
1961 return Err(BalValidationError::Mismatch(format!(
1962 "account {addr:?} has BAL nonce change at {bal_idx} \
1963 but not in execution state (expected={expected}, pre={seeded})"
1964 )));
1965 }
1966 }
1967 }
1968
1969 if let Some(expected_code) = find_exact_change_code(&acct.code_changes, bal_idx) {
1971 match actual {
1972 Some(a) => {
1973 let actual_code = if let Some(c) = codes.get(&a.info.code_hash) {
1974 c.code_bytes()
1975 } else {
1976 let c = store.get_account_code(a.info.code_hash).map_err(|e| {
1977 BalValidationError::Database(format!(
1978 "DB error reading account code for {addr:?}: {e}"
1979 ))
1980 })?;
1981 c.code_bytes()
1982 };
1983 if actual_code != *expected_code {
1984 return Err(BalValidationError::Mismatch(format!(
1985 "account {addr:?} code mismatch at index {bal_idx}"
1986 )));
1987 }
1988 if seeded_info.is_none() {
1991 seeded_info = Some(Self::seeded_account_triple(
1992 seed_idx,
1993 acct,
1994 system_seed,
1995 store,
1996 )?);
1997 }
1998 let seeded_hash = seeded_info.unwrap_or_default().2;
1999 if a.info.code_hash == seeded_hash {
2000 return Err(BalValidationError::Mismatch(format!(
2001 "account {addr:?} has spurious no-op BAL code change \
2002 at index {bal_idx}"
2003 )));
2004 }
2005 }
2006 None => {
2007 let code_hash = if let Some(a) = system_seed.get(&addr) {
2011 a.info.code_hash
2012 } else {
2013 store
2014 .get_account_state(addr)
2015 .map(|a| a.code_hash)
2016 .map_err(|e| {
2017 BalValidationError::Database(format!(
2018 "DB error reading account state for {addr:?}: {e}"
2019 ))
2020 })?
2021 };
2022 let pre_code = if let Some(c) = codes.get(&code_hash) {
2023 c.code_bytes()
2024 } else {
2025 let c = store.get_account_code(code_hash).map_err(|e| {
2026 BalValidationError::Database(format!(
2027 "DB error reading account code for hash \
2028 {code_hash:?}: {e}"
2029 ))
2030 })?;
2031 c.code_bytes()
2032 };
2033 if *expected_code == pre_code {
2034 return Err(BalValidationError::Mismatch(format!(
2038 "account {addr:?} has spurious no-op BAL code change \
2039 at index {bal_idx} for an account absent from execution state"
2040 )));
2041 }
2042 return Err(BalValidationError::Mismatch(format!(
2043 "account {addr:?} has BAL code change at {bal_idx} \
2044 but not in execution state"
2045 )));
2046 }
2047 }
2048 }
2049
2050 for sc in &acct.storage_changes {
2052 if let Some(expected_value) =
2053 find_exact_change_storage(&sc.slot_changes, bal_idx)
2054 {
2055 let key = ethrex_common::utils::u256_to_h256(sc.slot);
2056 let actual_value = actual.and_then(|a| a.storage.get(&key)).copied();
2057 if actual_value != Some(expected_value) {
2058 if actual.is_none() || actual_value.is_none() {
2064 let seeded = Self::seeded_storage(
2065 seed_idx,
2066 sc,
2067 addr,
2068 key,
2069 system_seed,
2070 store,
2071 tx_initial,
2072 )?;
2073 if expected_value == seeded {
2074 return Err(BalValidationError::Mismatch(format!(
2075 "account {addr:?} has spurious no-op BAL storage \
2076 change for slot {} at index {bal_idx} for a slot \
2077 absent from execution state: post==pre=={expected_value}",
2078 sc.slot
2079 )));
2080 }
2081 }
2082 return Err(BalValidationError::Mismatch(format!(
2083 "account {addr:?} storage slot {} mismatch at index {bal_idx}: \
2084 BAL={expected_value}, exec={actual_value:?}",
2085 sc.slot
2086 )));
2087 }
2088 let seeded = Self::seeded_storage(
2092 seed_idx,
2093 sc,
2094 addr,
2095 key,
2096 system_seed,
2097 store,
2098 tx_initial,
2099 )?;
2100 if expected_value == seeded {
2101 return Err(BalValidationError::Mismatch(format!(
2102 "account {addr:?} has spurious no-op BAL storage change for \
2103 slot {} at index {bal_idx}",
2104 sc.slot
2105 )));
2106 }
2107 }
2108 }
2109 }
2110 }
2111
2112 for (addr, account) in current_state {
2115 if account.is_unmodified() {
2116 continue;
2117 }
2118
2119 let Some(&bal_acct_idx) = index.addr_to_idx.get(addr) else {
2120 let pre = system_seed
2124 .get(addr)
2125 .map(|a| (a.info.balance, a.info.nonce, a.info.code_hash))
2126 .or_else(|| {
2127 store
2128 .get_account_state(*addr)
2129 .ok()
2130 .map(|a| (a.balance, a.nonce, a.code_hash))
2131 })
2132 .unwrap_or_default();
2133 let post = (
2134 account.info.balance,
2135 account.info.nonce,
2136 account.info.code_hash,
2137 );
2138 if pre != post {
2139 return Err(BalValidationError::Mismatch(format!(
2140 "account {addr:?} was modified by execution but is absent from BAL"
2141 )));
2142 }
2143 continue;
2144 };
2145
2146 let acct = &bal.accounts()[bal_acct_idx];
2147
2148 if !has_exact_change_balance(&acct.balance_changes, bal_idx) {
2150 let seeded = Self::seeded_balance(seed_idx, acct, system_seed, store)?;
2151 if account.info.balance != seeded {
2152 return Err(BalValidationError::Mismatch(format!(
2153 "account {addr:?} balance changed by execution ({}) but BAL has no \
2154 balance change at index {bal_idx} (seeded={seeded})",
2155 account.info.balance
2156 )));
2157 }
2158 }
2159
2160 if !has_exact_change_nonce(&acct.nonce_changes, bal_idx) {
2162 let seeded = Self::seeded_nonce(seed_idx, acct, system_seed, store)?;
2163 if account.info.nonce != seeded {
2164 return Err(BalValidationError::Mismatch(format!(
2165 "account {addr:?} nonce changed by execution ({}) but BAL has no \
2166 nonce change at index {bal_idx} (seeded={seeded})",
2167 account.info.nonce
2168 )));
2169 }
2170 }
2171
2172 if !has_exact_change_code(&acct.code_changes, bal_idx) {
2176 let seeded_hash = Self::seeded_code_hash(seed_idx, acct, system_seed, store)?;
2177 if account.info.code_hash != seeded_hash {
2178 return Err(BalValidationError::Mismatch(format!(
2179 "account {addr:?} code changed by execution but BAL has no \
2180 code change at index {bal_idx} (seeded_hash={seeded_hash:?})"
2181 )));
2182 }
2183 }
2184
2185 for (key_h256, &value) in &account.storage {
2187 let slot_u256 = u256_from_big_endian_const(key_h256.0);
2188 let pos = acct
2190 .storage_changes
2191 .partition_point(|sc| sc.slot < slot_u256);
2192 if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
2193 let sc = &acct.storage_changes[pos];
2194 if !has_exact_change_storage(&sc.slot_changes, bal_idx) {
2195 let seeded = Self::seeded_storage(
2200 seed_idx,
2201 sc,
2202 *addr,
2203 *key_h256,
2204 system_seed,
2205 store,
2206 tx_initial,
2207 )?;
2208 if value != seeded {
2209 return Err(BalValidationError::Mismatch(format!(
2210 "account {addr:?} storage slot {slot_u256} changed by \
2211 execution ({value}) but BAL has no change at index \
2212 {bal_idx} (seeded={seeded})"
2213 )));
2214 }
2215 }
2216 }
2217 else if !pure_reads.contains(&(*addr, slot_u256)) {
2231 let pre_value = Self::seeded_storage_pre_value(
2236 *addr,
2237 *key_h256,
2238 slot_u256,
2239 system_seed,
2240 store,
2241 tx_initial,
2242 )?;
2243 if value != pre_value {
2244 let declared_as_read = acct.storage_reads.contains(&slot_u256);
2245 return Err(BalValidationError::Mismatch(format!(
2246 "account {addr:?} storage slot {slot_u256} was written by \
2247 execution ({value}) but has no storage_changes entry at index \
2248 {bal_idx} (pre_value={pre_value}, declared_as_read={declared_as_read})"
2249 )));
2250 }
2251 }
2252 }
2253 }
2254
2255 Ok(())
2256 }
2257
2258 #[cfg(feature = "rayon")]
2270 fn validate_bal_withdrawal_index(
2271 db: &GeneralizedDatabase,
2272 bal: &BlockAccessList,
2273 withdrawal_idx: u32,
2274 index: &BalAddressIndex,
2275 ) -> Result<(), EvmError> {
2276 for acct in bal.accounts() {
2279 let addr = acct.address;
2280 let actual = db.current_accounts_state.get(&addr);
2281
2282 if let Some(expected) = find_exact_change_balance(&acct.balance_changes, withdrawal_idx)
2284 {
2285 match actual {
2286 Some(a) if a.info.balance == expected => {}
2287 Some(a) => {
2288 return Err(EvmError::Custom(format!(
2289 "BAL validation failed for withdrawal: account {addr:?} balance \
2290 mismatch at index {withdrawal_idx}: BAL={expected}, actual={}",
2291 a.info.balance
2292 )));
2293 }
2294 None => {
2295 return Err(EvmError::Custom(format!(
2296 "BAL validation failed for withdrawal: account {addr:?} has \
2297 balance change at index {withdrawal_idx} but was not touched \
2298 by withdrawal/request phase"
2299 )));
2300 }
2301 }
2302 }
2303
2304 if let Some(expected) = find_exact_change_nonce(&acct.nonce_changes, withdrawal_idx) {
2306 match actual {
2307 Some(a) if a.info.nonce == expected => {}
2308 Some(a) => {
2309 return Err(EvmError::Custom(format!(
2310 "BAL validation failed for withdrawal: account {addr:?} nonce \
2311 mismatch at index {withdrawal_idx}: BAL={expected}, actual={}",
2312 a.info.nonce
2313 )));
2314 }
2315 None => {
2316 return Err(EvmError::Custom(format!(
2317 "BAL validation failed for withdrawal: account {addr:?} has \
2318 nonce change at index {withdrawal_idx} but was not touched \
2319 by withdrawal/request phase"
2320 )));
2321 }
2322 }
2323 }
2324
2325 if let Some(expected_code) = find_exact_change_code(&acct.code_changes, withdrawal_idx)
2327 {
2328 let code_hash = if expected_code.is_empty() {
2329 *EMPTY_KECCAK_HASH
2330 } else {
2331 ethrex_common::utils::keccak(expected_code)
2332 };
2333 match actual {
2334 Some(a) if a.info.code_hash == code_hash => {}
2335 Some(_) => {
2336 return Err(EvmError::Custom(format!(
2337 "BAL validation failed for withdrawal: account {addr:?} code \
2338 mismatch at index {withdrawal_idx}"
2339 )));
2340 }
2341 None => {
2342 return Err(EvmError::Custom(format!(
2343 "BAL validation failed for withdrawal: account {addr:?} has \
2344 code change at index {withdrawal_idx} but was not touched \
2345 by withdrawal/request phase"
2346 )));
2347 }
2348 }
2349 }
2350
2351 for sc in &acct.storage_changes {
2353 if let Some(expected_value) =
2354 find_exact_change_storage(&sc.slot_changes, withdrawal_idx)
2355 {
2356 let key = ethrex_common::utils::u256_to_h256(sc.slot);
2357 let actual_value = actual.and_then(|a| a.storage.get(&key)).copied();
2358 if actual_value != Some(expected_value) {
2359 return Err(EvmError::Custom(format!(
2360 "BAL validation failed for withdrawal: account {addr:?} storage \
2361 slot {} mismatch at index {withdrawal_idx}: BAL={expected_value}, \
2362 actual={actual_value:?}",
2363 sc.slot
2364 )));
2365 }
2366 }
2367 }
2368 }
2369
2370 for (addr, account) in &db.current_accounts_state {
2373 if account.is_unmodified() {
2374 continue;
2375 }
2376
2377 let Some(&bal_acct_idx) = index.addr_to_idx.get(addr) else {
2378 let pre_state = db.store.get_account_state(*addr).map_err(|e| {
2382 EvmError::Custom(format!(
2383 "BAL validation failed for withdrawal: db error reading \
2384 account {addr:?}: {e}"
2385 ))
2386 })?;
2387 let pre = (pre_state.balance, pre_state.nonce, pre_state.code_hash);
2388 let post = (
2389 account.info.balance,
2390 account.info.nonce,
2391 account.info.code_hash,
2392 );
2393 if pre != post {
2394 return Err(EvmError::Custom(format!(
2395 "BAL validation failed for withdrawal: account {addr:?} was modified \
2396 during withdrawal/request phase but is absent from BAL"
2397 )));
2398 }
2399 for (key_h256, &value) in &account.storage {
2402 let pre_value = db.store.get_storage_value(*addr, *key_h256).map_err(|e| {
2403 EvmError::Custom(format!(
2404 "BAL validation failed for withdrawal: db error reading \
2405 storage {addr:?}[{}]: {e}",
2406 u256_from_big_endian_const(key_h256.0)
2407 ))
2408 })?;
2409 if value != pre_value {
2410 return Err(EvmError::Custom(format!(
2411 "BAL validation failed for withdrawal: account {addr:?} storage \
2412 slot {} changed during withdrawal/request phase but is absent \
2413 from BAL",
2414 u256_from_big_endian_const(key_h256.0)
2415 )));
2416 }
2417 }
2418 continue;
2419 };
2420
2421 let acct = &bal.accounts()[bal_acct_idx];
2422
2423 if !has_exact_change_balance(&acct.balance_changes, withdrawal_idx) {
2426 let seeded = match acct.balance_changes.last() {
2427 Some(c) => c.post_balance,
2428 None => {
2429 db.store
2430 .get_account_state(*addr)
2431 .map_err(|e| {
2432 EvmError::Custom(format!(
2433 "BAL validation failed for withdrawal: db error reading \
2434 account {addr:?}: {e}"
2435 ))
2436 })?
2437 .balance
2438 }
2439 };
2440 if account.info.balance != seeded {
2441 return Err(EvmError::Custom(format!(
2442 "BAL validation failed for withdrawal: account {addr:?} balance \
2443 changed during withdrawal/request phase ({}) but BAL has no \
2444 balance change at index {withdrawal_idx} (last_bal={seeded})",
2445 account.info.balance
2446 )));
2447 }
2448 }
2449
2450 if !has_exact_change_nonce(&acct.nonce_changes, withdrawal_idx) {
2452 let seeded = match acct.nonce_changes.last() {
2453 Some(c) => c.post_nonce,
2454 None => {
2455 db.store
2456 .get_account_state(*addr)
2457 .map_err(|e| {
2458 EvmError::Custom(format!(
2459 "BAL validation failed for withdrawal: db error reading \
2460 account {addr:?}: {e}"
2461 ))
2462 })?
2463 .nonce
2464 }
2465 };
2466 if account.info.nonce != seeded {
2467 return Err(EvmError::Custom(format!(
2468 "BAL validation failed for withdrawal: account {addr:?} nonce \
2469 changed during withdrawal/request phase ({}) but BAL has no \
2470 nonce change at index {withdrawal_idx} (last_bal={seeded})",
2471 account.info.nonce
2472 )));
2473 }
2474 }
2475
2476 if !has_exact_change_code(&acct.code_changes, withdrawal_idx) {
2478 let seeded_hash = match acct.code_changes.last() {
2479 Some(c) if c.new_code.is_empty() => *EMPTY_KECCAK_HASH,
2480 Some(c) => ethrex_common::utils::keccak(&c.new_code),
2481 None => {
2482 db.store
2483 .get_account_state(*addr)
2484 .map_err(|e| {
2485 EvmError::Custom(format!(
2486 "BAL validation failed for withdrawal: db error reading \
2487 account {addr:?}: {e}"
2488 ))
2489 })?
2490 .code_hash
2491 }
2492 };
2493 if account.info.code_hash != seeded_hash {
2494 return Err(EvmError::Custom(format!(
2495 "BAL validation failed for withdrawal: account {addr:?} code \
2496 changed during withdrawal/request phase but BAL has no \
2497 code change at index {withdrawal_idx} \
2498 (actual={:?}, last_bal={seeded_hash:?})",
2499 account.info.code_hash
2500 )));
2501 }
2502 }
2503
2504 for (key_h256, &value) in &account.storage {
2507 let slot_u256 = u256_from_big_endian_const(key_h256.0);
2508 let pos = acct
2509 .storage_changes
2510 .partition_point(|sc| sc.slot < slot_u256);
2511 if pos < acct.storage_changes.len() && acct.storage_changes[pos].slot == slot_u256 {
2512 let sc = &acct.storage_changes[pos];
2513 if !has_exact_change_storage(&sc.slot_changes, withdrawal_idx) {
2514 let seeded = match sc.slot_changes.last() {
2517 Some(c) => c.post_value,
2518 None => db.store.get_storage_value(*addr, *key_h256).map_err(|e| {
2519 EvmError::Custom(format!(
2520 "BAL validation failed for withdrawal: db error reading \
2521 storage {addr:?}[{slot_u256}]: {e}"
2522 ))
2523 })?,
2524 };
2525 if value != seeded {
2526 return Err(EvmError::Custom(format!(
2527 "BAL validation failed for withdrawal: account {addr:?} \
2528 storage slot {slot_u256} changed during withdrawal/request \
2529 phase ({value}) but BAL has no change at index \
2530 {withdrawal_idx} (last_bal={seeded})"
2531 )));
2532 }
2533 }
2534 } else {
2535 let pre_value = db.store.get_storage_value(*addr, *key_h256).map_err(|e| {
2538 EvmError::Custom(format!(
2539 "BAL validation failed for withdrawal: db error reading \
2540 storage {addr:?}[{slot_u256}]: {e}"
2541 ))
2542 })?;
2543 if value != pre_value {
2544 return Err(EvmError::Custom(format!(
2545 "BAL validation failed for withdrawal: account {addr:?} \
2546 storage slot {slot_u256} changed during withdrawal/request \
2547 phase ({value}) but slot is absent from BAL storage_changes \
2548 (pre={pre_value})"
2549 )));
2550 }
2551 }
2552 }
2553 }
2554
2555 Ok(())
2556 }
2557
2558 #[cfg(feature = "rayon")]
2576 fn validate_bal_pre_exec_index(
2577 db: &GeneralizedDatabase,
2578 bal: &BlockAccessList,
2579 index: &BalAddressIndex,
2580 ) -> Result<(), EvmError> {
2581 const PRE_EXEC_IDX: u32 = 0;
2582 let Some(active_accounts) = index.tx_to_accounts.get(&PRE_EXEC_IDX) else {
2583 return Ok(());
2584 };
2585
2586 let pre_account = |addr: Address| -> Result<(U256, u64, H256), EvmError> {
2591 let s = db.store.get_account_state(addr).map_err(|e| {
2592 EvmError::Custom(format!(
2593 "BAL validation failed for pre-exec: db error reading account {addr:?}: {e}"
2594 ))
2595 })?;
2596 Ok((s.balance, s.nonce, s.code_hash))
2597 };
2598
2599 for &acct_inner_idx in active_accounts {
2600 let acct = &bal.accounts()[acct_inner_idx];
2601 let addr = acct.address;
2602 let actual = db.current_accounts_state.get(&addr);
2603
2604 if let Some(expected) = find_exact_change_balance(&acct.balance_changes, PRE_EXEC_IDX) {
2606 match actual {
2607 Some(a) if a.info.balance == expected => {
2608 if expected == pre_account(addr)?.0 {
2609 return Err(EvmError::Custom(format!(
2610 "BAL validation failed for pre-exec: account {addr:?} has spurious \
2611 no-op balance change at index 0: post==pre=={expected}"
2612 )));
2613 }
2614 }
2615 Some(a) => {
2616 return Err(EvmError::Custom(format!(
2617 "BAL validation failed for pre-exec: account {addr:?} balance mismatch \
2618 at index 0: BAL={expected}, actual={}",
2619 a.info.balance
2620 )));
2621 }
2622 None => {
2623 return Err(EvmError::Custom(format!(
2624 "BAL validation failed for pre-exec: account {addr:?} has balance \
2625 change at index 0 but was not touched by the pre-exec phase"
2626 )));
2627 }
2628 }
2629 }
2630
2631 if let Some(expected) = find_exact_change_nonce(&acct.nonce_changes, PRE_EXEC_IDX) {
2633 match actual {
2634 Some(a) if a.info.nonce == expected => {
2635 if expected == pre_account(addr)?.1 {
2636 return Err(EvmError::Custom(format!(
2637 "BAL validation failed for pre-exec: account {addr:?} has spurious \
2638 no-op nonce change at index 0: post==pre=={expected}"
2639 )));
2640 }
2641 }
2642 Some(a) => {
2643 return Err(EvmError::Custom(format!(
2644 "BAL validation failed for pre-exec: account {addr:?} nonce mismatch \
2645 at index 0: BAL={expected}, actual={}",
2646 a.info.nonce
2647 )));
2648 }
2649 None => {
2650 return Err(EvmError::Custom(format!(
2651 "BAL validation failed for pre-exec: account {addr:?} has nonce \
2652 change at index 0 but was not touched by the pre-exec phase"
2653 )));
2654 }
2655 }
2656 }
2657
2658 if let Some(expected_code) = find_exact_change_code(&acct.code_changes, PRE_EXEC_IDX) {
2660 let expected_hash = if expected_code.is_empty() {
2661 *EMPTY_KECCAK_HASH
2662 } else {
2663 ethrex_common::utils::keccak(expected_code)
2664 };
2665 match actual {
2666 Some(a) if a.info.code_hash == expected_hash => {
2667 if expected_hash == pre_account(addr)?.2 {
2668 return Err(EvmError::Custom(format!(
2669 "BAL validation failed for pre-exec: account {addr:?} has spurious \
2670 no-op code change at index 0"
2671 )));
2672 }
2673 }
2674 Some(_) => {
2675 return Err(EvmError::Custom(format!(
2676 "BAL validation failed for pre-exec: account {addr:?} code mismatch \
2677 at index 0"
2678 )));
2679 }
2680 None => {
2681 return Err(EvmError::Custom(format!(
2682 "BAL validation failed for pre-exec: account {addr:?} has code \
2683 change at index 0 but was not touched by the pre-exec phase"
2684 )));
2685 }
2686 }
2687 }
2688
2689 for sc in &acct.storage_changes {
2691 let Some(expected_value) =
2692 find_exact_change_storage(&sc.slot_changes, PRE_EXEC_IDX)
2693 else {
2694 continue;
2695 };
2696 let key = ethrex_common::utils::u256_to_h256(sc.slot);
2697 let actual_value = actual.and_then(|a| a.storage.get(&key)).copied();
2698 if actual_value != Some(expected_value) {
2699 return Err(EvmError::Custom(format!(
2700 "BAL validation failed for pre-exec: account {addr:?} storage slot {} \
2701 mismatch at index 0: BAL={expected_value}, actual={actual_value:?}",
2702 sc.slot
2703 )));
2704 }
2705 let pre_value = db.store.get_storage_value(addr, key).map_err(|e| {
2706 EvmError::Custom(format!(
2707 "BAL validation failed for pre-exec: db error reading storage {addr:?} \
2708 slot {}: {e}",
2709 sc.slot
2710 ))
2711 })?;
2712 if expected_value == pre_value {
2713 return Err(EvmError::Custom(format!(
2714 "BAL validation failed for pre-exec: account {addr:?} has spurious no-op \
2715 storage change for slot {} at index 0: post==pre=={expected_value}",
2716 sc.slot
2717 )));
2718 }
2719 }
2720 }
2721
2722 Ok(())
2723 }
2724
2725 #[cfg(feature = "rayon")]
2731 pub fn warm_block(
2732 block: &Block,
2733 store: Arc<dyn Database>,
2734 vm_type: VMType,
2735 crypto: &dyn Crypto,
2736 cancelled: &AtomicBool,
2737 ) -> Result<(), EvmError> {
2738 let txs_with_sender = block
2739 .body
2740 .get_transactions_with_sender(crypto)
2741 .map_err(|error| {
2742 EvmError::Transaction(format!("Couldn't recover addresses with error: {error}"))
2743 })?;
2744
2745 Self::warm_txs(
2746 &txs_with_sender,
2747 &block.header,
2748 store.clone(),
2749 vm_type,
2750 crypto,
2751 &|| cancelled.load(Ordering::Relaxed),
2752 )?;
2753
2754 let mut db = GeneralizedDatabase::new(store.clone());
2755
2756 if cancelled.load(Ordering::Relaxed) {
2757 return Ok(());
2758 }
2759
2760 for withdrawal in block
2761 .body
2762 .withdrawals
2763 .iter()
2764 .flatten()
2765 .filter(|withdrawal| withdrawal.amount > 0)
2766 {
2767 db.get_account_mut(withdrawal.address).map_err(|_| {
2768 EvmError::DB(format!(
2769 "Withdrawal account {} not found",
2770 withdrawal.address
2771 ))
2772 })?;
2773 }
2774 Ok(())
2775 }
2776
2777 #[cfg(feature = "rayon")]
2785 pub fn warm_txs(
2786 txs_with_sender: &[(&Transaction, Address)],
2787 header: &BlockHeader,
2788 store: Arc<dyn Database>,
2789 vm_type: VMType,
2790 crypto: &dyn Crypto,
2791 should_stop: &(dyn Fn() -> bool + Sync),
2792 ) -> Result<(), EvmError> {
2793 let mut sender_groups: FxHashMap<Address, Vec<&Transaction>> = FxHashMap::default();
2796 for (tx, sender) in txs_with_sender {
2797 sender_groups.entry(*sender).or_default().push(tx);
2798 }
2799
2800 let chain_config = store.get_chain_config()?;
2803 let evm_config = EVMConfig::new_from_chain_config(&chain_config, header);
2804 let chain_id = chain_config.chain_id;
2805 let base_blob_fee_per_gas = get_base_fee_per_blob_gas(header.excess_blob_gas, &evm_config)?;
2807
2808 sender_groups.into_par_iter().for_each_with(
2811 Vec::with_capacity(STACK_LIMIT),
2812 |stack_pool, (sender, txs)| {
2813 if should_stop() {
2814 return;
2815 }
2816 let mut memory_pool = Vec::with_capacity(1);
2820 let mut group_db = GeneralizedDatabase::new(store.clone());
2822 for tx in txs {
2823 if should_stop() {
2824 return;
2825 }
2826 let _ = Self::execute_tx_in_block(
2827 tx,
2828 sender,
2829 header,
2830 &mut group_db,
2831 vm_type,
2832 base_blob_fee_per_gas,
2833 stack_pool,
2834 &mut memory_pool,
2835 true,
2836 crypto,
2837 evm_config,
2838 chain_id,
2839 None,
2842 );
2843 }
2844 },
2845 );
2846 Ok(())
2847 }
2848
2849 #[cfg(feature = "rayon")]
2852 pub fn bal_storage_slots(bal: &BlockAccessList) -> Vec<(Address, H256)> {
2853 bal.accounts()
2854 .iter()
2855 .flat_map(|ac| {
2856 ac.all_storage_slots()
2857 .map(move |slot| (ac.address, H256::from_uint(&slot)))
2858 })
2859 .collect()
2860 }
2861
2862 #[cfg(feature = "rayon")]
2871 pub fn warm_block_from_bal(
2872 bal: &BlockAccessList,
2873 store: Arc<dyn Database>,
2874 cancelled: &AtomicBool,
2875 ) -> Result<(), EvmError> {
2876 let accounts = bal.accounts();
2877 if accounts.is_empty() {
2878 return Ok(());
2879 }
2880
2881 let account_addresses: Vec<Address> = accounts.iter().map(|ac| ac.address).collect();
2887 store
2888 .prefetch_accounts(&account_addresses)
2889 .map_err(|e| EvmError::Custom(format!("prefetch_accounts: {e}")))?;
2890
2891 if cancelled.load(Ordering::Relaxed) {
2892 return Ok(());
2893 }
2894
2895 let code_hashes: Vec<ethrex_common::H256> = accounts
2899 .par_iter()
2900 .filter_map(|ac| {
2901 store
2902 .get_account_state(ac.address)
2903 .ok()
2904 .filter(|s| s.code_hash != *EMPTY_KECCAK_HASH)
2905 .map(|s| s.code_hash)
2906 })
2907 .collect();
2908 code_hashes.par_iter().for_each(|&h| {
2909 let _ = store.get_account_code(h);
2910 });
2911
2912 Ok(())
2913 }
2914
2915 fn send_state_transitions_tx(
2916 merkleizer: &Sender<Vec<AccountUpdate>>,
2917 db: &mut GeneralizedDatabase,
2918 queue_length: &AtomicUsize,
2919 ) -> Result<(), EvmError> {
2920 let transitions = LEVM::get_state_transitions_tx(db)?;
2921 merkleizer
2922 .send(transitions)
2923 .map_err(|e| EvmError::Custom(format!("send failed: {e}")))?;
2924 queue_length.fetch_add(1, Ordering::Relaxed);
2925 Ok(())
2926 }
2927
2928 fn setup_env(
2929 tx: &Transaction,
2930 tx_sender: Address,
2931 block_header: &BlockHeader,
2932 db: &GeneralizedDatabase,
2933 vm_type: VMType,
2934 ) -> Result<Environment, EvmError> {
2935 let chain_config = db.store.get_chain_config()?;
2939 let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
2940 let base_blob_fee_per_gas =
2941 get_base_fee_per_blob_gas(block_header.excess_blob_gas, &config)?;
2942 Self::setup_env_with_config(
2943 tx,
2944 tx_sender,
2945 block_header,
2946 config,
2947 chain_config.chain_id,
2948 vm_type,
2949 base_blob_fee_per_gas,
2950 )
2951 }
2952
2953 fn setup_env_with_config(
2957 tx: &Transaction,
2958 tx_sender: Address,
2959 block_header: &BlockHeader,
2960 config: EVMConfig,
2961 chain_id: u64,
2962 vm_type: VMType,
2963 base_blob_fee_per_gas: U256,
2964 ) -> Result<Environment, EvmError> {
2965 let gas_price: U256 = calculate_gas_price_for_tx(
2966 tx,
2967 block_header.base_fee_per_gas.unwrap_or_default(),
2968 &vm_type,
2969 )?;
2970
2971 let block_excess_blob_gas = block_header.excess_blob_gas;
2972 let env = Environment {
2973 origin: tx_sender,
2974 gas_limit: tx.gas_limit(),
2975 config,
2976 block_number: block_header.number,
2977 coinbase: block_header.coinbase,
2978 timestamp: block_header.timestamp,
2979 prev_randao: Some(block_header.prev_randao),
2980 slot_number: block_header
2981 .slot_number
2982 .map(U256::from)
2983 .unwrap_or(U256::zero()),
2984 chain_id: chain_id.into(),
2985 base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(),
2986 base_blob_fee_per_gas,
2987 gas_price,
2988 block_excess_blob_gas,
2989 block_blob_gas_used: block_header.blob_gas_used,
2990 tx_blob_hashes: tx.blob_versioned_hashes(),
2991 tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from),
2992 tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from),
2993 tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas(),
2994 tx_nonce: tx.nonce(),
2995 block_gas_limit: block_header.gas_limit,
2996 difficulty: block_header.difficulty,
2997 is_privileged: matches!(tx, Transaction::PrivilegedL2Transaction(_)),
2998 fee_token: tx.fee_token(),
2999 disable_balance_check: false,
3000 disable_nonce_check: false,
3001 disable_gas_allowance_check: false,
3002 disable_sender_eoa_check: false,
3003 is_system_call: false,
3004 };
3005
3006 Ok(env)
3007 }
3008
3009 pub fn execute_tx(
3010 tx: &Transaction,
3012 tx_sender: Address,
3014 block_header: &BlockHeader,
3016 db: &mut GeneralizedDatabase,
3017 vm_type: VMType,
3018 crypto: &dyn Crypto,
3019 stateless_validator: Option<&dyn StatelessValidator>,
3020 ) -> Result<ExecutionReport, EvmError> {
3021 let env = Self::setup_env(tx, tx_sender, block_header, db, vm_type)?;
3022 let mut vm = VM::new(
3023 env,
3024 db,
3025 tx,
3026 LevmCallTracer::disabled(),
3027 vm_type,
3028 crypto,
3029 stateless_validator,
3030 )?;
3031
3032 vm.execute().map_err(VMError::into)
3033 }
3034
3035 #[allow(clippy::too_many_arguments)]
3038 fn execute_tx_in_block(
3039 tx: &Transaction,
3041 tx_sender: Address,
3043 block_header: &BlockHeader,
3045 db: &mut GeneralizedDatabase,
3046 vm_type: VMType,
3047 base_blob_fee_per_gas: U256,
3048 stack_pool: &mut Vec<Stack>,
3049 memory_pool: &mut Vec<Memory>,
3050 disable_balance_check: bool,
3051 crypto: &dyn Crypto,
3052 config: EVMConfig,
3053 chain_id: u64,
3054 stateless_validator: Option<&dyn StatelessValidator>,
3055 ) -> Result<ExecutionReport, EvmError> {
3056 let mut env = Self::setup_env_with_config(
3057 tx,
3058 tx_sender,
3059 block_header,
3060 config,
3061 chain_id,
3062 vm_type,
3063 base_blob_fee_per_gas,
3064 )?;
3065 env.disable_balance_check = disable_balance_check;
3066 let mut vm = VM::new_pooled(
3070 env,
3071 db,
3072 tx,
3073 LevmCallTracer::disabled(),
3074 vm_type,
3075 crypto,
3076 stateless_validator,
3077 stack_pool,
3078 memory_pool,
3079 )?;
3080 let result = vm.execute().map_err(VMError::into);
3081 vm.reclaim_into(stack_pool, memory_pool);
3083 result
3084 }
3085
3086 pub fn undo_last_tx(db: &mut GeneralizedDatabase) -> Result<(), EvmError> {
3087 db.undo_last_transaction()?;
3088 Ok(())
3089 }
3090
3091 pub fn simulate_tx_from_generic(
3092 tx: &GenericTransaction,
3094 block_header: &BlockHeader,
3096 db: &mut GeneralizedDatabase,
3097 vm_type: VMType,
3098 crypto: &dyn Crypto,
3099 stateless_validator: Option<&dyn StatelessValidator>,
3100 ) -> Result<ExecutionResult, EvmError> {
3101 let mut env = env_from_generic(tx, block_header, db, vm_type)?;
3106
3107 adjust_disabled_base_fee(&mut env);
3108
3109 let converted_tx = generic_tx_to_transaction(tx)?;
3110 let mut vm = vm_from_generic(&converted_tx, env, db, vm_type, crypto, stateless_validator)?;
3111
3112 vm.execute()
3113 .map(|value| value.into())
3114 .map_err(VMError::into)
3115 }
3116
3117 #[allow(clippy::too_many_arguments)]
3132 pub fn simulate_frame_validation_prefix(
3133 tx: &Transaction,
3134 block_header: &BlockHeader,
3135 db: &mut GeneralizedDatabase,
3136 vm_type: VMType,
3137 crypto: &dyn Crypto,
3138 prefix: ðrex_common::types::ValidationPrefix,
3139 _canonical_paymaster_code_hash: Option<H256>,
3140 ) -> Result<FrameValidationOutcome, EvmError> {
3141 use ethrex_common::types::FRAME_TX_MAX_VERIFY_GAS;
3142
3143 let frame_tx = match tx {
3144 Transaction::FrameTransaction(ft) => ft,
3145 _ => {
3146 return Err(EvmError::Custom(
3147 "simulate_frame_validation_prefix requires a frame transaction".to_string(),
3148 ));
3149 }
3150 };
3151 let sender = frame_tx.sender;
3152
3153 let env = Self::setup_env(tx, sender, block_header, db, vm_type)?;
3154 let mut vm = VM::new(
3155 env,
3156 db,
3157 tx,
3158 LevmCallTracer::disabled(),
3159 vm_type,
3160 crypto,
3161 None,
3162 )?;
3163
3164 let canonical_pay_frame: Option<usize> = None;
3167
3168 let sim = match vm.run_frame_validation_prefix(
3169 &prefix.frame_indices,
3170 prefix.deploy_index,
3171 canonical_pay_frame,
3172 ) {
3173 Ok(sim) => sim,
3174 Err(err) => {
3175 return Ok(FrameValidationOutcome {
3179 passed: false,
3180 violation: Some(EvmError::from(err).to_string()),
3181 reservation_ceiling: Self::frame_tx_reservation_ceiling(frame_tx),
3182 accessed_paymaster: None,
3183 touched_sender_slots: Vec::new(),
3184 });
3185 }
3186 };
3187
3188 let reservation_ceiling = Self::frame_tx_reservation_ceiling(frame_tx);
3189 let touched_sender_slots = vm.validation_observer.touched_sender_slots.clone();
3190 let accessed_paymaster = sim.payer_address.map(|payer| (payer, false));
3197
3198 if let Some(violation) = &vm.validation_observer.violation {
3200 return Ok(FrameValidationOutcome {
3201 passed: false,
3202 violation: Some(format!("{violation:?}")),
3203 reservation_ceiling,
3204 accessed_paymaster,
3205 touched_sender_slots,
3206 });
3207 }
3208
3209 if sim.any_revert {
3211 return Ok(FrameValidationOutcome {
3212 passed: false,
3213 violation: Some("validation prefix frame reverted".to_string()),
3214 reservation_ceiling,
3215 accessed_paymaster,
3216 touched_sender_slots,
3217 });
3218 }
3219
3220 if sim.payer_address.is_none() {
3223 return Ok(FrameValidationOutcome {
3224 passed: false,
3225 violation: Some("validation prefix did not establish a payer".to_string()),
3226 reservation_ceiling,
3227 accessed_paymaster,
3228 touched_sender_slots,
3229 });
3230 }
3231
3232 if prefix.deploy_index.is_some() {
3234 let code = vm.db.get_account_code(sender).map_err(VMError::from)?;
3235 if code.is_empty() {
3236 return Ok(FrameValidationOutcome {
3237 passed: false,
3238 violation: Some(format!("{:?}", FrameSimViolation::DeployInstalledNoCode)),
3239 reservation_ceiling,
3240 accessed_paymaster,
3241 touched_sender_slots,
3242 });
3243 }
3244 }
3245
3246 if sim.total_gas_used > FRAME_TX_MAX_VERIFY_GAS {
3248 return Ok(FrameValidationOutcome {
3249 passed: false,
3250 violation: Some(format!(
3251 "validation prefix gas {} exceeds MAX_VERIFY_GAS {}",
3252 sim.total_gas_used, FRAME_TX_MAX_VERIFY_GAS
3253 )),
3254 reservation_ceiling,
3255 accessed_paymaster,
3256 touched_sender_slots,
3257 });
3258 }
3259
3260 Ok(FrameValidationOutcome {
3261 passed: true,
3262 violation: None,
3263 reservation_ceiling,
3264 accessed_paymaster,
3265 touched_sender_slots,
3266 })
3267 }
3268
3269 fn frame_tx_reservation_ceiling(frame_tx: ðrex_common::types::FrameTransaction) -> U256 {
3288 let gas_cost =
3289 U256::from(frame_tx.max_fee_per_gas).saturating_mul(U256::from(frame_tx.max_gas()));
3290 let blob_cost = U256::from(frame_tx.blob_versioned_hashes.len())
3291 .saturating_mul(U256::from(131072u64))
3292 .saturating_mul(frame_tx.max_fee_per_blob_gas);
3293 gas_cost.saturating_add(blob_cost)
3294 }
3295
3296 pub fn get_state_transitions(
3297 db: &mut GeneralizedDatabase,
3298 ) -> Result<Vec<AccountUpdate>, EvmError> {
3299 Ok(db.get_state_transitions()?)
3300 }
3301
3302 pub fn get_state_transitions_tx(
3303 db: &mut GeneralizedDatabase,
3304 ) -> Result<Vec<AccountUpdate>, EvmError> {
3305 Ok(db.get_state_transitions_tx()?)
3306 }
3307
3308 pub fn process_withdrawals(
3309 db: &mut GeneralizedDatabase,
3310 withdrawals: &[Withdrawal],
3311 ) -> Result<(), EvmError> {
3312 for (address, increment) in withdrawals
3314 .iter()
3315 .filter(|withdrawal| withdrawal.amount > 0)
3316 .map(|w| (w.address, u128::from(w.amount) * u128::from(GWEI_TO_WEI)))
3317 {
3318 let account = db
3319 .get_account_mut(address)
3320 .map_err(|_| EvmError::DB(format!("Withdrawal account {address} not found")))?;
3321
3322 let initial_balance = account.info.balance;
3323 account.info.balance += increment.into();
3324 let new_balance = account.info.balance;
3325
3326 if let Some(recorder) = db.bal_recorder_mut() {
3328 recorder.set_initial_balance(address, initial_balance);
3329 recorder.record_balance_change(address, new_balance);
3330 }
3331 }
3332 Ok(())
3333 }
3334
3335 pub fn beacon_root_contract_call(
3337 block_header: &BlockHeader,
3338 db: &mut GeneralizedDatabase,
3339 vm_type: VMType,
3340 crypto: &dyn Crypto,
3341 ) -> Result<(), EvmError> {
3342 if let VMType::L2(_) = vm_type {
3343 return Err(EvmError::InvalidEVM(
3344 "beacon_root_contract_call should not be called for L2 VM".to_string(),
3345 ));
3346 }
3347
3348 let beacon_root = block_header.parent_beacon_block_root.ok_or_else(|| {
3349 EvmError::Header("parent_beacon_block_root field is missing".to_string())
3350 })?;
3351
3352 generic_system_contract_levm(
3353 block_header,
3354 Bytes::copy_from_slice(beacon_root.as_bytes()),
3355 db,
3356 BEACON_ROOTS_ADDRESS.address,
3357 SYSTEM_ADDRESS,
3358 vm_type,
3359 crypto,
3360 )?;
3361 Ok(())
3362 }
3363
3364 pub fn process_block_hash_history(
3365 block_header: &BlockHeader,
3366 db: &mut GeneralizedDatabase,
3367 vm_type: VMType,
3368 crypto: &dyn Crypto,
3369 ) -> Result<(), EvmError> {
3370 if let VMType::L2(_) = vm_type {
3371 return Err(EvmError::InvalidEVM(
3372 "process_block_hash_history should not be called for L2 VM".to_string(),
3373 ));
3374 }
3375
3376 generic_system_contract_levm(
3377 block_header,
3378 Bytes::copy_from_slice(block_header.parent_hash.as_bytes()),
3379 db,
3380 HISTORY_STORAGE_ADDRESS.address,
3381 SYSTEM_ADDRESS,
3382 vm_type,
3383 crypto,
3384 )?;
3385 Ok(())
3386 }
3387
3388 pub fn install_expiry_verifier_code(
3394 db: &mut GeneralizedDatabase,
3395 crypto: &dyn Crypto,
3396 ) -> Result<(), EvmError> {
3397 const PREDEPLOY_NONCE: u64 = 1;
3399
3400 let current = db.get_account_code(EXPIRY_VERIFIER_PREDEPLOY.address)?;
3401 if current.code() == EXPIRY_VERIFIER_RUNTIME_BYTECODE.as_slice() {
3402 return Ok(());
3403 }
3404 let code = Code::from_bytecode(
3405 Bytes::from_static(&EXPIRY_VERIFIER_RUNTIME_BYTECODE),
3406 crypto,
3407 );
3408 let code_hash = code.hash;
3409 if let Some(recorder) = db.bal_recorder_mut() {
3413 recorder.record_code_change(EXPIRY_VERIFIER_PREDEPLOY.address, code.code_bytes());
3414 recorder.record_nonce_change(EXPIRY_VERIFIER_PREDEPLOY.address, PREDEPLOY_NONCE);
3415 }
3416 let acc = db
3417 .get_account_mut(EXPIRY_VERIFIER_PREDEPLOY.address)
3418 .map_err(EvmError::from)?;
3419 acc.info.code_hash = code_hash;
3420 acc.info.nonce = PREDEPLOY_NONCE;
3421 db.codes.entry(code_hash).or_insert(code);
3422 Ok(())
3423 }
3424
3425 pub(crate) fn read_withdrawal_requests(
3426 block_header: &BlockHeader,
3427 db: &mut GeneralizedDatabase,
3428 vm_type: VMType,
3429 crypto: &dyn Crypto,
3430 ) -> Result<ExecutionReport, EvmError> {
3431 if let VMType::L2(_) = vm_type {
3432 return Err(EvmError::InvalidEVM(
3433 "read_withdrawal_requests should not be called for L2 VM".to_string(),
3434 ));
3435 }
3436
3437 let report = generic_system_contract_levm(
3438 block_header,
3439 Bytes::new(),
3440 db,
3441 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
3442 SYSTEM_ADDRESS,
3443 vm_type,
3444 crypto,
3445 )?;
3446
3447 match report.result {
3448 TxResult::Success => Ok(report),
3449 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
3451 "REVERT when reading withdrawal requests with error: {vm_error:?}. According to EIP-7002, the revert of this system call invalidates the block.",
3452 ))),
3453 }
3454 }
3455
3456 pub(crate) fn dequeue_consolidation_requests(
3457 block_header: &BlockHeader,
3458 db: &mut GeneralizedDatabase,
3459 vm_type: VMType,
3460 crypto: &dyn Crypto,
3461 ) -> Result<ExecutionReport, EvmError> {
3462 if let VMType::L2(_) = vm_type {
3463 return Err(EvmError::InvalidEVM(
3464 "dequeue_consolidation_requests should not be called for L2 VM".to_string(),
3465 ));
3466 }
3467
3468 let report = generic_system_contract_levm(
3469 block_header,
3470 Bytes::new(),
3471 db,
3472 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
3473 SYSTEM_ADDRESS,
3474 vm_type,
3475 crypto,
3476 )?;
3477
3478 match report.result {
3479 TxResult::Success => Ok(report),
3480 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
3482 "REVERT when dequeuing consolidation requests with error: {vm_error:?}. According to EIP-7251, the revert of this system call invalidates the block.",
3483 ))),
3484 }
3485 }
3486
3487 pub(crate) fn read_builder_deposit_requests(
3488 block_header: &BlockHeader,
3489 db: &mut GeneralizedDatabase,
3490 vm_type: VMType,
3491 crypto: &dyn Crypto,
3492 ) -> Result<ExecutionReport, EvmError> {
3493 if let VMType::L2(_) = vm_type {
3494 return Err(EvmError::InvalidEVM(
3495 "read_builder_deposit_requests should not be called for L2 VM".to_string(),
3496 ));
3497 }
3498
3499 let report = generic_system_contract_levm(
3500 block_header,
3501 Bytes::new(),
3502 db,
3503 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
3504 SYSTEM_ADDRESS,
3505 vm_type,
3506 crypto,
3507 )?;
3508
3509 match report.result {
3510 TxResult::Success => Ok(report),
3511 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
3513 "REVERT when reading builder deposit requests with error: {vm_error:?}. According to EIP-8282, the revert of this system call invalidates the block.",
3514 ))),
3515 }
3516 }
3517
3518 pub(crate) fn dequeue_builder_exit_requests(
3519 block_header: &BlockHeader,
3520 db: &mut GeneralizedDatabase,
3521 vm_type: VMType,
3522 crypto: &dyn Crypto,
3523 ) -> Result<ExecutionReport, EvmError> {
3524 if let VMType::L2(_) = vm_type {
3525 return Err(EvmError::InvalidEVM(
3526 "dequeue_builder_exit_requests should not be called for L2 VM".to_string(),
3527 ));
3528 }
3529
3530 let report = generic_system_contract_levm(
3531 block_header,
3532 Bytes::new(),
3533 db,
3534 BUILDER_EXIT_CONTRACT_ADDRESS.address,
3535 SYSTEM_ADDRESS,
3536 vm_type,
3537 crypto,
3538 )?;
3539
3540 match report.result {
3541 TxResult::Success => Ok(report),
3542 TxResult::Revert(vm_error) => Err(EvmError::SystemContractCallFailed(format!(
3544 "REVERT when dequeuing builder exit requests with error: {vm_error:?}. According to EIP-8282, the revert of this system call invalidates the block.",
3545 ))),
3546 }
3547 }
3548
3549 pub fn create_access_list(
3550 mut tx: GenericTransaction,
3551 header: &BlockHeader,
3552 db: &mut GeneralizedDatabase,
3553 vm_type: VMType,
3554 crypto: &dyn Crypto,
3555 ) -> Result<(ExecutionResult, AccessList), VMError> {
3556 let mut env = env_from_generic(&tx, header, db, vm_type)?;
3557
3558 adjust_disabled_base_fee(&mut env);
3559
3560 let converted_tx = generic_tx_to_transaction(&tx)?;
3561 let mut vm = vm_from_generic(&converted_tx, env.clone(), db, vm_type, crypto, None)?;
3562
3563 vm.stateless_execute()?;
3564
3565 tx.access_list = vm.substate.make_access_list();
3567 let converted_tx = generic_tx_to_transaction(&tx)?;
3568 let mut vm = vm_from_generic(&converted_tx, env, db, vm_type, crypto, None)?;
3569
3570 let report = vm.stateless_execute()?;
3571
3572 Ok((
3573 report.into(),
3574 tx.access_list
3575 .into_iter()
3576 .map(|x| (x.address, x.storage_keys))
3577 .collect(),
3578 ))
3579 }
3580
3581 pub fn prepare_block(
3582 block: &Block,
3583 db: &mut GeneralizedDatabase,
3584 vm_type: VMType,
3585 crypto: &dyn Crypto,
3586 ) -> Result<(), EvmError> {
3587 let chain_config = db.store.get_chain_config()?;
3588 let block_header = &block.header;
3589 let fork = chain_config.fork(block_header.timestamp);
3590
3591 if let VMType::L2(_) = vm_type {
3593 return Ok(());
3594 }
3595
3596 if fork >= Fork::Hegota {
3600 Self::install_expiry_verifier_code(db, crypto)?;
3601 }
3602
3603 if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun {
3604 Self::beacon_root_contract_call(block_header, db, vm_type, crypto)?;
3605 }
3606
3607 if fork >= Fork::Prague {
3608 Self::process_block_hash_history(block_header, db, vm_type, crypto)?;
3610 }
3611 Ok(())
3612 }
3613}
3614
3615pub fn generic_system_contract_levm(
3616 block_header: &BlockHeader,
3617 calldata: Bytes,
3618 db: &mut GeneralizedDatabase,
3619 contract_address: Address,
3620 system_address: Address,
3621 vm_type: VMType,
3622 crypto: &dyn Crypto,
3623) -> Result<ExecutionReport, EvmError> {
3624 let chain_config = db.store.get_chain_config()?;
3625 let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
3626 let env = Environment {
3627 origin: system_address,
3628 gas_limit: SYS_CALL_GAS_LIMIT,
3639 block_number: block_header.number,
3640 coinbase: block_header.coinbase,
3641 timestamp: block_header.timestamp,
3642 prev_randao: Some(block_header.prev_randao),
3643 base_fee_per_gas: U256::zero(),
3644 gas_price: U256::zero(),
3645 block_excess_blob_gas: block_header.excess_blob_gas,
3646 block_blob_gas_used: block_header.blob_gas_used,
3647 block_gas_limit: block_header.gas_limit,
3652 is_system_call: true,
3653 config,
3654 ..Default::default()
3655 };
3656
3657 debug_assert!(
3663 env.gas_price.is_zero() && env.base_fee_per_gas.is_zero(),
3664 "system calls must run with a zero gas price"
3665 );
3666
3667 if PRAGUE_SYSTEM_CONTRACTS
3677 .iter()
3678 .chain(AMSTERDAM_REQUEST_PREDEPLOYS.iter())
3679 .any(|contract| contract.address == contract_address)
3680 && db.get_account_code(contract_address)?.is_empty()
3681 {
3682 return Err(EvmError::SystemContractCallFailed(format!(
3683 "System contract: {contract_address} has no code after deployment"
3684 )));
3685 };
3686
3687 let tx = &Transaction::EIP1559Transaction(EIP1559Transaction {
3688 to: TxKind::Call(contract_address),
3689 value: U256::zero(),
3690 data: calldata,
3691 ..Default::default()
3692 });
3693 if let Some(recorder) = db.bal_recorder.as_mut() {
3695 recorder.enter_system_call();
3696 }
3697
3698 let result = VM::new(
3699 env,
3700 db,
3701 tx,
3702 LevmCallTracer::disabled(),
3703 vm_type,
3704 crypto,
3705 None,
3706 )
3707 .and_then(|mut vm| vm.execute())
3708 .map_err(EvmError::from);
3709
3710 if let Some(recorder) = db.bal_recorder.as_mut() {
3712 recorder.exit_system_call();
3713 }
3714
3715 result
3716}
3717
3718#[allow(unreachable_code)]
3719#[allow(unused_variables)]
3720pub fn extract_all_requests_levm(
3721 receipts: &[Receipt],
3722 db: &mut GeneralizedDatabase,
3723 header: &BlockHeader,
3724 vm_type: VMType,
3725 crypto: &dyn Crypto,
3726) -> Result<Vec<Requests>, EvmError> {
3727 if let VMType::L2(_) = vm_type {
3728 return Err(EvmError::InvalidEVM(
3729 "extract_all_requests_levm should not be called for L2 VM".to_string(),
3730 ));
3731 }
3732
3733 let chain_config = db.store.get_chain_config()?;
3734 let fork = chain_config.fork(header.timestamp);
3735
3736 if fork < Fork::Prague {
3737 return Ok(Default::default());
3738 }
3739
3740 let withdrawals_data: Vec<u8> = LEVM::read_withdrawal_requests(header, db, vm_type, crypto)?
3741 .output
3742 .into();
3743 let consolidation_data: Vec<u8> =
3744 LEVM::dequeue_consolidation_requests(header, db, vm_type, crypto)?
3745 .output
3746 .into();
3747
3748 let deposits = Requests::from_deposit_receipts(chain_config.deposit_contract_address, receipts)
3749 .ok_or(EvmError::InvalidDepositRequest)?;
3750 let withdrawals = Requests::from_withdrawals_data(withdrawals_data);
3751 let consolidation = Requests::from_consolidation_data(consolidation_data);
3752
3753 let mut requests = vec![deposits, withdrawals, consolidation];
3754
3755 if fork >= Fork::Amsterdam {
3759 requests.reserve_exact(2);
3761 let builder_deposit_data: Vec<u8> =
3762 LEVM::read_builder_deposit_requests(header, db, vm_type, crypto)?
3763 .output
3764 .into();
3765 let builder_exit_data: Vec<u8> =
3766 LEVM::dequeue_builder_exit_requests(header, db, vm_type, crypto)?
3767 .output
3768 .into();
3769 requests.push(Requests::from_builder_deposit_data(builder_deposit_data));
3770 requests.push(Requests::from_builder_exit_data(builder_exit_data));
3771 }
3772
3773 Ok(requests)
3774}
3775
3776pub fn calculate_gas_price_for_generic(tx: &GenericTransaction, basefee: u64) -> U256 {
3779 if !tx.gas_price.is_zero() {
3780 tx.gas_price
3782 } else {
3783 min(
3785 tx.max_priority_fee_per_gas.unwrap_or(0) + basefee,
3786 tx.max_fee_per_gas.unwrap_or(0),
3787 )
3788 .into()
3789 }
3790}
3791
3792pub fn calculate_gas_price_for_tx(
3793 tx: &Transaction,
3794 mut fee_per_gas: u64,
3795 vm_type: &VMType,
3796) -> Result<U256, VMError> {
3797 let Some(max_priority_fee) = tx.max_priority_fee() else {
3798 return Ok(tx.gas_price());
3800 };
3801
3802 let max_fee_per_gas = tx.max_fee_per_gas().ok_or(VMError::TxValidation(
3803 TxValidationError::InsufficientMaxFeePerGas,
3804 ))?;
3805
3806 if let VMType::L2(fee_config) = vm_type
3807 && let Some(operator_fee_config) = &fee_config.operator_fee_config
3808 {
3809 fee_per_gas += operator_fee_config.operator_fee_per_gas;
3810 }
3811
3812 if fee_per_gas > max_fee_per_gas {
3813 return Err(VMError::TxValidation(
3814 TxValidationError::InsufficientMaxFeePerGas,
3815 ));
3816 }
3817
3818 Ok(min(max_priority_fee + fee_per_gas, max_fee_per_gas).into())
3819}
3820
3821pub(crate) fn adjust_disabled_base_fee(env: &mut Environment) {
3825 if env.gas_price == U256::zero() {
3826 env.base_fee_per_gas = U256::zero();
3827 }
3828 if env
3837 .tx_max_fee_per_blob_gas
3838 .is_some_and(|v| v == U256::zero())
3839 {
3840 env.base_blob_fee_per_gas = U256::zero();
3841 }
3842}
3843
3844fn adjust_disabled_l2_fees(env: &Environment, vm_type: VMType) -> VMType {
3846 if env.gas_price == U256::zero()
3847 && let VMType::L2(fee_config) = vm_type
3848 {
3849 return VMType::L2(FeeConfig {
3851 operator_fee_config: None,
3852 l1_fee_config: None,
3853 ..fee_config
3854 });
3855 }
3856 vm_type
3857}
3858
3859pub(crate) fn env_from_generic(
3860 tx: &GenericTransaction,
3861 header: &BlockHeader,
3862 db: &GeneralizedDatabase,
3863 vm_type: VMType,
3864) -> Result<Environment, VMError> {
3865 let chain_config = db.store.get_chain_config()?;
3866 let gas_price =
3867 calculate_gas_price_for_generic(tx, header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE));
3868 let block_excess_blob_gas = header.excess_blob_gas;
3869 let config = EVMConfig::new_from_chain_config(&chain_config, header);
3870
3871 let slot_number = if let VMType::L2(_) = vm_type {
3885 U256::zero()
3886 } else {
3887 header.slot_number.map(U256::from).unwrap_or(U256::zero())
3888 };
3889
3890 Ok(Environment {
3891 origin: tx.from.0.into(),
3892 gas_limit: tx
3893 .gas
3894 .unwrap_or(get_max_allowed_gas_limit(header.gas_limit, config.fork)), config,
3896 block_number: header.number,
3897 coinbase: header.coinbase,
3898 timestamp: header.timestamp,
3899 prev_randao: Some(header.prev_randao),
3900 slot_number,
3901 chain_id: chain_config.chain_id.into(),
3902 base_fee_per_gas: header.base_fee_per_gas.unwrap_or_default().into(),
3903 base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,
3904 gas_price,
3905 block_excess_blob_gas,
3906 block_blob_gas_used: header.blob_gas_used,
3907 tx_blob_hashes: tx.blob_versioned_hashes.clone(),
3908 tx_max_priority_fee_per_gas: tx.max_priority_fee_per_gas.map(U256::from),
3909 tx_max_fee_per_gas: tx.max_fee_per_gas.map(U256::from),
3910 tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas,
3911 tx_nonce: tx.nonce.unwrap_or_default(),
3912 block_gas_limit: header.gas_limit,
3913 difficulty: header.difficulty,
3914 is_privileged: false,
3915 fee_token: tx.fee_token,
3916 disable_balance_check: false,
3917 disable_nonce_check: true,
3923 disable_gas_allowance_check: true,
3929 disable_sender_eoa_check: true,
3932 is_system_call: false,
3933 })
3934}
3935
3936fn generic_tx_to_transaction(tx: &GenericTransaction) -> Result<Transaction, VMError> {
3951 let (signature_r, signature_s) = (U256::MAX, U256::MAX);
3954 let nonce = tx.nonce.unwrap_or_default();
3955 let gas_limit = tx.gas.unwrap_or_default();
3956 let max_fee_per_gas = tx.max_fee_per_gas.unwrap_or_default();
3957 let max_priority_fee_per_gas = tx.max_priority_fee_per_gas.unwrap_or_default();
3958 let chain_id = tx.chain_id.unwrap_or_default();
3959
3960 Ok(match &tx.authorization_list {
3961 Some(authorization_list) => Transaction::EIP7702Transaction(EIP7702Transaction {
3962 to: match tx.to {
3963 TxKind::Call(to) => to,
3964 TxKind::Create => {
3965 return Err(InternalError::msg("Generic Tx cannot be create type").into());
3966 }
3967 },
3968 value: tx.value,
3969 data: tx.input.clone(),
3970 access_list: tx
3971 .access_list
3972 .iter()
3973 .map(|list| (list.address, list.storage_keys.clone()))
3974 .collect(),
3975 authorization_list: authorization_list
3976 .iter()
3977 .map(|auth| Into::<AuthorizationTuple>::into(auth.clone()))
3978 .collect(),
3979 nonce,
3980 gas_limit,
3981 max_fee_per_gas,
3982 max_priority_fee_per_gas,
3983 chain_id,
3984 signature_r,
3985 signature_s,
3986 ..Default::default()
3987 }),
3988 None => Transaction::EIP1559Transaction(EIP1559Transaction {
3989 to: tx.to.clone(),
3990 value: tx.value,
3991 data: tx.input.clone(),
3992 access_list: tx
3993 .access_list
3994 .iter()
3995 .map(|list| (list.address, list.storage_keys.clone()))
3996 .collect(),
3997 nonce,
3998 gas_limit,
3999 max_fee_per_gas,
4000 max_priority_fee_per_gas,
4001 chain_id,
4002 signature_r,
4003 signature_s,
4004 ..Default::default()
4005 }),
4006 })
4007}
4008
4009fn vm_from_generic<'a>(
4010 tx: &'a Transaction,
4011 env: Environment,
4012 db: &'a mut GeneralizedDatabase,
4013 vm_type: VMType,
4014 crypto: &'a dyn Crypto,
4015 stateless_validator: Option<&'a dyn StatelessValidator>,
4016) -> Result<VM<'a>, VMError> {
4017 let vm_type = adjust_disabled_l2_fees(&env, vm_type);
4018 VM::new(
4019 env,
4020 db,
4021 tx,
4022 LevmCallTracer::disabled(),
4023 vm_type,
4024 crypto,
4025 stateless_validator,
4026 )
4027}
4028
4029pub fn get_max_allowed_gas_limit(block_gas_limit: u64, fork: Fork) -> u64 {
4030 if fork >= Fork::Osaka && fork < Fork::Amsterdam {
4038 POST_OSAKA_GAS_LIMIT_CAP
4039 } else {
4040 block_gas_limit
4041 }
4042}
4043
4044#[allow(dead_code)]
4052fn describe_balance_diff(expected: U256, actual: U256) -> String {
4053 let (sign, mag) = if expected >= actual {
4054 ("+", expected - actual)
4055 } else {
4056 ("-", actual - expected)
4057 };
4058 let Ok(mag_u128) = u128::try_from(mag) else {
4059 return format!("{sign}{mag}");
4060 };
4061 if mag_u128 == 0 {
4062 return "0".to_string();
4063 }
4064 let cpsb: u128 = 1530;
4065 let consts = [
4067 ("NEW_ACCOUNT", 120u128),
4068 ("STORAGE_SET", 64),
4069 ("AUTH_BASE", 23),
4070 ("AUTH_TOTAL", 143),
4071 ];
4072 for &gp in &[10u128, 1, 7, 100, 1000, 1_000_000_000] {
4074 if !mag_u128.is_multiple_of(gp) {
4075 continue;
4076 }
4077 let gas = mag_u128 / gp;
4078 if !gas.is_multiple_of(cpsb) {
4079 continue;
4080 }
4081 let bytes = gas / cpsb;
4082 for (name, c) in consts {
4083 if bytes.is_multiple_of(c) {
4084 let n = bytes / c;
4085 return format!(
4086 "{sign}{mag_u128} wei (= {gas} gas at {gp} wei/gas = {n}× {name}_state_gas)"
4087 );
4088 }
4089 }
4090 }
4091 format!("{sign}{mag_u128} wei")
4092}
4093
4094#[cfg(all(test, feature = "rayon"))]
4097mod bal_tests {
4098 use super::*;
4099 use ethrex_common::H256;
4100 use ethrex_common::types::AccountState;
4101 use ethrex_common::types::block_access_list::{
4102 AccountChanges, BalanceChange, NonceChange, SlotChange, StorageChange,
4103 };
4104 use ethrex_levm::errors::DatabaseError;
4105
4106 fn addr(byte: u8) -> Address {
4107 let mut a = Address::zero();
4108 a.0[19] = byte;
4109 a
4110 }
4111
4112 struct MockStore {
4114 accounts: FxHashMap<Address, AccountState>,
4115 }
4116
4117 impl MockStore {
4118 fn new() -> Self {
4119 Self {
4120 accounts: FxHashMap::default(),
4121 }
4122 }
4123
4124 fn with_account(mut self, address: Address, state: AccountState) -> Self {
4125 self.accounts.insert(address, state);
4126 self
4127 }
4128 }
4129
4130 impl Database for MockStore {
4131 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4132 Ok(self.accounts.get(&address).copied().unwrap_or_default())
4133 }
4134 fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
4135 Ok(U256::zero())
4136 }
4137 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4138 Ok(H256::zero())
4139 }
4140 fn get_chain_config(&self) -> Result<ethrex_common::types::ChainConfig, DatabaseError> {
4141 Err(DatabaseError::Custom("not implemented".into()))
4142 }
4143 fn get_account_code(&self, _: H256) -> Result<ethrex_common::types::Code, DatabaseError> {
4144 Ok(ethrex_common::types::Code::from_bytecode(
4145 Bytes::new(),
4146 ðrex_crypto::NativeCrypto,
4147 ))
4148 }
4149 fn get_code_metadata(
4150 &self,
4151 _: H256,
4152 ) -> Result<ethrex_common::types::CodeMetadata, DatabaseError> {
4153 Ok(ethrex_common::types::CodeMetadata { length: 0 })
4154 }
4155 }
4156
4157 #[test]
4158 fn test_bal_to_account_updates_basic() {
4159 let address = addr(1);
4161 let store = MockStore::new().with_account(
4162 address,
4163 AccountState {
4164 balance: U256::from(100),
4165 nonce: 5,
4166 code_hash: *EMPTY_KECCAK_HASH,
4167 storage_root: H256::zero(),
4168 },
4169 );
4170
4171 let bal = BlockAccessList::from_accounts(vec![
4172 AccountChanges::new(address)
4173 .with_balance_changes(vec![
4174 BalanceChange::new(1, U256::from(90)),
4175 BalanceChange::new(2, U256::from(80)),
4176 ])
4177 .with_nonce_changes(vec![NonceChange::new(1, 6)])
4178 .with_storage_changes(vec![SlotChange::with_changes(
4179 U256::from(42),
4180 vec![StorageChange::new(1, U256::from(999))],
4181 )]),
4182 ]);
4183
4184 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4185 assert_eq!(updates.len(), 1);
4186 let u = &updates[0];
4187 assert_eq!(u.address, address);
4188 assert!(!u.removed);
4189 let info = u.info.as_ref().unwrap();
4190 assert_eq!(info.balance, U256::from(80));
4192 assert_eq!(info.nonce, 6);
4193 assert_eq!(info.code_hash, *EMPTY_KECCAK_HASH);
4194 let key = ethrex_common::utils::u256_to_h256(U256::from(42));
4196 assert_eq!(*u.added_storage.get(&key).unwrap(), U256::from(999));
4197 }
4198
4199 #[test]
4200 fn test_bal_to_account_updates_highest_index_wins() {
4201 let address = addr(2);
4203 let store = MockStore::new().with_account(
4204 address,
4205 AccountState {
4206 balance: U256::from(1000),
4207 nonce: 0,
4208 code_hash: *EMPTY_KECCAK_HASH,
4209 storage_root: H256::zero(),
4210 },
4211 );
4212
4213 let bal = BlockAccessList::from_accounts(vec![
4214 AccountChanges::new(address).with_balance_changes(vec![
4215 BalanceChange::new(1, U256::from(900)),
4216 BalanceChange::new(2, U256::from(800)),
4217 BalanceChange::new(3, U256::from(700)),
4218 ]),
4219 ]);
4220
4221 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4222 assert_eq!(updates.len(), 1);
4223 assert_eq!(updates[0].info.as_ref().unwrap().balance, U256::from(700));
4224 }
4225
4226 #[test]
4227 fn test_bal_to_account_updates_reads_only_skipped() {
4228 let address = addr(3);
4230 let store = MockStore::new();
4231
4232 let bal = BlockAccessList::from_accounts(vec![
4233 AccountChanges::new(address).with_storage_reads(vec![U256::from(1)]),
4234 ]);
4235
4236 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4237 assert!(updates.is_empty());
4238 }
4239
4240 #[test]
4241 fn test_bal_to_account_updates_removal() {
4242 let address = addr(4);
4244 let store = MockStore::new().with_account(
4245 address,
4246 AccountState {
4247 balance: U256::from(50),
4248 nonce: 1,
4249 code_hash: *EMPTY_KECCAK_HASH,
4250 storage_root: H256::zero(),
4251 },
4252 );
4253
4254 let bal = BlockAccessList::from_accounts(vec![
4255 AccountChanges::new(address)
4256 .with_balance_changes(vec![BalanceChange::new(1, U256::zero())])
4257 .with_nonce_changes(vec![NonceChange::new(1, 0)]),
4258 ]);
4259
4260 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4261 assert_eq!(updates.len(), 1);
4262 assert!(updates[0].removed);
4263 }
4264
4265 #[test]
4266 fn test_bal_to_account_updates_storage_zero() {
4267 let address = addr(5);
4269 let store = MockStore::new();
4270
4271 let bal = BlockAccessList::from_accounts(vec![
4272 AccountChanges::new(address).with_storage_changes(vec![SlotChange::with_changes(
4273 U256::from(7),
4274 vec![StorageChange::new(1, U256::zero())],
4275 )]),
4276 ]);
4277
4278 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4279 assert_eq!(updates.len(), 1);
4280 let key = ethrex_common::utils::u256_to_h256(U256::from(7));
4281 assert_eq!(*updates[0].added_storage.get(&key).unwrap(), U256::zero());
4282 }
4283
4284 #[test]
4285 fn test_bal_to_account_updates_code_deployment() {
4286 let address = addr(6);
4288 let store = MockStore::new();
4289 let code = Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xf3]); let expected_hash =
4291 ethrex_common::types::Code::from_bytecode(code.clone(), ðrex_crypto::NativeCrypto)
4292 .hash;
4293
4294 let bal = BlockAccessList::from_accounts(vec![
4295 AccountChanges::new(address)
4296 .with_code_changes(vec![
4297 ethrex_common::types::block_access_list::CodeChange::new(1, code.clone()),
4298 ])
4299 .with_nonce_changes(vec![NonceChange::new(1, 1)]),
4300 ]);
4301
4302 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4303 assert_eq!(updates.len(), 1);
4304 let u = &updates[0];
4305 assert_eq!(u.info.as_ref().unwrap().code_hash, expected_hash);
4306 assert_eq!(u.code.as_ref().unwrap().code(), &code[..]);
4307 }
4308}
4309
4310#[cfg(test)]
4311mod system_call_coinbase_tests {
4312 use super::*;
4318 use ethrex_common::types::{AccountState, AccountUpdate, ChainConfig, Code, CodeMetadata};
4319 use ethrex_crypto::NativeCrypto;
4320 use ethrex_levm::db::Database;
4321 use ethrex_levm::errors::DatabaseError;
4322 use std::sync::Arc;
4323
4324 const HISTORY_RUNTIME_CODE: &str = concat!(
4326 "3373fffffffffffffffffffffffffffffffffffffffe1460465760203603604257",
4327 "5f35600143038111604257611fff81430311604257611fff900654",
4328 "5f5260205ff35b5f5ffd5b5f35611fff60014303065500",
4329 );
4330
4331 struct Store {
4332 chain_config: ChainConfig,
4333 history_code: Code,
4334 }
4335
4336 impl Database for Store {
4337 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4338 if address == HISTORY_STORAGE_ADDRESS.address {
4339 return Ok(AccountState {
4340 nonce: 1,
4341 code_hash: self.history_code.hash,
4342 ..Default::default()
4343 });
4344 }
4345 Ok(AccountState::default())
4346 }
4347 fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
4348 Ok(U256::zero())
4349 }
4350 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4351 Ok(H256::zero())
4352 }
4353 fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
4354 Ok(self.chain_config)
4355 }
4356 fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
4357 if code_hash == self.history_code.hash {
4358 return Ok(self.history_code.clone());
4359 }
4360 Ok(Code::default())
4361 }
4362 fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
4363 let length = if code_hash == self.history_code.hash {
4364 self.history_code.len() as u64
4365 } else {
4366 0
4367 };
4368 Ok(CodeMetadata { length })
4369 }
4370 }
4371
4372 fn history_code() -> Code {
4373 let bytes = hex::decode(HISTORY_RUNTIME_CODE).expect("history runtime code is valid hex");
4374 Code::from_bytecode(Bytes::from(bytes), &NativeCrypto)
4375 }
4376
4377 fn prague_db() -> GeneralizedDatabase {
4378 GeneralizedDatabase::new(Arc::new(Store {
4379 chain_config: ChainConfig {
4380 prague_time: Some(0),
4381 ..Default::default()
4382 },
4383 history_code: history_code(),
4384 }))
4385 }
4386
4387 fn parent_hash_value(parent_hash: H256) -> U256 {
4388 U256::from_big_endian(parent_hash.as_bytes())
4389 }
4390
4391 fn history_slot(block_number: u64) -> H256 {
4392 H256::from_low_u64_be((block_number - 1) % 8191)
4393 }
4394
4395 fn run_history_update(coinbase: Address) -> (Option<U256>, Vec<AccountUpdate>, H256) {
4399 let mut db = prague_db();
4400 let parent_hash = H256::from_low_u64_be(0x2935);
4401 let block_number = 42;
4402 let header = BlockHeader {
4403 parent_hash,
4404 coinbase,
4405 number: block_number,
4406 timestamp: 1,
4407 ..Default::default()
4408 };
4409
4410 LEVM::process_block_hash_history(&header, &mut db, VMType::L1, &NativeCrypto)
4411 .expect("history system call executes");
4412
4413 let slot = history_slot(block_number);
4414 let stored_value = db
4415 .current_accounts_state
4416 .get(&HISTORY_STORAGE_ADDRESS.address)
4417 .and_then(|account| account.storage.get(&slot).copied());
4418 let updates =
4419 LEVM::get_state_transitions(&mut db).expect("state transitions are generated");
4420
4421 (stored_value, updates, parent_hash)
4422 }
4423
4424 fn assert_history_write_emitted(coinbase: Address) {
4425 let (stored_value, updates, parent_hash) = run_history_update(coinbase);
4426 let slot = history_slot(42);
4427 assert_eq!(
4428 stored_value,
4429 Some(parent_hash_value(parent_hash)),
4430 "history storage must hold the parent hash after the system call"
4431 );
4432 assert!(
4433 updates.iter().any(|update| {
4434 update.address == HISTORY_STORAGE_ADDRESS.address
4435 && update.added_storage.get(&slot) == Some(&parent_hash_value(parent_hash))
4436 }),
4437 "the history-contract storage write must be emitted as a state update"
4438 );
4439 }
4440
4441 #[test]
4442 fn ordinary_coinbase_preserves_history_storage_write() {
4443 assert_history_write_emitted(Address::from_low_u64_be(0xbeef));
4444 }
4445
4446 #[test]
4449 fn history_address_coinbase_preserves_history_storage_write() {
4450 assert_history_write_emitted(HISTORY_STORAGE_ADDRESS.address);
4451 }
4452}
4453
4454#[cfg(all(test, feature = "rayon"))]
4459mod burned_fees_tests {
4460 use super::*;
4461 use ethrex_common::{
4462 constants::{DEFAULT_OMMERS_HASH, DEFAULT_REQUESTS_HASH},
4463 types::{AccountState, Block, BlockBody, BlockHeader, ChainConfig, Code, CodeMetadata},
4464 utils::keccak,
4465 };
4466 use ethrex_crypto::NativeCrypto;
4467 use ethrex_levm::errors::DatabaseError;
4468 use ethrex_rlp::encode::PayloadRLPEncode;
4469
4470 fn lstar_chain_config() -> ChainConfig {
4473 ChainConfig {
4474 chain_id: 1,
4475 homestead_block: Some(0),
4476 eip150_block: Some(0),
4477 eip155_block: Some(0),
4478 eip158_block: Some(0),
4479 byzantium_block: Some(0),
4480 constantinople_block: Some(0),
4481 petersburg_block: Some(0),
4482 istanbul_block: Some(0),
4483 berlin_block: Some(0),
4484 london_block: Some(0),
4485 terminal_total_difficulty: Some(0),
4486 terminal_total_difficulty_passed: true,
4487 shanghai_time: Some(0),
4488 cancun_time: Some(0),
4489 prague_time: Some(0),
4490 amsterdam_time: Some(0),
4491 lstar_time: Some(0),
4492 ..Default::default()
4493 }
4494 }
4495
4496 fn amsterdam_chain_config() -> ChainConfig {
4497 ChainConfig {
4498 chain_id: 1,
4499 homestead_block: Some(0),
4500 eip150_block: Some(0),
4501 eip155_block: Some(0),
4502 eip158_block: Some(0),
4503 byzantium_block: Some(0),
4504 constantinople_block: Some(0),
4505 petersburg_block: Some(0),
4506 istanbul_block: Some(0),
4507 berlin_block: Some(0),
4508 london_block: Some(0),
4509 terminal_total_difficulty: Some(0),
4510 terminal_total_difficulty_passed: true,
4511 shanghai_time: Some(0),
4512 cancun_time: Some(0),
4513 prague_time: Some(0),
4514 amsterdam_time: Some(0),
4515 lstar_time: None, ..Default::default()
4517 }
4518 }
4519
4520 struct MockDB {
4523 chain_config: ChainConfig,
4524 accounts: FxHashMap<Address, (AccountState, Bytes)>,
4525 storage: FxHashMap<(Address, H256), U256>,
4527 }
4528
4529 impl MockDB {
4530 fn new(chain_config: ChainConfig) -> Self {
4531 Self {
4532 chain_config,
4533 accounts: FxHashMap::default(),
4534 storage: FxHashMap::default(),
4535 }
4536 }
4537
4538 fn with_account(mut self, address: Address, balance: U256, code: Option<Bytes>) -> Self {
4539 let code_bytes = code.unwrap_or_default();
4540 let code_hash = if code_bytes.is_empty() {
4541 *EMPTY_KECCAK_HASH
4542 } else {
4543 keccak(&code_bytes)
4544 };
4545 let state = AccountState {
4546 nonce: 0,
4547 balance,
4548 storage_root: H256::zero(),
4549 code_hash,
4550 };
4551 self.accounts.insert(address, (state, code_bytes));
4552 self
4553 }
4554
4555 fn with_storage(mut self, address: Address, slot: H256, value: U256) -> Self {
4556 self.storage.insert((address, slot), value);
4557 self
4558 }
4559 }
4560
4561 impl Database for MockDB {
4562 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4563 Ok(self
4564 .accounts
4565 .get(&address)
4566 .map(|(s, _)| *s)
4567 .unwrap_or_default())
4568 }
4569
4570 fn get_storage_value(&self, address: Address, slot: H256) -> Result<U256, DatabaseError> {
4571 Ok(self
4572 .storage
4573 .get(&(address, slot))
4574 .copied()
4575 .unwrap_or_default())
4576 }
4577
4578 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4579 Ok(H256::zero())
4580 }
4581
4582 fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
4583 Ok(self.chain_config)
4584 }
4585
4586 fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
4587 let crypto = NativeCrypto;
4588 for (state, bytecode) in self.accounts.values() {
4589 if state.code_hash == code_hash && !bytecode.is_empty() {
4590 return Ok(Code::from_bytecode(bytecode.clone(), &crypto));
4591 }
4592 }
4593 Ok(Code::from_bytecode(Bytes::new(), &crypto))
4594 }
4595
4596 fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
4597 for (state, bytecode) in self.accounts.values() {
4598 if state.code_hash == code_hash {
4599 return Ok(CodeMetadata {
4600 length: bytecode.len() as u64,
4601 });
4602 }
4603 }
4604 Ok(CodeMetadata { length: 0 })
4605 }
4606 }
4607
4608 const PRIVATE_KEY: [u8; 32] = [
4611 0x4c, 0x08, 0x83, 0xa6, 0x91, 0x02, 0x93, 0x7d, 0x62, 0x31, 0x47, 0x1b, 0x5d, 0xbb, 0x62,
4612 0x04, 0xfe, 0x51, 0x29, 0x61, 0x70, 0x82, 0x79, 0x2a, 0xe4, 0x68, 0xd0, 0x1a, 0x3f, 0x36,
4613 0x23, 0x18,
4614 ];
4615
4616 fn sender_address() -> Address {
4617 use k256::ecdsa::SigningKey;
4618 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
4619 let uncompressed_pub = signing_key.verifying_key().to_encoded_point(false);
4620 let pub_hash = keccak(&uncompressed_pub.as_bytes()[1..]);
4621 Address::from_slice(&pub_hash.0[12..])
4622 }
4623
4624 fn signed_eth_transfer(to: Address, nonce: u64, base_fee: u64) -> Transaction {
4627 use k256::ecdsa::SigningKey;
4628
4629 let tx = EIP1559Transaction {
4630 chain_id: 1,
4631 nonce,
4632 max_priority_fee_per_gas: 1,
4633 max_fee_per_gas: base_fee + 10,
4634 gas_limit: 21_000,
4635 to: TxKind::Call(to),
4636 value: U256::zero(),
4637 ..Default::default()
4638 };
4639
4640 let mut buf = vec![0x02u8];
4642 tx.encode_payload(&mut buf);
4643 let hash = keccak(&buf);
4644
4645 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
4646 let (sig, rid) = signing_key
4647 .sign_prehash_recoverable(&hash.0)
4648 .expect("sign ok");
4649 let sig_bytes = sig.to_bytes();
4650
4651 Transaction::EIP1559Transaction(EIP1559Transaction {
4652 signature_r: U256::from_big_endian(&sig_bytes[..32]),
4653 signature_s: U256::from_big_endian(&sig_bytes[32..]),
4654 signature_y_parity: rid.to_byte() != 0,
4655 ..tx
4656 })
4657 }
4658
4659 const STOP_BYTECODE: &[u8] = &[0x00];
4661
4662 fn make_block_with_tx(tx: Transaction, base_fee: u64) -> Block {
4663 let header = BlockHeader {
4664 parent_hash: H256::zero(),
4665 ommers_hash: *DEFAULT_OMMERS_HASH,
4666 number: 1,
4667 gas_limit: 1_000_000,
4668 timestamp: 1,
4669 base_fee_per_gas: Some(base_fee),
4670 withdrawals_root: Some(H256::zero()),
4671 requests_hash: Some(*DEFAULT_REQUESTS_HASH),
4672 parent_beacon_block_root: None,
4674 ..Default::default()
4675 };
4676 let body = BlockBody {
4677 transactions: vec![tx],
4678 ommers: vec![],
4679 withdrawals: Some(vec![]),
4680 };
4681 Block::new(header, body)
4682 }
4683
4684 fn make_gen_db(chain_config: ChainConfig) -> GeneralizedDatabase {
4685 let sender = sender_address();
4686 let stop = Bytes::from_static(STOP_BYTECODE);
4687 let mock = MockDB::new(chain_config)
4688 .with_account(sender, U256::from(1_000_000_000u64), None)
4690 .with_account(
4692 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
4693 U256::zero(),
4694 Some(stop.clone()),
4695 )
4696 .with_account(
4697 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
4698 U256::zero(),
4699 Some(stop.clone()),
4700 )
4701 .with_account(
4703 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
4704 U256::zero(),
4705 Some(stop.clone()),
4706 )
4707 .with_account(
4708 BUILDER_EXIT_CONTRACT_ADDRESS.address,
4709 U256::zero(),
4710 Some(stop),
4711 );
4712 GeneralizedDatabase::new(Arc::new(mock))
4713 }
4714
4715 const SSTORE_CLEAR_BYTECODE: &[u8] = &[0x60, 0x00, 0x60, 0x00, 0x55, 0x00];
4720
4721 fn signed_call_tx(to: Address, nonce: u64, base_fee: u64, gas_limit: u64) -> Transaction {
4723 use k256::ecdsa::SigningKey;
4724
4725 let tx = EIP1559Transaction {
4726 chain_id: 1,
4727 nonce,
4728 max_priority_fee_per_gas: 1,
4729 max_fee_per_gas: base_fee + 10,
4730 gas_limit,
4731 to: TxKind::Call(to),
4732 value: U256::zero(),
4733 ..Default::default()
4734 };
4735
4736 let mut buf = vec![0x02u8];
4737 tx.encode_payload(&mut buf);
4738 let hash = keccak(&buf);
4739
4740 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
4741 let (sig, rid) = signing_key
4742 .sign_prehash_recoverable(&hash.0)
4743 .expect("sign ok");
4744 let sig_bytes = sig.to_bytes();
4745
4746 Transaction::EIP1559Transaction(EIP1559Transaction {
4747 signature_r: U256::from_big_endian(&sig_bytes[..32]),
4748 signature_s: U256::from_big_endian(&sig_bytes[32..]),
4749 signature_y_parity: rid.to_byte() != 0,
4750 ..tx
4751 })
4752 }
4753
4754 fn make_gen_db_sstore_clear(
4757 chain_config: ChainConfig,
4758 contract_addr: Address,
4759 ) -> GeneralizedDatabase {
4760 let sender = sender_address();
4761 let stop = Bytes::from_static(STOP_BYTECODE);
4762 let mock = MockDB::new(chain_config)
4763 .with_account(sender, U256::from(100_000_000_000u64), None)
4764 .with_account(
4765 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
4766 U256::zero(),
4767 Some(stop.clone()),
4768 )
4769 .with_account(
4770 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
4771 U256::zero(),
4772 Some(stop.clone()),
4773 )
4774 .with_account(
4776 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
4777 U256::zero(),
4778 Some(stop.clone()),
4779 )
4780 .with_account(
4781 BUILDER_EXIT_CONTRACT_ADDRESS.address,
4782 U256::zero(),
4783 Some(stop),
4784 )
4785 .with_account(
4786 contract_addr,
4787 U256::zero(),
4788 Some(Bytes::from_static(SSTORE_CLEAR_BYTECODE)),
4789 )
4790 .with_storage(contract_addr, H256::zero(), U256::one());
4792 GeneralizedDatabase::new(Arc::new(mock))
4793 }
4794
4795 #[test]
4798 fn test_burned_fees_block_aggregate() {
4799 use std::sync::atomic::AtomicUsize;
4800 use std::sync::mpsc;
4801
4802 const BASE_FEE: u64 = 10;
4803 let recipient = Address::from_low_u64_be(0xBEEF);
4811 let tx = signed_eth_transfer(recipient, 0, BASE_FEE);
4812 let block = make_block_with_tx(tx, BASE_FEE);
4813 let crypto = NativeCrypto;
4814
4815 let mut db1 = make_gen_db(lstar_chain_config());
4817 let (result, _bal) = LEVM::execute_block(&block, &mut db1, VMType::L1, &crypto, None)
4818 .expect("execute_block succeeded");
4819 let cumulative = result
4820 .receipts
4821 .last()
4822 .map(|r| r.cumulative_gas_used)
4823 .unwrap_or(0);
4824 let expected = compute_burned_fees(BASE_FEE, cumulative, 0, 0);
4825 assert_eq!(
4826 result.burned_fees,
4827 Some(expected),
4828 "execute_block: burned_fees must equal base_fee * post-refund gas ({cumulative})"
4829 );
4830
4831 let mut db2 = make_gen_db(lstar_chain_config());
4833 let (tx_sender, _rx) = mpsc::channel();
4834 let queue_len = AtomicUsize::new(0);
4835 let tx2 = signed_eth_transfer(recipient, 0, BASE_FEE);
4836 let block2 = make_block_with_tx(tx2, BASE_FEE);
4837 let (result_pipeline, _bal) = LEVM::execute_block_pipeline(
4838 &block2,
4839 &mut db2,
4840 VMType::L1,
4841 Some(tx_sender),
4842 &queue_len,
4843 &crypto,
4844 None, false,
4846 None,
4847 )
4848 .expect("execute_block_pipeline succeeded");
4849 assert_eq!(
4850 result_pipeline.burned_fees, result.burned_fees,
4851 "execute_block_pipeline: burned_fees must match execute_block (production/verification consistency)"
4852 );
4853
4854 let tx3 = signed_eth_transfer(recipient, 0, BASE_FEE);
4856 let block3 = make_block_with_tx(tx3, BASE_FEE);
4857 let mut db3 = make_gen_db(amsterdam_chain_config());
4858 let (result_pre_lstar, _) =
4859 LEVM::execute_block(&block3, &mut db3, VMType::L1, &crypto, None)
4860 .expect("pre-LStar execute_block succeeded");
4861 assert_eq!(
4862 result_pre_lstar.burned_fees, None,
4863 "pre-LStar: burned_fees must be None"
4864 );
4865 }
4866
4867 #[test]
4884 fn test_burned_fees_post_refund_discriminating() {
4885 use std::sync::atomic::AtomicUsize;
4886 use std::sync::mpsc;
4887
4888 const BASE_FEE: u64 = 10;
4889 const TX_GAS_LIMIT: u64 = 100_000;
4891
4892 let contract_addr = Address::from_low_u64_be(0xC0DEBEEF);
4893 let crypto = NativeCrypto;
4894
4895 let tx_a = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4897 let block_a = make_block_with_tx(tx_a, BASE_FEE);
4898 let mut db_a = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4899 let (result_a, _) = LEVM::execute_block(&block_a, &mut db_a, VMType::L1, &crypto, None)
4900 .expect("execute_block with SSTORE clear succeeded");
4901
4902 let cumulative_a = result_a.receipts[0].cumulative_gas_used;
4903 assert!(
4905 result_a.block_gas_used > cumulative_a,
4906 "SSTORE refund must cause block_gas_used ({}) > cumulative_gas_used ({})",
4907 result_a.block_gas_used,
4908 cumulative_a,
4909 );
4910 assert_eq!(
4912 result_a.burned_fees,
4913 Some(BASE_FEE * cumulative_a),
4914 "execute_block: burned_fees must use post-refund gas (cumulative={}, block_gas_used={})",
4915 cumulative_a,
4916 result_a.block_gas_used,
4917 );
4918
4919 let tx_b = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4921 let block_b = make_block_with_tx(tx_b, BASE_FEE);
4922 let mut db_b = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4923 let (tx_sender_b, _rx_b) = mpsc::channel();
4924 let queue_len_b = AtomicUsize::new(0);
4925 let (result_b, _) = LEVM::execute_block_pipeline(
4926 &block_b,
4927 &mut db_b,
4928 VMType::L1,
4929 Some(tx_sender_b),
4930 &queue_len_b,
4931 &crypto,
4932 None, false,
4934 None,
4935 )
4936 .expect("execute_block_pipeline sequential with SSTORE clear succeeded");
4937
4938 let cumulative_b = result_b.receipts[0].cumulative_gas_used;
4939 assert!(
4940 result_b.block_gas_used > cumulative_b,
4941 "sequential pipeline: SSTORE refund must be present",
4942 );
4943 assert_eq!(
4944 result_b.burned_fees,
4945 Some(BASE_FEE * cumulative_b),
4946 "execute_block_pipeline (sequential): burned_fees must use post-refund gas",
4947 );
4948
4949 let tx_c = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4952 let block_c = make_block_with_tx(tx_c, BASE_FEE);
4953 let mut db_c1 = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4954 let (_, bal_c) = LEVM::execute_block(&block_c, &mut db_c1, VMType::L1, &crypto, None)
4955 .expect("BAL generation succeeded");
4956 let bal_c = bal_c.expect("LStar must produce a BAL");
4957
4958 let mut db_c2 = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4959 let (tx_sender_c, _rx_c) = mpsc::channel();
4960 let queue_len_c = AtomicUsize::new(0);
4961 let (result_c, _) = LEVM::execute_block_pipeline(
4962 &block_c,
4963 &mut db_c2,
4964 VMType::L1,
4965 Some(tx_sender_c),
4966 &queue_len_c,
4967 &crypto,
4968 Some(std::sync::Arc::new(bal_c)), true,
4970 None,
4971 )
4972 .expect("execute_block_pipeline parallel with SSTORE clear succeeded");
4973
4974 let cumulative_c = result_c.receipts[0].cumulative_gas_used;
4975 assert!(
4976 result_c.block_gas_used > cumulative_c,
4977 "parallel pipeline: SSTORE refund must be present",
4978 );
4979 assert_eq!(
4980 result_c.burned_fees,
4981 Some(BASE_FEE * cumulative_c),
4982 "execute_block_pipeline (parallel): burned_fees must use post-refund gas",
4983 );
4984
4985 assert_eq!(
4987 result_a.burned_fees, result_b.burned_fees,
4988 "execute_block vs sequential pipeline: burned_fees must match",
4989 );
4990 assert_eq!(
4991 result_a.burned_fees, result_c.burned_fees,
4992 "execute_block vs parallel pipeline: burned_fees must match",
4993 );
4994
4995 let tx_d = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4997 let block_d = make_block_with_tx(tx_d, BASE_FEE);
4998 let mut db_d = make_gen_db_sstore_clear(amsterdam_chain_config(), contract_addr);
4999 let (result_d, _) = LEVM::execute_block(&block_d, &mut db_d, VMType::L1, &crypto, None)
5000 .expect("pre-LStar with SSTORE clear succeeded");
5001 assert_eq!(
5002 result_d.burned_fees, None,
5003 "pre-LStar: burned_fees must be None"
5004 );
5005 }
5006}
5007
5008#[cfg(test)]
5009mod simulated_tx_encoding_tests {
5010 use super::*;
5020 use ethrex_common::types::GenericTransaction;
5021 use ethrex_rlp::encode::RLPEncode;
5022
5023 fn signed_equivalent(gas_limit: u64, data: Bytes) -> Transaction {
5026 Transaction::EIP1559Transaction(EIP1559Transaction {
5027 chain_id: 9_999,
5028 nonce: 42,
5029 max_priority_fee_per_gas: 1_000_000_000,
5030 max_fee_per_gas: 2_000_000_000,
5031 gas_limit,
5032 to: TxKind::Create,
5033 value: U256::from(1u64),
5034 data,
5035 access_list: vec![],
5036 signature_y_parity: true,
5037 signature_r: U256::MAX,
5038 signature_s: U256::MAX,
5039 ..Default::default()
5040 })
5041 }
5042
5043 fn generic(gas_limit: u64, data: Bytes) -> GenericTransaction {
5044 GenericTransaction {
5045 to: TxKind::Create,
5046 value: U256::from(1u64),
5047 input: data,
5048 nonce: Some(42),
5049 gas: Some(gas_limit),
5050 max_fee_per_gas: Some(2_000_000_000),
5051 max_priority_fee_per_gas: Some(1_000_000_000),
5052 chain_id: Some(9_999),
5053 ..Default::default()
5054 }
5055 }
5056
5057 #[test]
5058 fn simulated_tx_never_encodes_shorter_than_the_signed_one() {
5059 for (gas_limit, data) in [
5060 (21_000u64, Bytes::new()),
5061 (150_000, Bytes::from_static(&[0x60, 0x00, 0x60, 0x00])),
5062 (5_000_000, Bytes::from(vec![0xab; 4_096])),
5063 ] {
5064 let simulated = generic_tx_to_transaction(&generic(gas_limit, data.clone()))
5065 .expect("conversion should succeed");
5066 let signed = signed_equivalent(gas_limit, data.clone());
5067 assert!(
5068 simulated.length() >= signed.length(),
5069 "simulated tx encodes {} bytes, signed one {} — the L2 would under-reserve \
5070 the L1 fee by the difference (gas_limit {gas_limit}, {} data bytes)",
5071 simulated.length(),
5072 signed.length(),
5073 data.len(),
5074 );
5075 }
5076 }
5077}