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};
19use 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<&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 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 receipt = Receipt::new(
154            tx.tx_type(),
155            execution_report.is_success(),
156            *cumulative_gas_spent,
157            execution_report.logs.clone(),
158        );
159
160        Ok((receipt, execution_report))
161    }
162
163    pub fn undo_last_tx(&mut self) -> Result<(), EvmError> {
164        LEVM::undo_last_tx(&mut self.db)
165    }
166
167    /// Wraps [LEVM::beacon_root_contract_call], [LEVM::process_block_hash_history].
168    /// This function is used to run/apply all the system contracts to the state.
169    pub fn apply_system_calls(&mut self, block_header: &BlockHeader) -> Result<(), EvmError> {
170        let chain_config = self.db.store.get_chain_config()?;
171        let fork = chain_config.fork(block_header.timestamp);
172
173        if block_header.parent_beacon_block_root.is_some() && fork >= Fork::Cancun {
174            LEVM::beacon_root_contract_call(
175                block_header,
176                &mut self.db,
177                self.vm_type,
178                self.crypto.as_ref(),
179            )?;
180        }
181
182        if fork >= Fork::Prague {
183            LEVM::process_block_hash_history(
184                block_header,
185                &mut self.db,
186                self.vm_type,
187                self.crypto.as_ref(),
188            )?;
189        }
190
191        Ok(())
192    }
193
194    /// Wraps the [LEVM::get_state_transitions] which gathers the information from a [CacheDB].
195    /// The output is `Vec<AccountUpdate>`.
196    pub fn get_state_transitions(&mut self) -> Result<Vec<AccountUpdate>, EvmError> {
197        LEVM::get_state_transitions(&mut self.db)
198    }
199
200    /// Wraps [LEVM::process_withdrawals].
201    /// Applies the withdrawals to the state or the block_chache if using [LEVM].
202    pub fn process_withdrawals(&mut self, withdrawals: &[Withdrawal]) -> Result<(), EvmError> {
203        LEVM::process_withdrawals(&mut self.db, withdrawals)
204    }
205
206    pub fn extract_requests(
207        &mut self,
208        receipts: &[Receipt],
209        header: &BlockHeader,
210    ) -> Result<Vec<Requests>, EvmError> {
211        levm::extract_all_requests_levm(
212            receipts,
213            &mut self.db,
214            header,
215            self.vm_type,
216            self.crypto.as_ref(),
217        )
218    }
219
220    /// Takes the Block Access List (BAL) from the database if recording was enabled.
221    /// Returns `None` if BAL recording was not enabled.
222    pub fn take_bal(&mut self) -> Option<BlockAccessList> {
223        self.db.take_bal()
224    }
225
226    /// Enables BAL (Block Access List) recording for EIP-7928.
227    pub fn enable_bal_recording(&mut self) {
228        self.db.enable_bal_recording();
229    }
230
231    /// Sets the current block access index for BAL recording per EIP-7928 spec (uint32).
232    pub fn set_bal_index(&mut self, index: u32) {
233        self.db.set_bal_index(index);
234    }
235
236    pub fn simulate_tx_from_generic(
237        &mut self,
238        tx: &GenericTransaction,
239        header: &BlockHeader,
240    ) -> Result<ExecutionResult, EvmError> {
241        LEVM::simulate_tx_from_generic(tx, header, &mut self.db, self.vm_type, self.crypto.as_ref())
242    }
243
244    pub fn create_access_list(
245        &mut self,
246        tx: &GenericTransaction,
247        header: &BlockHeader,
248    ) -> Result<(u64, AccessList, Option<String>), EvmError> {
249        let result = {
250            LEVM::create_access_list(
251                tx.clone(),
252                header,
253                &mut self.db,
254                self.vm_type,
255                self.crypto.as_ref(),
256            )?
257        };
258
259        match result {
260            (
261                ExecutionResult::Success {
262                    gas_used,
263                    gas_refunded: _,
264                    logs: _,
265                    output: _,
266                },
267                access_list,
268            ) => Ok((gas_used, access_list, None)),
269            (
270                ExecutionResult::Revert {
271                    gas_used,
272                    output: _,
273                },
274                access_list,
275            ) => Ok((
276                gas_used,
277                access_list,
278                Some("Transaction Reverted".to_string()),
279            )),
280            (ExecutionResult::Halt { reason, gas_used }, access_list) => {
281                Ok((gas_used, access_list, Some(reason)))
282            }
283        }
284    }
285}
286
287#[derive(Clone, Debug)]
288pub struct BlockExecutionResult {
289    pub receipts: Vec<Receipt>,
290    pub requests: Vec<Requests>,
291    /// Block gas used (PRE-REFUND for Amsterdam+ per EIP-7778).
292    /// This differs from receipt cumulative_gas_used which is POST-REFUND.
293    pub block_gas_used: u64,
294    /// Per-tx gas-dimension breakdown. Populated by `execute_block`; left empty by
295    /// L2 producer / committer paths that build a `BlockExecutionResult` from
296    /// re-derived data. Used by `validate_gas_used` mismatch logging to localize
297    /// which tx and which dimension caused the divergence.
298    pub tx_gas_breakdowns: Vec<TxGasBreakdown>,
299}
300
301/// Per-tx gas-dimension snapshot captured at the block-execution boundary.
302/// All fields are pre-refund except `gas_spent` and `gas_refunded` which are
303/// the user-pays (post-refund) values.
304#[derive(Clone, Debug)]
305pub struct TxGasBreakdown {
306    pub tx_index: usize,
307    pub tx_hash: ethrex_common::H256,
308    pub status: TxStatus,
309    /// Pre-refund gas used (block-level dimension under EIP-7778).
310    pub gas_used: u64,
311    /// Post-refund gas paid by the sender.
312    pub gas_spent: u64,
313    pub gas_refunded: u64,
314    /// EIP-8037 state-gas portion of `gas_used` (Amsterdam+); 0 pre-Amsterdam.
315    pub state_gas_used: u64,
316    /// `gas_used - state_gas_used`. Saturating to avoid underflow on edge cases.
317    pub regular_gas_used: u64,
318}
319
320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
321pub enum TxStatus {
322    Success,
323    Revert,
324    Halt,
325}
326
327impl core::fmt::Display for TxStatus {
328    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
329        match self {
330            TxStatus::Success => f.write_str("success"),
331            TxStatus::Revert => f.write_str("revert"),
332            TxStatus::Halt => f.write_str("halt"),
333        }
334    }
335}
336
337impl TxGasBreakdown {
338    pub fn from_report(
339        tx_index: usize,
340        tx_hash: ethrex_common::H256,
341        report: &ExecutionReport,
342    ) -> Self {
343        let status = match &report.result {
344            TxResult::Success => TxStatus::Success,
345            TxResult::Revert(err) if err.is_revert_opcode() => TxStatus::Revert,
346            TxResult::Revert(_) => TxStatus::Halt,
347        };
348        Self {
349            tx_index,
350            tx_hash,
351            status,
352            gas_used: report.gas_used,
353            gas_spent: report.gas_spent,
354            gas_refunded: report.gas_refunded,
355            state_gas_used: report.state_gas_used,
356            regular_gas_used: report.gas_used.saturating_sub(report.state_gas_used),
357        }
358    }
359}
360
361/// Emit a structured per-tx gas-dimension dump. Called from the block-validation
362/// site when `block_gas_used` disagrees with `header.gas_used`. If `breakdowns` is
363/// empty (paths that don't populate it, e.g. L2 producer), a one-liner is logged.
364pub fn log_gas_used_mismatch(
365    breakdowns: &[TxGasBreakdown],
366    block_number: u64,
367    actual: u64,
368    expected: u64,
369) {
370    let delta = actual as i128 - expected as i128;
371    if breakdowns.is_empty() {
372        ::tracing::error!(
373            block = block_number,
374            actual,
375            expected,
376            delta,
377            "block gas_used mismatch (no per-tx breakdown available on this path)",
378        );
379        return;
380    }
381    let sum_regular: u64 = breakdowns.iter().map(|b| b.regular_gas_used).sum();
382    let sum_state: u64 = breakdowns.iter().map(|b| b.state_gas_used).sum();
383    let sum_refunded: u64 = breakdowns.iter().map(|b| b.gas_refunded).sum();
384    ::tracing::error!(
385        block = block_number,
386        actual,
387        expected,
388        delta,
389        n_txs = breakdowns.len(),
390        sum_regular,
391        sum_state,
392        max_dim = sum_regular.max(sum_state),
393        sum_refunded,
394        "block gas_used mismatch",
395    );
396    for b in breakdowns {
397        ::tracing::error!(
398            tx_idx = b.tx_index,
399            tx_hash = %b.tx_hash,
400            status = %b.status,
401            gas_used = b.gas_used,
402            regular = b.regular_gas_used,
403            state = b.state_gas_used,
404            gas_spent = b.gas_spent,
405            gas_refunded = b.gas_refunded,
406            "  tx breakdown",
407        );
408    }
409}