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    #[error(
107        "Gas allowance exceeded. Block gas limit: {block_gas_limit}, transaction gas limit: {tx_gas_limit}"
108    )]
109    GasAllowanceExceeded {
110        block_gas_limit: u64,
111        tx_gas_limit: u64,
112    },
113    #[error("Insufficient max fee per gas")]
114    InsufficientMaxFeePerGas,
115    #[error(
116        "Insufficient max fee per blob gas. Expected at least {base_fee_per_blob_gas}, got: {tx_max_fee_per_blob_gas}"
117    )]
118    InsufficientMaxFeePerBlobGas {
119        base_fee_per_blob_gas: U256,
120        tx_max_fee_per_blob_gas: U256,
121    },
122    #[error("Type 3 transactions are not supported before the Cancun fork")]
123    Type3TxPreFork,
124    #[error("Type 3 transaction without blobs")]
125    Type3TxZeroBlobs,
126    #[error("Invalid blob versioned hash")]
127    Type3TxInvalidBlobVersionedHash,
128    #[error(
129        "Blob count exceeded. Max blob count: {max_blob_count}, actual blob count: {actual_blob_count}"
130    )]
131    Type3TxBlobCountExceeded {
132        max_blob_count: usize,
133        actual_blob_count: usize,
134    },
135    #[error("Contract creation in blob transaction")]
136    Type3TxContractCreation,
137    #[error("Type 4 transactions are not supported before the Prague fork")]
138    Type4TxPreFork,
139    #[error("Empty authorization list in type 4 transaction")]
140    Type4TxAuthorizationListIsEmpty,
141    #[error("Contract creation in type 4 transaction")]
142    Type4TxContractCreation,
143    #[error("Frame transactions (EIP-8141) are not supported before the Hegota fork")]
144    FrameTxPreFork,
145    #[error("Gas limit price product overflow")]
146    GasLimitPriceProductOverflow,
147    #[error(
148        "Transaction gas limit exceeds maximum. Transaction hash: {tx_hash}, transaction gas limit: {tx_gas_limit}"
149    )]
150    TxMaxGasLimitExceeded { tx_hash: H256, tx_gas_limit: u64 },
151    #[error("Invalid frame transaction: VERIFY frame did not call APPROVE or payer not approved")]
152    InvalidFrameTransaction,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
156pub enum InternalError {
157    #[error("Arithmetic operation overflowed")]
158    Overflow,
159    #[error("Arithmetic operation underflowed")]
160    Underflow,
161    #[error("Cannot divide by zero")]
162    DivisionByZero,
163    #[error("Tried to convert one type to another")]
164    TypeConversion,
165    #[error("CallFrame not found")]
166    CallFrame,
167    #[error("Tried to slice non-existing data")]
168    Slicing,
169    #[error("Account not found when it should've been in the cache.")]
170    AccountNotFound,
171    #[error("Invalid precompile address. Tried to execute a precompile that does not exist.")]
172    InvalidPrecompileAddress,
173    #[error("Invalid Fork")]
174    InvalidFork,
175    #[error("Account should had been delegated")]
176    AccountNotDelegated,
177    #[error("No recipient found for privileged transaction")]
178    RecipientNotFoundForPrivilegedTransaction,
179    #[error("Memory Size Sverflow")]
180    MemorySizeOverflow,
181    #[error("Custom error: {0}")]
182    Custom(String),
183    /// Unexpected error when accessing the database, used in trait `Database`.
184    #[error("Database access error: {0}")]
185    Database(#[from] DatabaseError),
186    #[error("{0}")]
187    FakeExponentialError(#[from] FakeExponentialError),
188}
189
190impl InternalError {
191    pub fn msg(msg: &'static str) -> Self {
192        Self::Custom(msg.to_owned())
193    }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
197pub enum PrecompileError {
198    #[error("Error while parsing the calldata")]
199    ParsingInputError,
200    #[error("There is not enough gas to execute precompiled contract")]
201    NotEnoughGas,
202    #[error("Invalid point")]
203    InvalidPoint,
204    #[error("The point is not in the subgroup")]
205    PointNotInSubgroup,
206    #[error("The G1 point is not in the curve")]
207    BLS12381G1PointNotInCurve,
208    #[error("The G2 point is not in the curve")]
209    BLS12381G2PointNotInCurve,
210    #[error("Mod-exp base length is too large")]
211    ModExpBaseTooLarge,
212    #[error("Mod-exp exponent length is too large")]
213    ModExpExpTooLarge,
214    #[error("Mod-exp modulus length is too large")]
215    ModExpModulusTooLarge,
216    #[error("Coordinate Exceeds Field Modulus")]
217    CoordinateExceedsFieldModulus,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
221pub enum DatabaseError {
222    #[error("{0}")]
223    Custom(String),
224}
225
226#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
227pub enum OpcodeResult {
228    Continue,
229    Halt,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub enum TxResult {
234    Success,
235    Revert(VMError),
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct ExecutionReport {
240    pub result: TxResult,
241    /// Gas used before refunds (for block-level accounting).
242    /// Pre-EIP-7778: This is the post-refund gas.
243    /// Post-EIP-7778: This is the pre-refund gas.
244    pub gas_used: u64,
245    /// Gas spent after refunds (what the user actually pays).
246    /// This is always the post-refund gas value.
247    /// Pre-EIP-7778: Same as gas_used.
248    /// Post-EIP-7778: gas_used - refunds (capped).
249    pub gas_spent: u64,
250    pub gas_refunded: u64,
251    /// EIP-8037: State gas portion of gas_used (Amsterdam+).
252    /// Block gas_used = max(sum(regular_gas), sum(state_gas)).
253    pub state_gas_used: u64,
254    pub output: Bytes,
255    pub logs: Vec<Log>,
256    /// For frame transactions: the address that paid for gas
257    pub payer_address: Option<Address>,
258    /// For frame transactions: per-frame results (status, gas_used, logs).
259    /// `status` is a `FRAME_RECEIPT_STATUS_*` code (0 = failure, 1 = success,
260    /// 3 = skipped due to failed atomic batch, per EIP-8141 receipt encoding).
261    pub frame_results: Option<Vec<(u8, u64, Vec<Log>)>>,
262}
263
264impl ExecutionReport {
265    pub fn is_success(&self) -> bool {
266        matches!(self.result, TxResult::Success)
267    }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct ContextResult {
272    pub result: TxResult,
273    /// Gas used before refunds (for block-level accounting).
274    pub gas_used: u64,
275    /// Gas spent after refunds (what the user actually pays).
276    pub gas_spent: u64,
277    pub output: Bytes,
278}
279
280impl ContextResult {
281    pub fn is_success(&self) -> bool {
282        matches!(self.result, TxResult::Success)
283    }
284
285    /// Returns true if this result is an address collision error.
286    pub fn is_collision(&self) -> bool {
287        matches!(
288            self.result,
289            TxResult::Revert(VMError::ExceptionalHalt(
290                ExceptionalHalt::AddressAlreadyOccupied
291            ))
292        )
293    }
294
295    /// True if the failure was caused by the REVERT opcode (intentional revert).
296    /// Used to gate behaviour that differs between REVERT and ExceptionalHalt —
297    /// e.g. return data is propagated to the parent on REVERT only.
298    pub fn is_revert_opcode(&self) -> bool {
299        matches!(self.result, TxResult::Revert(VMError::RevertOpcode))
300    }
301}