Skip to main content

ethrex_levm/
errors.rs

1use bytes::Bytes;
2use derive_more::derive::Display;
3use ethrex_common::{
4    Address, H256, U256,
5    types::{FakeExponentialError, Log},
6};
7use serde::{Deserialize, Serialize};
8use thiserror;
9
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize, Display)]
11pub enum VMError {
12    /// Errors that break execution, they shouldn't ever happen. Contains subcategory `DatabaseError`.
13    Internal(#[from] InternalError),
14    /// Returned when a transaction doesn't pass all validations before executing.
15    TxValidation(#[from] TxValidationError),
16    /// Errors contemplated by the EVM, they revert and consume all gas of the current context.
17    ExceptionalHalt(#[from] ExceptionalHalt),
18    /// Revert Opcode called. It behaves like ExceptionalHalt, except it doesn't consume all gas left.
19    RevertOpcode,
20}
21
22impl VMError {
23    /// These errors are unexpected and indicate critical issues.
24    /// They should not cause a transaction to revert silently but instead fail loudly, propagating the error.
25    pub fn should_propagate(&self) -> bool {
26        matches!(self, VMError::Internal(_))
27    }
28
29    /// Error triggered by revert opcode. This error doesn't consume all gas left in context.
30    pub fn is_revert_opcode(&self) -> bool {
31        matches!(self, VMError::RevertOpcode)
32    }
33}
34
35impl From<DatabaseError> for VMError {
36    fn from(err: DatabaseError) -> Self {
37        VMError::Internal(InternalError::Database(err))
38    }
39}
40
41impl From<PrecompileError> for VMError {
42    fn from(err: PrecompileError) -> Self {
43        VMError::ExceptionalHalt(ExceptionalHalt::Precompile(err))
44    }
45}
46
47/// Useful to use ? in try_into, specially when slicing with known bounds to fixed size arrays,
48/// which is a error that never really happens.
49impl From<std::array::TryFromSliceError> for VMError {
50    fn from(_: std::array::TryFromSliceError) -> Self {
51        VMError::Internal(InternalError::TypeConversion)
52    }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
56pub enum ExceptionalHalt {
57    #[error("Stack Underflow")]
58    StackUnderflow,
59    #[error("Stack Overflow")]
60    StackOverflow,
61    #[error("Invalid Jump")]
62    InvalidJump,
63    #[error("Opcode Not Allowed In Static Context")]
64    OpcodeNotAllowedInStaticContext,
65    #[error("Invalid Contract Prefix")]
66    InvalidContractPrefix,
67    #[error("Very Large Number")]
68    VeryLargeNumber,
69    #[error("Invalid Opcode")]
70    InvalidOpcode,
71    #[error("Address Already Occupied")]
72    AddressAlreadyOccupied,
73    #[error("Contract Output Too Big")]
74    ContractOutputTooBig,
75    #[error("Offset out of bounds")]
76    OutOfBounds,
77    #[error("Out Of Gas")]
78    OutOfGas,
79    #[error("Precompile execution error: {0}")]
80    Precompile(#[from] PrecompileError),
81}
82
83// Error strings are attached to execution-spec-tests mapping https://github.com/ethereum/execution-spec-tests
84// If any change is made here without changing the mapper it will break some hive tests.
85#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
86pub enum TxValidationError {
87    #[error("Sender account {0} shouldn't be a contract")]
88    SenderNotEOA(Address),
89    #[error("Insufficient account funds")]
90    InsufficientAccountFunds,
91    #[error("Nonce is max")]
92    NonceIsMax,
93    #[error("Nonce mismatch: expected {expected}, got {actual}")]
94    NonceMismatch { expected: u64, actual: u64 },
95    #[error("Initcode size exceeded, max size: {max_size}, actual size: {actual_size}")]
96    InitcodeSizeExceeded { max_size: usize, actual_size: usize },
97    #[error("Priority fee {priority_fee} is greater than max fee per gas {max_fee_per_gas}")]
98    PriorityGreaterThanMaxFeePerGas {
99        priority_fee: U256,
100        max_fee_per_gas: U256,
101    },
102    #[error("Transaction gas limit lower than the minimum gas cost to execute the transaction")]
103    IntrinsicGasTooLow,
104    #[error("Transaction gas limit lower than the gas cost floor for calldata tokens")]
105    IntrinsicGasBelowFloorGasCost,
106    /// L2-only. Raised by the L2 hook's `reserve_l1_gas` when the gas limit cannot
107    /// cover the L1 data-availability gas reserved on top of the intrinsic cost or
108    /// the calldata floor. Distinct from [`TxValidationError::IntrinsicGasTooLow`]
109    /// on purpose: the reserved amount is derived from the L1 fee config and the
110    /// block's gas price, so this failure is TRANSIENT — the same transaction can
111    /// succeed in a later block once L1 fees drop. Consumers that classify errors
112    /// as permanent-vs-transient (the payload builders' `is_deterministic_invalid`)
113    /// rely on the two not being conflated.
114    #[error("Transaction gas limit cannot cover the reserved L1 data availability gas")]
115    L1GasReservationTooLow,
116    #[error(
117        "Gas allowance exceeded. Block gas limit: {block_gas_limit}, transaction gas limit: {tx_gas_limit}"
118    )]
119    GasAllowanceExceeded {
120        block_gas_limit: u64,
121        tx_gas_limit: u64,
122    },
123    #[error("Insufficient max fee per gas")]
124    InsufficientMaxFeePerGas,
125    #[error(
126        "Insufficient max fee per blob gas. Expected at least {base_fee_per_blob_gas}, got: {tx_max_fee_per_blob_gas}"
127    )]
128    InsufficientMaxFeePerBlobGas {
129        base_fee_per_blob_gas: U256,
130        tx_max_fee_per_blob_gas: U256,
131    },
132    #[error("Type 3 transactions are not supported before the Cancun fork")]
133    Type3TxPreFork,
134    #[error("Type 3 transaction without blobs")]
135    Type3TxZeroBlobs,
136    #[error("Invalid blob versioned hash")]
137    Type3TxInvalidBlobVersionedHash,
138    #[error(
139        "Blob count exceeded. Max blob count: {max_blob_count}, actual blob count: {actual_blob_count}"
140    )]
141    Type3TxBlobCountExceeded {
142        max_blob_count: usize,
143        actual_blob_count: usize,
144    },
145    #[error("Contract creation in blob transaction")]
146    Type3TxContractCreation,
147    #[error("Type 4 transactions are not supported before the Prague fork")]
148    Type4TxPreFork,
149    #[error("Empty authorization list in type 4 transaction")]
150    Type4TxAuthorizationListIsEmpty,
151    #[error("Contract creation in type 4 transaction")]
152    Type4TxContractCreation,
153    #[error("Frame transactions (EIP-8141) are not supported before the Hegota fork")]
154    FrameTxPreFork,
155    #[error("Gas limit price product overflow")]
156    GasLimitPriceProductOverflow,
157    #[error(
158        "Transaction gas limit exceeds maximum. Transaction hash: {tx_hash}, transaction gas limit: {tx_gas_limit}"
159    )]
160    TxMaxGasLimitExceeded { tx_hash: H256, tx_gas_limit: u64 },
161    #[error("Invalid frame transaction: VERIFY frame did not call APPROVE or payer not approved")]
162    InvalidFrameTransaction,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
166pub enum InternalError {
167    #[error("Arithmetic operation overflowed")]
168    Overflow,
169    #[error("Arithmetic operation underflowed")]
170    Underflow,
171    #[error("Cannot divide by zero")]
172    DivisionByZero,
173    #[error("Tried to convert one type to another")]
174    TypeConversion,
175    #[error("CallFrame not found")]
176    CallFrame,
177    #[error("Tried to slice non-existing data")]
178    Slicing,
179    #[error("Account not found when it should've been in the cache.")]
180    AccountNotFound,
181    #[error("Invalid precompile address. Tried to execute a precompile that does not exist.")]
182    InvalidPrecompileAddress,
183    #[error("Invalid Fork")]
184    InvalidFork,
185    #[error("Account should had been delegated")]
186    AccountNotDelegated,
187    #[error("No recipient found for privileged transaction")]
188    RecipientNotFoundForPrivilegedTransaction,
189    #[error("Memory Size Sverflow")]
190    MemorySizeOverflow,
191    #[error("Custom error: {0}")]
192    Custom(String),
193    /// Unexpected error when accessing the database, used in trait `Database`.
194    #[error("Database access error: {0}")]
195    Database(#[from] DatabaseError),
196    #[error("{0}")]
197    FakeExponentialError(#[from] FakeExponentialError),
198}
199
200impl InternalError {
201    pub fn msg(msg: &'static str) -> Self {
202        Self::Custom(msg.to_owned())
203    }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
207pub enum PrecompileError {
208    #[error("Error while parsing the calldata")]
209    ParsingInputError,
210    #[error("There is not enough gas to execute precompiled contract")]
211    NotEnoughGas,
212    #[error("Invalid point")]
213    InvalidPoint,
214    #[error("The point is not in the subgroup")]
215    PointNotInSubgroup,
216    #[error("The G1 point is not in the curve")]
217    BLS12381G1PointNotInCurve,
218    #[error("The G2 point is not in the curve")]
219    BLS12381G2PointNotInCurve,
220    #[error("Mod-exp base length is too large")]
221    ModExpBaseTooLarge,
222    #[error("Mod-exp exponent length is too large")]
223    ModExpExpTooLarge,
224    #[error("Mod-exp modulus length is too large")]
225    ModExpModulusTooLarge,
226    #[error("Coordinate Exceeds Field Modulus")]
227    CoordinateExceedsFieldModulus,
228    /// EXECUTE precompile: the L1-submitted block failed stateless validation.
229    /// Attacker-controllable: tx continues, forwarded gas is consumed, tx is includable.
230    #[error("EXECUTE: stateless validation failed")]
231    ExecuteValidationFailed,
232    /// EXECUTE precompile: the calldata is malformed or violates L2 constraints.
233    /// Attacker-controllable input: treat as CALL-level failure, not an internal invariant.
234    #[error("EXECUTE: invalid input for native-rollup execution")]
235    ExecuteInvalidInput,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
239pub enum DatabaseError {
240    #[error("{0}")]
241    Custom(String),
242}
243
244#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
245pub enum OpcodeResult {
246    Continue,
247    Halt,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
251pub enum TxResult {
252    Success,
253    Revert(VMError),
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct ExecutionReport {
258    pub result: TxResult,
259    /// Gas used before refunds (for block-level accounting).
260    /// Pre-EIP-7778: This is the post-refund gas.
261    /// Post-EIP-7778: This is the pre-refund gas.
262    pub gas_used: u64,
263    /// Gas spent after refunds (what the user actually pays).
264    /// This is always the post-refund gas value.
265    /// Pre-EIP-7778: Same as gas_used.
266    /// Post-EIP-7778: gas_used - refunds (capped).
267    pub gas_spent: u64,
268    pub gas_refunded: u64,
269    /// EIP-8037: State gas portion of gas_used (Amsterdam+).
270    /// Block gas_used = max(sum(regular_gas), sum(state_gas)).
271    pub state_gas_used: u64,
272    pub output: Bytes,
273    pub logs: Vec<Log>,
274    /// For frame transactions: the address that paid for gas
275    pub payer_address: Option<Address>,
276    /// For frame transactions: per-frame results (status, gas_used, logs).
277    /// `status` is a `FRAME_RECEIPT_STATUS_*` code (0 = failure, 1 = success,
278    /// 3 = skipped due to failed atomic batch, per EIP-8141 receipt encoding).
279    pub frame_results: Option<Vec<(u8, u64, Vec<Log>)>>,
280}
281
282impl ExecutionReport {
283    pub fn is_success(&self) -> bool {
284        matches!(self.result, TxResult::Success)
285    }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct ContextResult {
290    pub result: TxResult,
291    /// Gas used before refunds (for block-level accounting).
292    pub gas_used: u64,
293    /// Gas spent after refunds (what the user actually pays).
294    pub gas_spent: u64,
295    pub output: Bytes,
296}
297
298impl ContextResult {
299    pub fn is_success(&self) -> bool {
300        matches!(self.result, TxResult::Success)
301    }
302
303    /// Returns true if this result is an address collision error.
304    pub fn is_collision(&self) -> bool {
305        matches!(
306            self.result,
307            TxResult::Revert(VMError::ExceptionalHalt(
308                ExceptionalHalt::AddressAlreadyOccupied
309            ))
310        )
311    }
312
313    /// True if the failure was caused by the REVERT opcode (intentional revert).
314    /// Used to gate behaviour that differs between REVERT and ExceptionalHalt —
315    /// e.g. return data is propagated to the parent on REVERT only.
316    pub fn is_revert_opcode(&self) -> bool {
317        matches!(self.result, TxResult::Revert(VMError::RevertOpcode))
318    }
319}