Skip to main content

ethrex_vm/backends/
mod.rs

1pub mod levm;
2use levm::LEVM;
3
4use crate::db::{DynVmDatabase, VmDatabase};
5use crate::errors::EvmError;
6use crate::execution_result::ExecutionResult;
7use ethrex_common::types::block_access_list::BlockAccessList;
8use ethrex_common::types::requests::Requests;
9use ethrex_common::types::{
10    AccessList, AccountUpdate, Block, BlockHeader, Fork, GenericTransaction, Receipt, Transaction,
11    Withdrawal,
12};
13use ethrex_common::{Address, types::fee_config::FeeConfig};
14use ethrex_crypto::Crypto;
15pub use ethrex_levm::StatelessValidator;
16pub use ethrex_levm::call_frame::CallFrameBackup;
17use ethrex_levm::db::gen_db::GeneralizedDatabase;
18pub use ethrex_levm::db::{BLOATED_BATCH_THRESHOLD, CachingDatabase, Database as LevmDatabase};
19pub use ethrex_levm::errors::{self, InternalError, VMError};
20use ethrex_levm::errors::{ExecutionReport, TxResult};
21pub use ethrex_levm::vm::VMType;
22use std::sync::Arc;
23use std::sync::atomic::AtomicUsize;
24use std::sync::mpsc::Sender;
25use tracing::instrument;
26
27#[derive(Clone)]
28pub struct Evm {
29    pub db: GeneralizedDatabase,
30    pub vm_type: VMType,
31    pub crypto: Arc<dyn Crypto>,
32    pub stateless_validator: Option<Arc<dyn StatelessValidator>>,
33}
34
35impl core::fmt::Debug for Evm {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        write!(f, "LEVM",)
38    }
39}
40
41impl Evm {
42    /// Creates a new EVM instance, but with block hash in zero, so if we want to execute a block or transaction we have to set it.
43    pub fn new_for_l1(db: impl VmDatabase + 'static, crypto: Arc<dyn Crypto>) -> Self {
44        let wrapped_db: DynVmDatabase = Box::new(db);
45        Evm {
46            db: GeneralizedDatabase::new(Arc::new(wrapped_db)),
47            vm_type: VMType::L1,
48            crypto,
49            stateless_validator: None,
50        }
51    }
52
53    pub fn new_for_l2(
54        db: impl VmDatabase + 'static,
55        fee_config: FeeConfig,
56        crypto: Arc<dyn Crypto>,
57    ) -> Result<Self, EvmError> {
58        let wrapped_db: DynVmDatabase = Box::new(db);
59
60        let evm = Evm {
61            db: GeneralizedDatabase::new(Arc::new(wrapped_db)),
62            vm_type: VMType::L2(fee_config),
63            crypto,
64            stateless_validator: None,
65        };
66
67        Ok(evm)
68    }
69
70    pub fn new_from_db_for_l1(
71        store: Arc<impl LevmDatabase + 'static>,
72        crypto: Arc<dyn Crypto>,
73    ) -> Self {
74        Self::_new_from_db(store, VMType::L1, crypto)
75    }
76
77    pub fn new_from_db_for_l2(
78        store: Arc<impl LevmDatabase + 'static>,
79        fee_config: FeeConfig,
80        crypto: Arc<dyn Crypto>,
81    ) -> Self {
82        Self::_new_from_db(store, VMType::L2(fee_config), crypto)
83    }
84
85    fn _new_from_db(
86        store: Arc<impl LevmDatabase + 'static>,
87        vm_type: VMType,
88        crypto: Arc<dyn Crypto>,
89    ) -> Self {
90        Evm {
91            db: GeneralizedDatabase::new(store),
92            vm_type,
93            crypto,
94            stateless_validator: None,
95        }
96    }
97
98    /// Execute a block and return the execution result.
99    ///
100    /// Also records and returns the Block Access List (EIP-7928) for Amsterdam+ forks.
101    /// The BAL will be `None` for pre-Amsterdam forks.
102    pub fn execute_block(
103        &mut self,
104        block: &Block,
105    ) -> Result<(BlockExecutionResult, Option<BlockAccessList>), EvmError> {
106        LEVM::execute_block(
107            block,
108            &mut self.db,
109            self.vm_type,
110            self.crypto.as_ref(),
111            self.stateless_validator.as_deref(),
112        )
113    }
114
115    #[instrument(
116        level = "trace",
117        name = "Block execution",
118        skip_all,
119        fields(namespace = "block_execution")
120    )]
121    pub fn execute_block_pipeline(
122        &mut self,
123        block: &Block,
124        merkleizer: Option<Sender<Vec<AccountUpdate>>>,
125        queue_length: &AtomicUsize,
126        bal: Option<Arc<BlockAccessList>>,
127        bal_parallel_exec_enabled: bool,
128    ) -> Result<(BlockExecutionResult, Option<BlockAccessList>), EvmError> {
129        LEVM::execute_block_pipeline(
130            block,
131            &mut self.db,
132            self.vm_type,
133            merkleizer,
134            queue_length,
135            self.crypto.as_ref(),
136            bal,
137            bal_parallel_exec_enabled,
138            self.stateless_validator.as_deref(),
139        )
140    }
141
142    /// Wraps [LEVM::execute_tx].
143    /// Updates `remaining_gas` (pre-refund) for block gas accounting and
144    /// `cumulative_gas_spent` (post-refund) for receipt cumulative tracking.
145    /// Returns (Receipt, gas_spent) where gas_spent is post-refund for block value calculation.
146    #[allow(clippy::too_many_arguments)]
147    pub fn execute_tx(
148        &mut self,
149        tx: &Transaction,
150        block_header: &BlockHeader,
151        cumulative_gas_spent: &mut u64,
152        sender: Address,
153    ) -> Result<(Receipt, ExecutionReport), EvmError> {
154        let mut execution_report = LEVM::execute_tx(
155            tx,
156            sender,
157            block_header,
158            &mut self.db,
159            self.vm_type,
160            self.crypto.as_ref(),
161            self.stateless_validator.as_deref(),
162        )?;
163
164        // Track cumulative post-refund gas for receipt
165        *cumulative_gas_spent += execution_report.gas_spent;
166
167        let mut receipt = Receipt::new(
168            tx.tx_type(),
169            execution_report.is_success(),
170            *cumulative_gas_spent,
171            execution_report.logs.clone(),
172        );
173
174        // For frame transactions, populate payer and per-frame receipts
175        if matches!(tx, Transaction::FrameTransaction(_)) {
176            receipt.payer = execution_report.payer_address;
177            receipt.frame_receipts = execution_report.frame_results.take().map(|results| {
178                results
179                    .into_iter()
180                    .map(
181                        |(status, gas_used, logs)| ethrex_common::types::FrameReceipt {
182                            status,
183                            gas_used,
184                            logs,
185                        },
186                    )
187                    .collect()
188            });
189        }
190
191        Ok((receipt, execution_report))
192    }
193
194    pub fn undo_last_tx(&mut self) -> Result<(), EvmError> {
195        LEVM::undo_last_tx(&mut self.db)
196    }
197
198    /// Wraps [LEVM::beacon_root_contract_call], [LEVM::process_block_hash_history].
199    /// This function is used to run/apply all the system contracts to the state.
200    pub fn apply_system_calls(&mut self, block_header: &BlockHeader) -> Result<(), EvmError> {
201        let chain_config = self.db.store.get_chain_config()?;
202        let fork = chain_config.fork(block_header.timestamp);
203
204        // EIP-8141: the expiry verifier predeploy must exist from Hegota
205        // activation onward. Idempotent install for the
206        // payload-build path; the block-import path is hooked in prepare_block.
207        if fork >= Fork::Hegota && matches!(self.vm_type, VMType::L1) {
208            LEVM::install_expiry_verifier_code(&mut self.db, self.crypto.as_ref())?;
209        }
210
211        if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun {
212            LEVM::beacon_root_contract_call(
213                block_header,
214                &mut self.db,
215                self.vm_type,
216                self.crypto.as_ref(),
217            )?;
218        }
219
220        if fork >= Fork::Prague {
221            LEVM::process_block_hash_history(
222                block_header,
223                &mut self.db,
224                self.vm_type,
225                self.crypto.as_ref(),
226            )?;
227        }
228
229        Ok(())
230    }
231
232    /// Wraps the [LEVM::get_state_transitions] which gathers the information from a [CacheDB].
233    /// The output is `Vec<AccountUpdate>`.
234    pub fn get_state_transitions(&mut self) -> Result<Vec<AccountUpdate>, EvmError> {
235        LEVM::get_state_transitions(&mut self.db)
236    }
237
238    /// Wraps [LEVM::process_withdrawals].
239    /// Applies the withdrawals to the state or the block_chache if using [LEVM].
240    pub fn process_withdrawals(&mut self, withdrawals: &[Withdrawal]) -> Result<(), EvmError> {
241        LEVM::process_withdrawals(&mut self.db, withdrawals)
242    }
243
244    pub fn extract_requests(
245        &mut self,
246        receipts: &[Receipt],
247        header: &BlockHeader,
248    ) -> Result<Vec<Requests>, EvmError> {
249        levm::extract_all_requests_levm(
250            receipts,
251            &mut self.db,
252            header,
253            self.vm_type,
254            self.crypto.as_ref(),
255        )
256    }
257
258    /// Takes the Block Access List (BAL) from the database if recording was enabled.
259    /// Returns `None` if BAL recording was not enabled.
260    pub fn take_bal(&mut self) -> Option<BlockAccessList> {
261        self.db.take_bal()
262    }
263
264    /// Enables BAL (Block Access List) recording for EIP-7928.
265    pub fn enable_bal_recording(&mut self) {
266        self.db.enable_bal_recording();
267    }
268
269    /// Sets the current block access index for BAL recording per EIP-7928 spec (uint32).
270    pub fn set_bal_index(&mut self, index: u32) {
271        self.db.set_bal_index(index);
272    }
273
274    pub fn simulate_tx_from_generic(
275        &mut self,
276        tx: &GenericTransaction,
277        header: &BlockHeader,
278    ) -> Result<ExecutionResult, EvmError> {
279        LEVM::simulate_tx_from_generic(
280            tx,
281            header,
282            &mut self.db,
283            self.vm_type,
284            self.crypto.as_ref(),
285            self.stateless_validator.as_deref(),
286        )
287    }
288
289    /// EIP-8141 mempool validation-prefix simulation (local peer policy).
290    /// Wraps [`LEVM::simulate_frame_validation_prefix`].
291    pub fn simulate_frame_validation_prefix(
292        &mut self,
293        tx: &Transaction,
294        header: &BlockHeader,
295        prefix: &ethrex_common::types::ValidationPrefix,
296        canonical_paymaster_code_hash: Option<ethrex_common::H256>,
297    ) -> Result<FrameValidationOutcome, EvmError> {
298        LEVM::simulate_frame_validation_prefix(
299            tx,
300            header,
301            &mut self.db,
302            self.vm_type,
303            self.crypto.as_ref(),
304            prefix,
305            canonical_paymaster_code_hash,
306        )
307    }
308
309    pub fn create_access_list(
310        &mut self,
311        tx: &GenericTransaction,
312        header: &BlockHeader,
313    ) -> Result<(u64, AccessList, Option<String>), EvmError> {
314        let result = {
315            LEVM::create_access_list(
316                tx.clone(),
317                header,
318                &mut self.db,
319                self.vm_type,
320                self.crypto.as_ref(),
321            )?
322        };
323
324        match result {
325            (
326                ExecutionResult::Success {
327                    gas_used,
328                    gas_refunded: _,
329                    logs: _,
330                    output: _,
331                },
332                access_list,
333            ) => Ok((gas_used, access_list, None)),
334            (
335                ExecutionResult::Revert {
336                    gas_used,
337                    output: _,
338                },
339                access_list,
340            ) => Ok((
341                gas_used,
342                access_list,
343                Some("Transaction Reverted".to_string()),
344            )),
345            (ExecutionResult::Halt { reason, gas_used }, access_list) => {
346                Ok((gas_used, access_list, Some(reason)))
347            }
348        }
349    }
350}
351
352/// Compute burned fees for a block (EIP-8079, LStar+).
353///
354/// Formula: `base_fee_per_gas * gas_spent + blob_base_fee * blob_gas_used`
355///
356/// `gas_spent` is the post-refund Σ gas_spent across all transactions (= the last
357/// receipt's `cumulative_gas_used`).  Per EIP-8079 the fee basis is post-refund:
358/// users receive EIP-3529 refunds, so only the net gas actually charged is burned.
359/// Production and verification always compute the identical value because both paths
360/// build identical receipts from the same transactions and initial state.
361///
362/// Uses saturating arithmetic; saturates at `u64::MAX` on overflow.
363pub fn compute_burned_fees(
364    base_fee_per_gas: u64,
365    gas_spent: u64,
366    blob_base_fee: u64,
367    blob_gas_used: u64,
368) -> u64 {
369    base_fee_per_gas
370        .saturating_mul(gas_spent)
371        .saturating_add(blob_base_fee.saturating_mul(blob_gas_used))
372}
373
374/// Outcome of an EIP-8141 mempool validation-prefix simulation
375/// ([`Evm::simulate_frame_validation_prefix`]). A local peer policy result, not
376/// a consensus value.
377#[derive(Clone, Debug)]
378pub struct FrameValidationOutcome {
379    /// Whether the validation prefix passed every trace rule and produced a
380    /// payer without reverting within the verify-gas budget.
381    pub passed: bool,
382    /// The first validation-trace violation observed, if any (rendered to a
383    /// string for the admission error). `None` when `passed` is true.
384    pub violation: Option<String>,
385    /// An upper bound on what the payer can be charged in any block that may
386    /// include this transaction, used by the paymaster reservation accounting.
387    /// Not the consensus `max_cost` of TXPARAM 0x06: see
388    /// [`Evm::simulate_frame_validation_prefix`]'s reservation-ceiling helper for
389    /// why the blob term is priced at `max_fee_per_blob_gas` here.
390    pub reservation_ceiling: ethrex_common::U256,
391    /// The paymaster accessed by the prefix and whether its code matched the
392    /// canonical paymaster hash. `None` when no distinct paymaster was
393    /// identified (e.g. self-funded self_verify).
394    pub accessed_paymaster: Option<(Address, bool)>,
395    /// Sender storage slots touched during the prefix, recorded for the
396    /// admission-time revalidation affected-set (Phase 3).
397    pub touched_sender_slots: Vec<ethrex_common::H256>,
398}
399
400#[derive(Clone, Debug)]
401pub struct BlockExecutionResult {
402    pub receipts: Vec<Receipt>,
403    pub requests: Vec<Requests>,
404    /// Block gas used (PRE-REFUND for Amsterdam+ per EIP-7778).
405    /// This differs from receipt cumulative_gas_used which is POST-REFUND.
406    pub block_gas_used: u64,
407    /// Total base-fee + blob-base-fee burned in this block (EIP-8079, LStar+).
408    /// `None` for pre-LStar forks.
409    /// Formula: `base_fee_per_gas * Σgas_spent + blob_base_fee * blob_gas_used`,
410    /// where `Σgas_spent` = `receipts.last().cumulative_gas_used` (post-refund; per EIP-8079).
411    /// Uses saturating arithmetic; saturates at u64::MAX on extreme values.
412    pub burned_fees: Option<u64>,
413    /// Per-tx gas-dimension breakdown. Populated by `execute_block`; left empty by
414    /// L2 producer / committer paths that build a `BlockExecutionResult` from
415    /// re-derived data. Used by `validate_gas_used` mismatch logging to localize
416    /// which tx and which dimension caused the divergence.
417    pub tx_gas_breakdowns: Vec<TxGasBreakdown>,
418}
419
420/// Per-tx gas-dimension snapshot captured at the block-execution boundary.
421/// All fields are pre-refund except `gas_spent` and `gas_refunded` which are
422/// the user-pays (post-refund) values.
423#[derive(Clone, Debug)]
424pub struct TxGasBreakdown {
425    pub tx_index: usize,
426    pub tx_hash: ethrex_common::H256,
427    pub status: TxStatus,
428    /// Pre-refund gas used (block-level dimension under EIP-7778).
429    pub gas_used: u64,
430    /// Post-refund gas paid by the sender.
431    pub gas_spent: u64,
432    pub gas_refunded: u64,
433    /// EIP-8037 state-gas portion of `gas_used` (Amsterdam+); 0 pre-Amsterdam.
434    pub state_gas_used: u64,
435    /// `gas_used - state_gas_used`. Saturating to avoid underflow on edge cases.
436    pub regular_gas_used: u64,
437}
438
439#[derive(Clone, Copy, Debug, PartialEq, Eq)]
440pub enum TxStatus {
441    Success,
442    Revert,
443    Halt,
444}
445
446impl core::fmt::Display for TxStatus {
447    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
448        match self {
449            TxStatus::Success => f.write_str("success"),
450            TxStatus::Revert => f.write_str("revert"),
451            TxStatus::Halt => f.write_str("halt"),
452        }
453    }
454}
455
456impl TxGasBreakdown {
457    pub fn from_report(
458        tx_index: usize,
459        tx_hash: ethrex_common::H256,
460        report: &ExecutionReport,
461    ) -> Self {
462        let status = match &report.result {
463            TxResult::Success => TxStatus::Success,
464            TxResult::Revert(err) if err.is_revert_opcode() => TxStatus::Revert,
465            TxResult::Revert(_) => TxStatus::Halt,
466        };
467        Self {
468            tx_index,
469            tx_hash,
470            status,
471            gas_used: report.gas_used,
472            gas_spent: report.gas_spent,
473            gas_refunded: report.gas_refunded,
474            state_gas_used: report.state_gas_used,
475            regular_gas_used: report.gas_used.saturating_sub(report.state_gas_used),
476        }
477    }
478}
479
480/// Emit a structured per-tx gas-dimension dump. Called from the block-validation
481/// site when `block_gas_used` disagrees with `header.gas_used`. If `breakdowns` is
482/// empty (paths that don't populate it, e.g. L2 producer), a one-liner is logged.
483pub fn log_gas_used_mismatch(
484    breakdowns: &[TxGasBreakdown],
485    block_number: u64,
486    actual: u64,
487    expected: u64,
488) {
489    let delta = actual as i128 - expected as i128;
490    if breakdowns.is_empty() {
491        ::tracing::error!(
492            block = block_number,
493            actual,
494            expected,
495            delta,
496            "block gas_used mismatch (no per-tx breakdown available on this path)",
497        );
498        return;
499    }
500    let sum_regular: u64 = breakdowns.iter().map(|b| b.regular_gas_used).sum();
501    let sum_state: u64 = breakdowns.iter().map(|b| b.state_gas_used).sum();
502    let sum_refunded: u64 = breakdowns.iter().map(|b| b.gas_refunded).sum();
503    ::tracing::error!(
504        block = block_number,
505        actual,
506        expected,
507        delta,
508        n_txs = breakdowns.len(),
509        sum_regular,
510        sum_state,
511        max_dim = sum_regular.max(sum_state),
512        sum_refunded,
513        "block gas_used mismatch",
514    );
515    for b in breakdowns {
516        ::tracing::error!(
517            tx_idx = b.tx_index,
518            tx_hash = %b.tx_hash,
519            status = %b.status,
520            gas_used = b.gas_used,
521            regular = b.regular_gas_used,
522            state = b.state_gas_used,
523            gas_spent = b.gas_spent,
524            gas_refunded = b.gas_refunded,
525            "  tx breakdown",
526        );
527    }
528}