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 is_system_call: false,
3003 };
3004
3005 Ok(env)
3006 }
3007
3008 pub fn execute_tx(
3009 tx: &Transaction,
3011 tx_sender: Address,
3013 block_header: &BlockHeader,
3015 db: &mut GeneralizedDatabase,
3016 vm_type: VMType,
3017 crypto: &dyn Crypto,
3018 stateless_validator: Option<&dyn StatelessValidator>,
3019 ) -> Result<ExecutionReport, EvmError> {
3020 let env = Self::setup_env(tx, tx_sender, block_header, db, vm_type)?;
3021 let mut vm = VM::new(
3022 env,
3023 db,
3024 tx,
3025 LevmCallTracer::disabled(),
3026 vm_type,
3027 crypto,
3028 stateless_validator,
3029 )?;
3030
3031 vm.execute().map_err(VMError::into)
3032 }
3033
3034 #[allow(clippy::too_many_arguments)]
3037 fn execute_tx_in_block(
3038 tx: &Transaction,
3040 tx_sender: Address,
3042 block_header: &BlockHeader,
3044 db: &mut GeneralizedDatabase,
3045 vm_type: VMType,
3046 base_blob_fee_per_gas: U256,
3047 stack_pool: &mut Vec<Stack>,
3048 memory_pool: &mut Vec<Memory>,
3049 disable_balance_check: bool,
3050 crypto: &dyn Crypto,
3051 config: EVMConfig,
3052 chain_id: u64,
3053 stateless_validator: Option<&dyn StatelessValidator>,
3054 ) -> Result<ExecutionReport, EvmError> {
3055 let mut env = Self::setup_env_with_config(
3056 tx,
3057 tx_sender,
3058 block_header,
3059 config,
3060 chain_id,
3061 vm_type,
3062 base_blob_fee_per_gas,
3063 )?;
3064 env.disable_balance_check = disable_balance_check;
3065 let mut vm = VM::new_pooled(
3069 env,
3070 db,
3071 tx,
3072 LevmCallTracer::disabled(),
3073 vm_type,
3074 crypto,
3075 stateless_validator,
3076 stack_pool,
3077 memory_pool,
3078 )?;
3079 let result = vm.execute().map_err(VMError::into);
3080 vm.reclaim_into(stack_pool, memory_pool);
3082 result
3083 }
3084
3085 pub fn undo_last_tx(db: &mut GeneralizedDatabase) -> Result<(), EvmError> {
3086 db.undo_last_transaction()?;
3087 Ok(())
3088 }
3089
3090 pub fn simulate_tx_from_generic(
3091 tx: &GenericTransaction,
3093 block_header: &BlockHeader,
3095 db: &mut GeneralizedDatabase,
3096 vm_type: VMType,
3097 crypto: &dyn Crypto,
3098 stateless_validator: Option<&dyn StatelessValidator>,
3099 ) -> Result<ExecutionResult, EvmError> {
3100 let mut env = env_from_generic(tx, block_header, db, vm_type)?;
3101
3102 env.disable_gas_allowance_check = true;
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
3821fn 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
3829 .tx_max_fee_per_blob_gas
3830 .is_some_and(|v| v == U256::zero())
3831 {
3832 env.block_excess_blob_gas = None;
3833 }
3834}
3835
3836fn adjust_disabled_l2_fees(env: &Environment, vm_type: VMType) -> VMType {
3838 if env.gas_price == U256::zero()
3839 && let VMType::L2(fee_config) = vm_type
3840 {
3841 return VMType::L2(FeeConfig {
3843 operator_fee_config: None,
3844 l1_fee_config: None,
3845 ..fee_config
3846 });
3847 }
3848 vm_type
3849}
3850
3851fn env_from_generic(
3852 tx: &GenericTransaction,
3853 header: &BlockHeader,
3854 db: &GeneralizedDatabase,
3855 vm_type: VMType,
3856) -> Result<Environment, VMError> {
3857 let chain_config = db.store.get_chain_config()?;
3858 let gas_price =
3859 calculate_gas_price_for_generic(tx, header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE));
3860 let block_excess_blob_gas = header.excess_blob_gas;
3861 let config = EVMConfig::new_from_chain_config(&chain_config, header);
3862
3863 let slot_number = if let VMType::L2(_) = vm_type {
3877 U256::zero()
3878 } else {
3879 header.slot_number.map(U256::from).unwrap_or(U256::zero())
3880 };
3881
3882 Ok(Environment {
3883 origin: tx.from.0.into(),
3884 gas_limit: tx
3885 .gas
3886 .unwrap_or(get_max_allowed_gas_limit(header.gas_limit, config.fork)), config,
3888 block_number: header.number,
3889 coinbase: header.coinbase,
3890 timestamp: header.timestamp,
3891 prev_randao: Some(header.prev_randao),
3892 slot_number,
3893 chain_id: chain_config.chain_id.into(),
3894 base_fee_per_gas: header.base_fee_per_gas.unwrap_or_default().into(),
3895 base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,
3896 gas_price,
3897 block_excess_blob_gas,
3898 block_blob_gas_used: header.blob_gas_used,
3899 tx_blob_hashes: tx.blob_versioned_hashes.clone(),
3900 tx_max_priority_fee_per_gas: tx.max_priority_fee_per_gas.map(U256::from),
3901 tx_max_fee_per_gas: tx.max_fee_per_gas.map(U256::from),
3902 tx_max_fee_per_blob_gas: tx.max_fee_per_blob_gas,
3903 tx_nonce: tx.nonce.unwrap_or_default(),
3904 block_gas_limit: header.gas_limit,
3905 difficulty: header.difficulty,
3906 is_privileged: false,
3907 fee_token: tx.fee_token,
3908 disable_balance_check: false,
3909 disable_nonce_check: true,
3915 disable_gas_allowance_check: false,
3918 is_system_call: false,
3919 })
3920}
3921
3922fn generic_tx_to_transaction(tx: &GenericTransaction) -> Result<Transaction, VMError> {
3937 let (signature_r, signature_s) = (U256::MAX, U256::MAX);
3940 let nonce = tx.nonce.unwrap_or_default();
3941 let gas_limit = tx.gas.unwrap_or_default();
3942 let max_fee_per_gas = tx.max_fee_per_gas.unwrap_or_default();
3943 let max_priority_fee_per_gas = tx.max_priority_fee_per_gas.unwrap_or_default();
3944 let chain_id = tx.chain_id.unwrap_or_default();
3945
3946 Ok(match &tx.authorization_list {
3947 Some(authorization_list) => Transaction::EIP7702Transaction(EIP7702Transaction {
3948 to: match tx.to {
3949 TxKind::Call(to) => to,
3950 TxKind::Create => {
3951 return Err(InternalError::msg("Generic Tx cannot be create type").into());
3952 }
3953 },
3954 value: tx.value,
3955 data: tx.input.clone(),
3956 access_list: tx
3957 .access_list
3958 .iter()
3959 .map(|list| (list.address, list.storage_keys.clone()))
3960 .collect(),
3961 authorization_list: authorization_list
3962 .iter()
3963 .map(|auth| Into::<AuthorizationTuple>::into(auth.clone()))
3964 .collect(),
3965 nonce,
3966 gas_limit,
3967 max_fee_per_gas,
3968 max_priority_fee_per_gas,
3969 chain_id,
3970 signature_r,
3971 signature_s,
3972 ..Default::default()
3973 }),
3974 None => Transaction::EIP1559Transaction(EIP1559Transaction {
3975 to: tx.to.clone(),
3976 value: tx.value,
3977 data: tx.input.clone(),
3978 access_list: tx
3979 .access_list
3980 .iter()
3981 .map(|list| (list.address, list.storage_keys.clone()))
3982 .collect(),
3983 nonce,
3984 gas_limit,
3985 max_fee_per_gas,
3986 max_priority_fee_per_gas,
3987 chain_id,
3988 signature_r,
3989 signature_s,
3990 ..Default::default()
3991 }),
3992 })
3993}
3994
3995fn vm_from_generic<'a>(
3996 tx: &'a Transaction,
3997 env: Environment,
3998 db: &'a mut GeneralizedDatabase,
3999 vm_type: VMType,
4000 crypto: &'a dyn Crypto,
4001 stateless_validator: Option<&'a dyn StatelessValidator>,
4002) -> Result<VM<'a>, VMError> {
4003 let vm_type = adjust_disabled_l2_fees(&env, vm_type);
4004 VM::new(
4005 env,
4006 db,
4007 tx,
4008 LevmCallTracer::disabled(),
4009 vm_type,
4010 crypto,
4011 stateless_validator,
4012 )
4013}
4014
4015pub fn get_max_allowed_gas_limit(block_gas_limit: u64, fork: Fork) -> u64 {
4016 if fork >= Fork::Osaka && fork < Fork::Amsterdam {
4024 POST_OSAKA_GAS_LIMIT_CAP
4025 } else {
4026 block_gas_limit
4027 }
4028}
4029
4030#[allow(dead_code)]
4038fn describe_balance_diff(expected: U256, actual: U256) -> String {
4039 let (sign, mag) = if expected >= actual {
4040 ("+", expected - actual)
4041 } else {
4042 ("-", actual - expected)
4043 };
4044 let Ok(mag_u128) = u128::try_from(mag) else {
4045 return format!("{sign}{mag}");
4046 };
4047 if mag_u128 == 0 {
4048 return "0".to_string();
4049 }
4050 let cpsb: u128 = 1530;
4051 let consts = [
4053 ("NEW_ACCOUNT", 120u128),
4054 ("STORAGE_SET", 64),
4055 ("AUTH_BASE", 23),
4056 ("AUTH_TOTAL", 143),
4057 ];
4058 for &gp in &[10u128, 1, 7, 100, 1000, 1_000_000_000] {
4060 if !mag_u128.is_multiple_of(gp) {
4061 continue;
4062 }
4063 let gas = mag_u128 / gp;
4064 if !gas.is_multiple_of(cpsb) {
4065 continue;
4066 }
4067 let bytes = gas / cpsb;
4068 for (name, c) in consts {
4069 if bytes.is_multiple_of(c) {
4070 let n = bytes / c;
4071 return format!(
4072 "{sign}{mag_u128} wei (= {gas} gas at {gp} wei/gas = {n}× {name}_state_gas)"
4073 );
4074 }
4075 }
4076 }
4077 format!("{sign}{mag_u128} wei")
4078}
4079
4080#[cfg(all(test, feature = "rayon"))]
4083mod bal_tests {
4084 use super::*;
4085 use ethrex_common::H256;
4086 use ethrex_common::types::AccountState;
4087 use ethrex_common::types::block_access_list::{
4088 AccountChanges, BalanceChange, NonceChange, SlotChange, StorageChange,
4089 };
4090 use ethrex_levm::errors::DatabaseError;
4091
4092 fn addr(byte: u8) -> Address {
4093 let mut a = Address::zero();
4094 a.0[19] = byte;
4095 a
4096 }
4097
4098 struct MockStore {
4100 accounts: FxHashMap<Address, AccountState>,
4101 }
4102
4103 impl MockStore {
4104 fn new() -> Self {
4105 Self {
4106 accounts: FxHashMap::default(),
4107 }
4108 }
4109
4110 fn with_account(mut self, address: Address, state: AccountState) -> Self {
4111 self.accounts.insert(address, state);
4112 self
4113 }
4114 }
4115
4116 impl Database for MockStore {
4117 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4118 Ok(self.accounts.get(&address).copied().unwrap_or_default())
4119 }
4120 fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
4121 Ok(U256::zero())
4122 }
4123 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4124 Ok(H256::zero())
4125 }
4126 fn get_chain_config(&self) -> Result<ethrex_common::types::ChainConfig, DatabaseError> {
4127 Err(DatabaseError::Custom("not implemented".into()))
4128 }
4129 fn get_account_code(&self, _: H256) -> Result<ethrex_common::types::Code, DatabaseError> {
4130 Ok(ethrex_common::types::Code::from_bytecode(
4131 Bytes::new(),
4132 ðrex_crypto::NativeCrypto,
4133 ))
4134 }
4135 fn get_code_metadata(
4136 &self,
4137 _: H256,
4138 ) -> Result<ethrex_common::types::CodeMetadata, DatabaseError> {
4139 Ok(ethrex_common::types::CodeMetadata { length: 0 })
4140 }
4141 }
4142
4143 #[test]
4144 fn test_bal_to_account_updates_basic() {
4145 let address = addr(1);
4147 let store = MockStore::new().with_account(
4148 address,
4149 AccountState {
4150 balance: U256::from(100),
4151 nonce: 5,
4152 code_hash: *EMPTY_KECCAK_HASH,
4153 storage_root: H256::zero(),
4154 },
4155 );
4156
4157 let bal = BlockAccessList::from_accounts(vec![
4158 AccountChanges::new(address)
4159 .with_balance_changes(vec![
4160 BalanceChange::new(1, U256::from(90)),
4161 BalanceChange::new(2, U256::from(80)),
4162 ])
4163 .with_nonce_changes(vec![NonceChange::new(1, 6)])
4164 .with_storage_changes(vec![SlotChange::with_changes(
4165 U256::from(42),
4166 vec![StorageChange::new(1, U256::from(999))],
4167 )]),
4168 ]);
4169
4170 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4171 assert_eq!(updates.len(), 1);
4172 let u = &updates[0];
4173 assert_eq!(u.address, address);
4174 assert!(!u.removed);
4175 let info = u.info.as_ref().unwrap();
4176 assert_eq!(info.balance, U256::from(80));
4178 assert_eq!(info.nonce, 6);
4179 assert_eq!(info.code_hash, *EMPTY_KECCAK_HASH);
4180 let key = ethrex_common::utils::u256_to_h256(U256::from(42));
4182 assert_eq!(*u.added_storage.get(&key).unwrap(), U256::from(999));
4183 }
4184
4185 #[test]
4186 fn test_bal_to_account_updates_highest_index_wins() {
4187 let address = addr(2);
4189 let store = MockStore::new().with_account(
4190 address,
4191 AccountState {
4192 balance: U256::from(1000),
4193 nonce: 0,
4194 code_hash: *EMPTY_KECCAK_HASH,
4195 storage_root: H256::zero(),
4196 },
4197 );
4198
4199 let bal = BlockAccessList::from_accounts(vec![
4200 AccountChanges::new(address).with_balance_changes(vec![
4201 BalanceChange::new(1, U256::from(900)),
4202 BalanceChange::new(2, U256::from(800)),
4203 BalanceChange::new(3, U256::from(700)),
4204 ]),
4205 ]);
4206
4207 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4208 assert_eq!(updates.len(), 1);
4209 assert_eq!(updates[0].info.as_ref().unwrap().balance, U256::from(700));
4210 }
4211
4212 #[test]
4213 fn test_bal_to_account_updates_reads_only_skipped() {
4214 let address = addr(3);
4216 let store = MockStore::new();
4217
4218 let bal = BlockAccessList::from_accounts(vec![
4219 AccountChanges::new(address).with_storage_reads(vec![U256::from(1)]),
4220 ]);
4221
4222 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4223 assert!(updates.is_empty());
4224 }
4225
4226 #[test]
4227 fn test_bal_to_account_updates_removal() {
4228 let address = addr(4);
4230 let store = MockStore::new().with_account(
4231 address,
4232 AccountState {
4233 balance: U256::from(50),
4234 nonce: 1,
4235 code_hash: *EMPTY_KECCAK_HASH,
4236 storage_root: H256::zero(),
4237 },
4238 );
4239
4240 let bal = BlockAccessList::from_accounts(vec![
4241 AccountChanges::new(address)
4242 .with_balance_changes(vec![BalanceChange::new(1, U256::zero())])
4243 .with_nonce_changes(vec![NonceChange::new(1, 0)]),
4244 ]);
4245
4246 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4247 assert_eq!(updates.len(), 1);
4248 assert!(updates[0].removed);
4249 }
4250
4251 #[test]
4252 fn test_bal_to_account_updates_storage_zero() {
4253 let address = addr(5);
4255 let store = MockStore::new();
4256
4257 let bal = BlockAccessList::from_accounts(vec![
4258 AccountChanges::new(address).with_storage_changes(vec![SlotChange::with_changes(
4259 U256::from(7),
4260 vec![StorageChange::new(1, U256::zero())],
4261 )]),
4262 ]);
4263
4264 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4265 assert_eq!(updates.len(), 1);
4266 let key = ethrex_common::utils::u256_to_h256(U256::from(7));
4267 assert_eq!(*updates[0].added_storage.get(&key).unwrap(), U256::zero());
4268 }
4269
4270 #[test]
4271 fn test_bal_to_account_updates_code_deployment() {
4272 let address = addr(6);
4274 let store = MockStore::new();
4275 let code = Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xf3]); let expected_hash =
4277 ethrex_common::types::Code::from_bytecode(code.clone(), ðrex_crypto::NativeCrypto)
4278 .hash;
4279
4280 let bal = BlockAccessList::from_accounts(vec![
4281 AccountChanges::new(address)
4282 .with_code_changes(vec![
4283 ethrex_common::types::block_access_list::CodeChange::new(1, code.clone()),
4284 ])
4285 .with_nonce_changes(vec![NonceChange::new(1, 1)]),
4286 ]);
4287
4288 let updates = LEVM::bal_to_account_updates(&bal, &store).unwrap();
4289 assert_eq!(updates.len(), 1);
4290 let u = &updates[0];
4291 assert_eq!(u.info.as_ref().unwrap().code_hash, expected_hash);
4292 assert_eq!(u.code.as_ref().unwrap().code(), &code[..]);
4293 }
4294}
4295
4296#[cfg(test)]
4297mod system_call_coinbase_tests {
4298 use super::*;
4304 use ethrex_common::types::{AccountState, AccountUpdate, ChainConfig, Code, CodeMetadata};
4305 use ethrex_crypto::NativeCrypto;
4306 use ethrex_levm::db::Database;
4307 use ethrex_levm::errors::DatabaseError;
4308 use std::sync::Arc;
4309
4310 const HISTORY_RUNTIME_CODE: &str = concat!(
4312 "3373fffffffffffffffffffffffffffffffffffffffe1460465760203603604257",
4313 "5f35600143038111604257611fff81430311604257611fff900654",
4314 "5f5260205ff35b5f5ffd5b5f35611fff60014303065500",
4315 );
4316
4317 struct Store {
4318 chain_config: ChainConfig,
4319 history_code: Code,
4320 }
4321
4322 impl Database for Store {
4323 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4324 if address == HISTORY_STORAGE_ADDRESS.address {
4325 return Ok(AccountState {
4326 nonce: 1,
4327 code_hash: self.history_code.hash,
4328 ..Default::default()
4329 });
4330 }
4331 Ok(AccountState::default())
4332 }
4333 fn get_storage_value(&self, _: Address, _: H256) -> Result<U256, DatabaseError> {
4334 Ok(U256::zero())
4335 }
4336 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4337 Ok(H256::zero())
4338 }
4339 fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
4340 Ok(self.chain_config)
4341 }
4342 fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
4343 if code_hash == self.history_code.hash {
4344 return Ok(self.history_code.clone());
4345 }
4346 Ok(Code::default())
4347 }
4348 fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
4349 let length = if code_hash == self.history_code.hash {
4350 self.history_code.len() as u64
4351 } else {
4352 0
4353 };
4354 Ok(CodeMetadata { length })
4355 }
4356 }
4357
4358 fn history_code() -> Code {
4359 let bytes = hex::decode(HISTORY_RUNTIME_CODE).expect("history runtime code is valid hex");
4360 Code::from_bytecode(Bytes::from(bytes), &NativeCrypto)
4361 }
4362
4363 fn prague_db() -> GeneralizedDatabase {
4364 GeneralizedDatabase::new(Arc::new(Store {
4365 chain_config: ChainConfig {
4366 prague_time: Some(0),
4367 ..Default::default()
4368 },
4369 history_code: history_code(),
4370 }))
4371 }
4372
4373 fn parent_hash_value(parent_hash: H256) -> U256 {
4374 U256::from_big_endian(parent_hash.as_bytes())
4375 }
4376
4377 fn history_slot(block_number: u64) -> H256 {
4378 H256::from_low_u64_be((block_number - 1) % 8191)
4379 }
4380
4381 fn run_history_update(coinbase: Address) -> (Option<U256>, Vec<AccountUpdate>, H256) {
4385 let mut db = prague_db();
4386 let parent_hash = H256::from_low_u64_be(0x2935);
4387 let block_number = 42;
4388 let header = BlockHeader {
4389 parent_hash,
4390 coinbase,
4391 number: block_number,
4392 timestamp: 1,
4393 ..Default::default()
4394 };
4395
4396 LEVM::process_block_hash_history(&header, &mut db, VMType::L1, &NativeCrypto)
4397 .expect("history system call executes");
4398
4399 let slot = history_slot(block_number);
4400 let stored_value = db
4401 .current_accounts_state
4402 .get(&HISTORY_STORAGE_ADDRESS.address)
4403 .and_then(|account| account.storage.get(&slot).copied());
4404 let updates =
4405 LEVM::get_state_transitions(&mut db).expect("state transitions are generated");
4406
4407 (stored_value, updates, parent_hash)
4408 }
4409
4410 fn assert_history_write_emitted(coinbase: Address) {
4411 let (stored_value, updates, parent_hash) = run_history_update(coinbase);
4412 let slot = history_slot(42);
4413 assert_eq!(
4414 stored_value,
4415 Some(parent_hash_value(parent_hash)),
4416 "history storage must hold the parent hash after the system call"
4417 );
4418 assert!(
4419 updates.iter().any(|update| {
4420 update.address == HISTORY_STORAGE_ADDRESS.address
4421 && update.added_storage.get(&slot) == Some(&parent_hash_value(parent_hash))
4422 }),
4423 "the history-contract storage write must be emitted as a state update"
4424 );
4425 }
4426
4427 #[test]
4428 fn ordinary_coinbase_preserves_history_storage_write() {
4429 assert_history_write_emitted(Address::from_low_u64_be(0xbeef));
4430 }
4431
4432 #[test]
4435 fn history_address_coinbase_preserves_history_storage_write() {
4436 assert_history_write_emitted(HISTORY_STORAGE_ADDRESS.address);
4437 }
4438}
4439
4440#[cfg(all(test, feature = "rayon"))]
4445mod burned_fees_tests {
4446 use super::*;
4447 use ethrex_common::{
4448 constants::{DEFAULT_OMMERS_HASH, DEFAULT_REQUESTS_HASH},
4449 types::{AccountState, Block, BlockBody, BlockHeader, ChainConfig, Code, CodeMetadata},
4450 utils::keccak,
4451 };
4452 use ethrex_crypto::NativeCrypto;
4453 use ethrex_levm::errors::DatabaseError;
4454 use ethrex_rlp::encode::PayloadRLPEncode;
4455
4456 fn lstar_chain_config() -> ChainConfig {
4459 ChainConfig {
4460 chain_id: 1,
4461 homestead_block: Some(0),
4462 eip150_block: Some(0),
4463 eip155_block: Some(0),
4464 eip158_block: Some(0),
4465 byzantium_block: Some(0),
4466 constantinople_block: Some(0),
4467 petersburg_block: Some(0),
4468 istanbul_block: Some(0),
4469 berlin_block: Some(0),
4470 london_block: Some(0),
4471 terminal_total_difficulty: Some(0),
4472 terminal_total_difficulty_passed: true,
4473 shanghai_time: Some(0),
4474 cancun_time: Some(0),
4475 prague_time: Some(0),
4476 amsterdam_time: Some(0),
4477 lstar_time: Some(0),
4478 ..Default::default()
4479 }
4480 }
4481
4482 fn amsterdam_chain_config() -> ChainConfig {
4483 ChainConfig {
4484 chain_id: 1,
4485 homestead_block: Some(0),
4486 eip150_block: Some(0),
4487 eip155_block: Some(0),
4488 eip158_block: Some(0),
4489 byzantium_block: Some(0),
4490 constantinople_block: Some(0),
4491 petersburg_block: Some(0),
4492 istanbul_block: Some(0),
4493 berlin_block: Some(0),
4494 london_block: Some(0),
4495 terminal_total_difficulty: Some(0),
4496 terminal_total_difficulty_passed: true,
4497 shanghai_time: Some(0),
4498 cancun_time: Some(0),
4499 prague_time: Some(0),
4500 amsterdam_time: Some(0),
4501 lstar_time: None, ..Default::default()
4503 }
4504 }
4505
4506 struct MockDB {
4509 chain_config: ChainConfig,
4510 accounts: FxHashMap<Address, (AccountState, Bytes)>,
4511 storage: FxHashMap<(Address, H256), U256>,
4513 }
4514
4515 impl MockDB {
4516 fn new(chain_config: ChainConfig) -> Self {
4517 Self {
4518 chain_config,
4519 accounts: FxHashMap::default(),
4520 storage: FxHashMap::default(),
4521 }
4522 }
4523
4524 fn with_account(mut self, address: Address, balance: U256, code: Option<Bytes>) -> Self {
4525 let code_bytes = code.unwrap_or_default();
4526 let code_hash = if code_bytes.is_empty() {
4527 *EMPTY_KECCAK_HASH
4528 } else {
4529 keccak(&code_bytes)
4530 };
4531 let state = AccountState {
4532 nonce: 0,
4533 balance,
4534 storage_root: H256::zero(),
4535 code_hash,
4536 };
4537 self.accounts.insert(address, (state, code_bytes));
4538 self
4539 }
4540
4541 fn with_storage(mut self, address: Address, slot: H256, value: U256) -> Self {
4542 self.storage.insert((address, slot), value);
4543 self
4544 }
4545 }
4546
4547 impl Database for MockDB {
4548 fn get_account_state(&self, address: Address) -> Result<AccountState, DatabaseError> {
4549 Ok(self
4550 .accounts
4551 .get(&address)
4552 .map(|(s, _)| *s)
4553 .unwrap_or_default())
4554 }
4555
4556 fn get_storage_value(&self, address: Address, slot: H256) -> Result<U256, DatabaseError> {
4557 Ok(self
4558 .storage
4559 .get(&(address, slot))
4560 .copied()
4561 .unwrap_or_default())
4562 }
4563
4564 fn get_block_hash(&self, _: u64) -> Result<H256, DatabaseError> {
4565 Ok(H256::zero())
4566 }
4567
4568 fn get_chain_config(&self) -> Result<ChainConfig, DatabaseError> {
4569 Ok(self.chain_config)
4570 }
4571
4572 fn get_account_code(&self, code_hash: H256) -> Result<Code, DatabaseError> {
4573 let crypto = NativeCrypto;
4574 for (state, bytecode) in self.accounts.values() {
4575 if state.code_hash == code_hash && !bytecode.is_empty() {
4576 return Ok(Code::from_bytecode(bytecode.clone(), &crypto));
4577 }
4578 }
4579 Ok(Code::from_bytecode(Bytes::new(), &crypto))
4580 }
4581
4582 fn get_code_metadata(&self, code_hash: H256) -> Result<CodeMetadata, DatabaseError> {
4583 for (state, bytecode) in self.accounts.values() {
4584 if state.code_hash == code_hash {
4585 return Ok(CodeMetadata {
4586 length: bytecode.len() as u64,
4587 });
4588 }
4589 }
4590 Ok(CodeMetadata { length: 0 })
4591 }
4592 }
4593
4594 const PRIVATE_KEY: [u8; 32] = [
4597 0x4c, 0x08, 0x83, 0xa6, 0x91, 0x02, 0x93, 0x7d, 0x62, 0x31, 0x47, 0x1b, 0x5d, 0xbb, 0x62,
4598 0x04, 0xfe, 0x51, 0x29, 0x61, 0x70, 0x82, 0x79, 0x2a, 0xe4, 0x68, 0xd0, 0x1a, 0x3f, 0x36,
4599 0x23, 0x18,
4600 ];
4601
4602 fn sender_address() -> Address {
4603 use k256::ecdsa::SigningKey;
4604 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
4605 let uncompressed_pub = signing_key.verifying_key().to_encoded_point(false);
4606 let pub_hash = keccak(&uncompressed_pub.as_bytes()[1..]);
4607 Address::from_slice(&pub_hash.0[12..])
4608 }
4609
4610 fn signed_eth_transfer(to: Address, nonce: u64, base_fee: u64) -> Transaction {
4613 use k256::ecdsa::SigningKey;
4614
4615 let tx = EIP1559Transaction {
4616 chain_id: 1,
4617 nonce,
4618 max_priority_fee_per_gas: 1,
4619 max_fee_per_gas: base_fee + 10,
4620 gas_limit: 21_000,
4621 to: TxKind::Call(to),
4622 value: U256::zero(),
4623 ..Default::default()
4624 };
4625
4626 let mut buf = vec![0x02u8];
4628 tx.encode_payload(&mut buf);
4629 let hash = keccak(&buf);
4630
4631 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
4632 let (sig, rid) = signing_key
4633 .sign_prehash_recoverable(&hash.0)
4634 .expect("sign ok");
4635 let sig_bytes = sig.to_bytes();
4636
4637 Transaction::EIP1559Transaction(EIP1559Transaction {
4638 signature_r: U256::from_big_endian(&sig_bytes[..32]),
4639 signature_s: U256::from_big_endian(&sig_bytes[32..]),
4640 signature_y_parity: rid.to_byte() != 0,
4641 ..tx
4642 })
4643 }
4644
4645 const STOP_BYTECODE: &[u8] = &[0x00];
4647
4648 fn make_block_with_tx(tx: Transaction, base_fee: u64) -> Block {
4649 let header = BlockHeader {
4650 parent_hash: H256::zero(),
4651 ommers_hash: *DEFAULT_OMMERS_HASH,
4652 number: 1,
4653 gas_limit: 1_000_000,
4654 timestamp: 1,
4655 base_fee_per_gas: Some(base_fee),
4656 withdrawals_root: Some(H256::zero()),
4657 requests_hash: Some(*DEFAULT_REQUESTS_HASH),
4658 parent_beacon_block_root: None,
4660 ..Default::default()
4661 };
4662 let body = BlockBody {
4663 transactions: vec![tx],
4664 ommers: vec![],
4665 withdrawals: Some(vec![]),
4666 };
4667 Block::new(header, body)
4668 }
4669
4670 fn make_gen_db(chain_config: ChainConfig) -> GeneralizedDatabase {
4671 let sender = sender_address();
4672 let stop = Bytes::from_static(STOP_BYTECODE);
4673 let mock = MockDB::new(chain_config)
4674 .with_account(sender, U256::from(1_000_000_000u64), None)
4676 .with_account(
4678 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
4679 U256::zero(),
4680 Some(stop.clone()),
4681 )
4682 .with_account(
4683 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
4684 U256::zero(),
4685 Some(stop.clone()),
4686 )
4687 .with_account(
4689 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
4690 U256::zero(),
4691 Some(stop.clone()),
4692 )
4693 .with_account(
4694 BUILDER_EXIT_CONTRACT_ADDRESS.address,
4695 U256::zero(),
4696 Some(stop),
4697 );
4698 GeneralizedDatabase::new(Arc::new(mock))
4699 }
4700
4701 const SSTORE_CLEAR_BYTECODE: &[u8] = &[0x60, 0x00, 0x60, 0x00, 0x55, 0x00];
4706
4707 fn signed_call_tx(to: Address, nonce: u64, base_fee: u64, gas_limit: u64) -> Transaction {
4709 use k256::ecdsa::SigningKey;
4710
4711 let tx = EIP1559Transaction {
4712 chain_id: 1,
4713 nonce,
4714 max_priority_fee_per_gas: 1,
4715 max_fee_per_gas: base_fee + 10,
4716 gas_limit,
4717 to: TxKind::Call(to),
4718 value: U256::zero(),
4719 ..Default::default()
4720 };
4721
4722 let mut buf = vec![0x02u8];
4723 tx.encode_payload(&mut buf);
4724 let hash = keccak(&buf);
4725
4726 let signing_key = SigningKey::from_bytes(&PRIVATE_KEY.into()).expect("valid key");
4727 let (sig, rid) = signing_key
4728 .sign_prehash_recoverable(&hash.0)
4729 .expect("sign ok");
4730 let sig_bytes = sig.to_bytes();
4731
4732 Transaction::EIP1559Transaction(EIP1559Transaction {
4733 signature_r: U256::from_big_endian(&sig_bytes[..32]),
4734 signature_s: U256::from_big_endian(&sig_bytes[32..]),
4735 signature_y_parity: rid.to_byte() != 0,
4736 ..tx
4737 })
4738 }
4739
4740 fn make_gen_db_sstore_clear(
4743 chain_config: ChainConfig,
4744 contract_addr: Address,
4745 ) -> GeneralizedDatabase {
4746 let sender = sender_address();
4747 let stop = Bytes::from_static(STOP_BYTECODE);
4748 let mock = MockDB::new(chain_config)
4749 .with_account(sender, U256::from(100_000_000_000u64), None)
4750 .with_account(
4751 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS.address,
4752 U256::zero(),
4753 Some(stop.clone()),
4754 )
4755 .with_account(
4756 CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS.address,
4757 U256::zero(),
4758 Some(stop.clone()),
4759 )
4760 .with_account(
4762 BUILDER_DEPOSIT_CONTRACT_ADDRESS.address,
4763 U256::zero(),
4764 Some(stop.clone()),
4765 )
4766 .with_account(
4767 BUILDER_EXIT_CONTRACT_ADDRESS.address,
4768 U256::zero(),
4769 Some(stop),
4770 )
4771 .with_account(
4772 contract_addr,
4773 U256::zero(),
4774 Some(Bytes::from_static(SSTORE_CLEAR_BYTECODE)),
4775 )
4776 .with_storage(contract_addr, H256::zero(), U256::one());
4778 GeneralizedDatabase::new(Arc::new(mock))
4779 }
4780
4781 #[test]
4784 fn test_burned_fees_block_aggregate() {
4785 use std::sync::atomic::AtomicUsize;
4786 use std::sync::mpsc;
4787
4788 const BASE_FEE: u64 = 10;
4789 let recipient = Address::from_low_u64_be(0xBEEF);
4797 let tx = signed_eth_transfer(recipient, 0, BASE_FEE);
4798 let block = make_block_with_tx(tx, BASE_FEE);
4799 let crypto = NativeCrypto;
4800
4801 let mut db1 = make_gen_db(lstar_chain_config());
4803 let (result, _bal) = LEVM::execute_block(&block, &mut db1, VMType::L1, &crypto, None)
4804 .expect("execute_block succeeded");
4805 let cumulative = result
4806 .receipts
4807 .last()
4808 .map(|r| r.cumulative_gas_used)
4809 .unwrap_or(0);
4810 let expected = compute_burned_fees(BASE_FEE, cumulative, 0, 0);
4811 assert_eq!(
4812 result.burned_fees,
4813 Some(expected),
4814 "execute_block: burned_fees must equal base_fee * post-refund gas ({cumulative})"
4815 );
4816
4817 let mut db2 = make_gen_db(lstar_chain_config());
4819 let (tx_sender, _rx) = mpsc::channel();
4820 let queue_len = AtomicUsize::new(0);
4821 let tx2 = signed_eth_transfer(recipient, 0, BASE_FEE);
4822 let block2 = make_block_with_tx(tx2, BASE_FEE);
4823 let (result_pipeline, _bal) = LEVM::execute_block_pipeline(
4824 &block2,
4825 &mut db2,
4826 VMType::L1,
4827 Some(tx_sender),
4828 &queue_len,
4829 &crypto,
4830 None, false,
4832 None,
4833 )
4834 .expect("execute_block_pipeline succeeded");
4835 assert_eq!(
4836 result_pipeline.burned_fees, result.burned_fees,
4837 "execute_block_pipeline: burned_fees must match execute_block (production/verification consistency)"
4838 );
4839
4840 let tx3 = signed_eth_transfer(recipient, 0, BASE_FEE);
4842 let block3 = make_block_with_tx(tx3, BASE_FEE);
4843 let mut db3 = make_gen_db(amsterdam_chain_config());
4844 let (result_pre_lstar, _) =
4845 LEVM::execute_block(&block3, &mut db3, VMType::L1, &crypto, None)
4846 .expect("pre-LStar execute_block succeeded");
4847 assert_eq!(
4848 result_pre_lstar.burned_fees, None,
4849 "pre-LStar: burned_fees must be None"
4850 );
4851 }
4852
4853 #[test]
4870 fn test_burned_fees_post_refund_discriminating() {
4871 use std::sync::atomic::AtomicUsize;
4872 use std::sync::mpsc;
4873
4874 const BASE_FEE: u64 = 10;
4875 const TX_GAS_LIMIT: u64 = 100_000;
4877
4878 let contract_addr = Address::from_low_u64_be(0xC0DEBEEF);
4879 let crypto = NativeCrypto;
4880
4881 let tx_a = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4883 let block_a = make_block_with_tx(tx_a, BASE_FEE);
4884 let mut db_a = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4885 let (result_a, _) = LEVM::execute_block(&block_a, &mut db_a, VMType::L1, &crypto, None)
4886 .expect("execute_block with SSTORE clear succeeded");
4887
4888 let cumulative_a = result_a.receipts[0].cumulative_gas_used;
4889 assert!(
4891 result_a.block_gas_used > cumulative_a,
4892 "SSTORE refund must cause block_gas_used ({}) > cumulative_gas_used ({})",
4893 result_a.block_gas_used,
4894 cumulative_a,
4895 );
4896 assert_eq!(
4898 result_a.burned_fees,
4899 Some(BASE_FEE * cumulative_a),
4900 "execute_block: burned_fees must use post-refund gas (cumulative={}, block_gas_used={})",
4901 cumulative_a,
4902 result_a.block_gas_used,
4903 );
4904
4905 let tx_b = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4907 let block_b = make_block_with_tx(tx_b, BASE_FEE);
4908 let mut db_b = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4909 let (tx_sender_b, _rx_b) = mpsc::channel();
4910 let queue_len_b = AtomicUsize::new(0);
4911 let (result_b, _) = LEVM::execute_block_pipeline(
4912 &block_b,
4913 &mut db_b,
4914 VMType::L1,
4915 Some(tx_sender_b),
4916 &queue_len_b,
4917 &crypto,
4918 None, false,
4920 None,
4921 )
4922 .expect("execute_block_pipeline sequential with SSTORE clear succeeded");
4923
4924 let cumulative_b = result_b.receipts[0].cumulative_gas_used;
4925 assert!(
4926 result_b.block_gas_used > cumulative_b,
4927 "sequential pipeline: SSTORE refund must be present",
4928 );
4929 assert_eq!(
4930 result_b.burned_fees,
4931 Some(BASE_FEE * cumulative_b),
4932 "execute_block_pipeline (sequential): burned_fees must use post-refund gas",
4933 );
4934
4935 let tx_c = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4938 let block_c = make_block_with_tx(tx_c, BASE_FEE);
4939 let mut db_c1 = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4940 let (_, bal_c) = LEVM::execute_block(&block_c, &mut db_c1, VMType::L1, &crypto, None)
4941 .expect("BAL generation succeeded");
4942 let bal_c = bal_c.expect("LStar must produce a BAL");
4943
4944 let mut db_c2 = make_gen_db_sstore_clear(lstar_chain_config(), contract_addr);
4945 let (tx_sender_c, _rx_c) = mpsc::channel();
4946 let queue_len_c = AtomicUsize::new(0);
4947 let (result_c, _) = LEVM::execute_block_pipeline(
4948 &block_c,
4949 &mut db_c2,
4950 VMType::L1,
4951 Some(tx_sender_c),
4952 &queue_len_c,
4953 &crypto,
4954 Some(std::sync::Arc::new(bal_c)), true,
4956 None,
4957 )
4958 .expect("execute_block_pipeline parallel with SSTORE clear succeeded");
4959
4960 let cumulative_c = result_c.receipts[0].cumulative_gas_used;
4961 assert!(
4962 result_c.block_gas_used > cumulative_c,
4963 "parallel pipeline: SSTORE refund must be present",
4964 );
4965 assert_eq!(
4966 result_c.burned_fees,
4967 Some(BASE_FEE * cumulative_c),
4968 "execute_block_pipeline (parallel): burned_fees must use post-refund gas",
4969 );
4970
4971 assert_eq!(
4973 result_a.burned_fees, result_b.burned_fees,
4974 "execute_block vs sequential pipeline: burned_fees must match",
4975 );
4976 assert_eq!(
4977 result_a.burned_fees, result_c.burned_fees,
4978 "execute_block vs parallel pipeline: burned_fees must match",
4979 );
4980
4981 let tx_d = signed_call_tx(contract_addr, 0, BASE_FEE, TX_GAS_LIMIT);
4983 let block_d = make_block_with_tx(tx_d, BASE_FEE);
4984 let mut db_d = make_gen_db_sstore_clear(amsterdam_chain_config(), contract_addr);
4985 let (result_d, _) = LEVM::execute_block(&block_d, &mut db_d, VMType::L1, &crypto, None)
4986 .expect("pre-LStar with SSTORE clear succeeded");
4987 assert_eq!(
4988 result_d.burned_fees, None,
4989 "pre-LStar: burned_fees must be None"
4990 );
4991 }
4992}
4993
4994#[cfg(test)]
4995mod simulated_tx_encoding_tests {
4996 use super::*;
5006 use ethrex_common::types::GenericTransaction;
5007 use ethrex_rlp::encode::RLPEncode;
5008
5009 fn signed_equivalent(gas_limit: u64, data: Bytes) -> Transaction {
5012 Transaction::EIP1559Transaction(EIP1559Transaction {
5013 chain_id: 9_999,
5014 nonce: 42,
5015 max_priority_fee_per_gas: 1_000_000_000,
5016 max_fee_per_gas: 2_000_000_000,
5017 gas_limit,
5018 to: TxKind::Create,
5019 value: U256::from(1u64),
5020 data,
5021 access_list: vec![],
5022 signature_y_parity: true,
5023 signature_r: U256::MAX,
5024 signature_s: U256::MAX,
5025 ..Default::default()
5026 })
5027 }
5028
5029 fn generic(gas_limit: u64, data: Bytes) -> GenericTransaction {
5030 GenericTransaction {
5031 to: TxKind::Create,
5032 value: U256::from(1u64),
5033 input: data,
5034 nonce: Some(42),
5035 gas: Some(gas_limit),
5036 max_fee_per_gas: Some(2_000_000_000),
5037 max_priority_fee_per_gas: Some(1_000_000_000),
5038 chain_id: Some(9_999),
5039 ..Default::default()
5040 }
5041 }
5042
5043 #[test]
5044 fn simulated_tx_never_encodes_shorter_than_the_signed_one() {
5045 for (gas_limit, data) in [
5046 (21_000u64, Bytes::new()),
5047 (150_000, Bytes::from_static(&[0x60, 0x00, 0x60, 0x00])),
5048 (5_000_000, Bytes::from(vec![0xab; 4_096])),
5049 ] {
5050 let simulated = generic_tx_to_transaction(&generic(gas_limit, data.clone()))
5051 .expect("conversion should succeed");
5052 let signed = signed_equivalent(gas_limit, data.clone());
5053 assert!(
5054 simulated.length() >= signed.length(),
5055 "simulated tx encodes {} bytes, signed one {} — the L2 would under-reserve \
5056 the L1 fee by the difference (gas_limit {gas_limit}, {} data bytes)",
5057 simulated.length(),
5058 signed.length(),
5059 data.len(),
5060 );
5061 }
5062 }
5063}