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