Skip to main content

ethrex_vm/backends/
mod.rs

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