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