Skip to main content

revm_context_interface/
result.rs

1//! Result of the EVM execution. Containing both execution result, state and errors.
2//!
3//! [`ExecutionResult`] is the result of the EVM execution.
4//!
5//! [`InvalidTransaction`] is the error that is returned when the transaction is invalid.
6//!
7//! [`InvalidHeader`] is the error that is returned when the header is invalid.
8//!
9//! [`SuccessReason`] is the reason that the transaction successfully completed.
10use crate::{context::ContextError, transaction::TransactionError};
11use core::fmt::{self, Debug};
12use database_interface::DBErrorMarker;
13use primitives::{Address, Bytes, Log, U256};
14use state::EvmState;
15use std::{borrow::Cow, boxed::Box, string::String, sync::Arc, vec::Vec};
16
17/// Trait for the halt reason.
18pub trait HaltReasonTr: Clone + Debug + PartialEq + Eq + From<HaltReason> {}
19
20impl<T> HaltReasonTr for T where T: Clone + Debug + PartialEq + Eq + From<HaltReason> {}
21
22/// Tuple containing evm execution result and state.s
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct ExecResultAndState<R, S = EvmState> {
26    /// Execution result
27    pub result: R,
28    /// Output State.
29    pub state: S,
30}
31
32/// Type alias for backwards compatibility.
33pub type ResultAndState<H = HaltReason, S = EvmState> = ExecResultAndState<ExecutionResult<H>, S>;
34
35/// Tuple containing multiple execution results and state.
36pub type ResultVecAndState<R, S> = ExecResultAndState<Vec<R>, S>;
37
38impl<R, S> ExecResultAndState<R, S> {
39    /// Creates new ResultAndState.
40    pub const fn new(result: R, state: S) -> Self {
41        Self { result, state }
42    }
43}
44
45/// Gas accounting result from transaction execution.
46///
47/// Self-contained gas snapshot with all values needed for downstream consumers.
48///
49/// ## Stored values
50///
51/// | Getter                 | Source                             | Description                                    |
52/// |------------------------|------------------------------------|------------------------------------------------|
53/// | [`total_gas_spent()`]  | `Gas::spent()` = limit − remaining | Total gas consumed before refund               |
54/// | [`inner_refunded()`]   | `Gas::refunded()` as u64           | Gas refunded (capped per EIP-3529)             |
55/// | [`floor_gas()`]        | `InitialAndFloorGas::floor_gas`    | EIP-7623 floor gas (0 if not applicable)       |
56/// | [`state_gas_spent_final()`] | `Gas::state_gas_spent`        | State gas consumed during execution (EIP-8037) |
57///
58/// [`total_gas_spent()`]: ResultGas::total_gas_spent
59/// [`inner_refunded()`]: ResultGas::inner_refunded
60/// [`floor_gas()`]: ResultGas::floor_gas
61/// [`state_gas_spent_final()`]: ResultGas::state_gas_spent_final
62///
63/// ## Derived values
64///
65/// - [`tx_gas_used()`](ResultGas::tx_gas_used) = `max(total_gas_spent − refunded, floor_gas)` (the value that goes into receipts)
66/// - [`block_regular_gas_used()`](ResultGas::block_regular_gas_used) = `total_gas_spent − state_gas_spent`
67///   (EIP-8037 + EIP-7778: pre-refund; refund and floor only affect `tx_gas_used`, not block-level regular gas)
68/// - [`block_state_gas_used()`](ResultGas::block_state_gas_used) = `state_gas_spent`
69/// - [`spent_sub_refunded()`](ResultGas::spent_sub_refunded) = `total_gas_spent − refunded` (before floor gas check)
70/// - [`final_refunded()`](ResultGas::final_refunded) = `refunded` when floor gas is inactive, `0` when floor gas kicks in
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub struct ResultGas {
74    /// Total gas spent consisting of regular and state gas.
75    /// For actual gas used, use [`used()`](ResultGas::used).
76    #[cfg_attr(feature = "serde", serde(rename = "gas_spent"))]
77    total_gas_spent: u64,
78    /// State gas consumed during execution (EIP-8037), net of the EIP-7702
79    /// per-authorization state-gas refund applied at result-build time.
80    /// Tracks gas for storage creation, account creation, and code deposit.
81    /// Zero when state gas is not enabled.
82    #[cfg_attr(feature = "serde", serde(default))]
83    state_gas_spent: u64,
84    /// Gas refund amount (capped per EIP-3529).
85    ///
86    /// Note: This is the raw refund before EIP-7623 floor gas adjustment.
87    /// Use [`final_refunded()`](ResultGas::final_refunded) for the effective refund.
88    #[cfg_attr(feature = "serde", serde(rename = "gas_refunded"))]
89    refunded: u64,
90    /// EIP-7623 floor gas. Zero when not applicable.
91    floor_gas: u64,
92}
93
94impl ResultGas {
95    /****** Constructor functions *****/
96
97    /// Creates a new `ResultGas`.
98    #[inline]
99    #[deprecated(
100        since = "32.0.0",
101        note = "It can be a footgun as gas limit is removed, use ResultGas::with_* functions instead"
102    )]
103    pub const fn new(total_gas_spent: u64, refunded: u64, floor_gas: u64) -> Self {
104        Self {
105            total_gas_spent,
106            refunded,
107            floor_gas,
108            state_gas_spent: 0,
109        }
110    }
111
112    /// Creates a new `ResultGas` with state gas tracking.
113    #[inline]
114    pub const fn new_with_state_gas(
115        total_gas_spent: u64,
116        refunded: u64,
117        floor_gas: u64,
118        state_gas_spent: u64,
119    ) -> Self {
120        Self {
121            total_gas_spent,
122            refunded,
123            floor_gas,
124            state_gas_spent,
125        }
126    }
127
128    /****** Simple getters *****/
129
130    /// Returns the total gas spent inside execution before any refund.
131    ///
132    /// If you want final gas used, use [`used()`](ResultGas::used).
133    #[inline]
134    pub const fn total_gas_spent(&self) -> u64 {
135        self.total_gas_spent
136    }
137
138    /// Returns the final state gas spent during execution (EIP-8037).
139    ///
140    /// The stored value is already net of the EIP-7702 per-authorization
141    /// state-gas refund (subtracted when the result is built).
142    ///
143    /// This is same as [`ResultGas::block_state_gas_used`] for the transaction.
144    #[inline]
145    pub const fn state_gas_spent_final(&self) -> u64 {
146        self.state_gas_spent
147    }
148
149    /// Returns the EIP-7623 floor gas.
150    #[inline]
151    pub const fn floor_gas(&self) -> u64 {
152        self.floor_gas
153    }
154
155    /// Returns the raw refund from EVM execution, before EIP-7623 floor gas adjustment.
156    ///
157    /// This is the `refunded` field value (capped per EIP-3529 but not adjusted for floor gas).
158    /// See [`final_refunded()`](ResultGas::final_refunded) for the effective refund.
159    #[inline]
160    pub const fn inner_refunded(&self) -> u64 {
161        self.refunded
162    }
163
164    /// Returns the total gas spent.
165    #[inline]
166    #[deprecated(
167        since = "32.0.0",
168        note = "After EIP-8037 gas is split on
169    regular and state gas, this method is no longer valid.
170    Use [`ResultGas::total_gas_spent`] instead"
171    )]
172    pub const fn spent(&self) -> u64 {
173        self.total_gas_spent()
174    }
175
176    /****** Simple setters *****/
177
178    /// Sets the `total_gas_spent` field by mutable reference.
179    #[inline]
180    pub const fn set_total_gas_spent(&mut self, total_gas_spent: u64) {
181        self.total_gas_spent = total_gas_spent;
182    }
183
184    /// Sets the `refunded` field by mutable reference.
185    #[inline]
186    pub const fn set_refunded(&mut self, refunded: u64) {
187        self.refunded = refunded;
188    }
189
190    /// Sets the `floor_gas` field by mutable reference.
191    #[inline]
192    pub const fn set_floor_gas(&mut self, floor_gas: u64) {
193        self.floor_gas = floor_gas;
194    }
195
196    /// Sets the `state_gas_spent` field by mutable reference.
197    #[inline]
198    pub const fn set_state_gas_spent(&mut self, state_gas_spent: u64) {
199        self.state_gas_spent = state_gas_spent;
200    }
201
202    /// Sets the `spent` field by mutable reference.
203    #[inline]
204    #[deprecated(
205        since = "32.0.0",
206        note = "After EIP-8037 gas is split on
207            regular and state gas, this method is no longer valid.
208            Use [`ResultGas::set_total_gas_spent`] instead"
209    )]
210    pub const fn set_spent(&mut self, spent: u64) {
211        self.total_gas_spent = spent;
212    }
213
214    /****** Builder with_* methods *****/
215
216    /// Sets the `total_gas_spent` field.
217    #[inline]
218    pub const fn with_total_gas_spent(mut self, total_gas_spent: u64) -> Self {
219        self.total_gas_spent = total_gas_spent;
220        self
221    }
222
223    /// Sets the `refunded` field.
224    #[inline]
225    pub const fn with_refunded(mut self, refunded: u64) -> Self {
226        self.refunded = refunded;
227        self
228    }
229
230    /// Sets the `floor_gas` field.
231    #[inline]
232    pub const fn with_floor_gas(mut self, floor_gas: u64) -> Self {
233        self.floor_gas = floor_gas;
234        self
235    }
236
237    /// Sets the `state_gas_spent` field.
238    #[inline]
239    pub const fn with_state_gas_spent(mut self, state_gas_spent: u64) -> Self {
240        self.state_gas_spent = state_gas_spent;
241        self
242    }
243
244    /// Sets the `spent` field.
245    #[inline]
246    #[deprecated(
247        since = "32.0.0",
248        note = "After EIP-8037 gas is split on
249    regular and state gas, this method is no longer valid.
250    Use [`ResultGas::with_total_gas_spent`] instead"
251    )]
252    pub const fn with_spent(mut self, spent: u64) -> Self {
253        self.total_gas_spent = spent;
254        self
255    }
256
257    /* Aggregated getters */
258
259    /// Returns the total gas used by the transaction.
260    ///
261    /// This value is set inside Receipt.
262    #[inline]
263    pub const fn tx_gas_used(&self) -> u64 {
264        // consiste of regular and state gas.
265        let total_gas_spent = self.total_gas_spent();
266        // from total gas subtract the refunded gas. Refunded is capped by 20% of total gas spent.
267        let tx_gas_refunded = total_gas_spent.saturating_sub(self.inner_refunded());
268        max(tx_gas_refunded, self.floor_gas())
269    }
270
271    /// Returns the regular gas used by the block per EIP-8037 + EIP-7778.
272    ///
273    /// `total_gas_spent - state_gas_spent` (pre-refund). Refund and floor are
274    /// applied to the combined pre-refund total and only affect `tx_gas_used`
275    /// (the receipt value), not the block-level regular component.
276    #[inline]
277    pub const fn block_regular_gas_used(&self) -> u64 {
278        self.total_gas_spent()
279            .saturating_sub(self.state_gas_spent_final())
280    }
281
282    /// Returns the state gas used by the block.
283    ///
284    /// This is same as [`ResultGas::state_gas_spent_final`] for the block.
285    #[inline]
286    pub const fn block_state_gas_used(&self) -> u64 {
287        self.state_gas_spent_final()
288    }
289
290    /// Returns the final gas used: `max(spent - refunded, floor_gas)`.
291    ///
292    /// This is the value used for receipt `cumulative_gas_used` accumulation
293    /// and the per-transaction gas charge.
294    #[inline]
295    #[deprecated(
296        since = "32.0.0",
297        note = "Used is not descriptive enough, use [`ResultGas::tx_gas_used`] instead"
298    )]
299    pub const fn used(&self) -> u64 {
300        // EIP-7623: Increase calldata cost
301        // spend at least a gas_floor amount of gas.
302        let spent_sub_refunded = self.spent_sub_refunded();
303        if spent_sub_refunded < self.floor_gas {
304            return self.floor_gas;
305        }
306        spent_sub_refunded
307    }
308
309    /// Returns the gas spent minus the refunded gas.
310    ///
311    /// This does not take into account EIP-7623 floor gas. If you want to get the gas used in
312    /// receipt, use [`used()`](ResultGas::used) instead.
313    #[inline]
314    pub const fn spent_sub_refunded(&self) -> u64 {
315        self.total_gas_spent().saturating_sub(self.refunded)
316    }
317
318    /// Returns the effective refund after EIP-7623 floor gas adjustment.
319    ///
320    /// When floor gas kicks in (`spent - refunded < floor_gas`), the refund is zero
321    /// because the floor gas charge absorbs it entirely. Otherwise returns the raw refund.
322    #[inline]
323    pub const fn final_refunded(&self) -> u64 {
324        if self.spent_sub_refunded() < self.floor_gas {
325            0
326        } else {
327            self.refunded
328        }
329    }
330}
331
332/// Const function that returns the maximum of two u64 values.
333#[inline(always)]
334pub const fn max(a: u64, b: u64) -> u64 {
335    if a > b {
336        a
337    } else {
338        b
339    }
340}
341
342/// Const function that returns the minimum of two u64 values.
343#[inline(always)]
344pub const fn min(a: u64, b: u64) -> u64 {
345    if a < b {
346        a
347    } else {
348        b
349    }
350}
351
352impl fmt::Display for ResultGas {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        write!(
355            f,
356            "Gas used: {}, total spent: {}",
357            self.tx_gas_used(),
358            self.total_gas_spent()
359        )?;
360        if self.refunded > 0 {
361            write!(f, ", refunded: {}", self.refunded)?;
362        }
363        if self.floor_gas > 0 {
364            write!(f, ", floor: {}", self.floor_gas)?;
365        }
366        if self.state_gas_spent > 0 {
367            write!(f, ", state_gas: {}", self.state_gas_spent)?;
368        }
369        Ok(())
370    }
371}
372
373/// Result of a transaction execution
374#[derive(Clone, Debug, PartialEq, Eq, Hash)]
375#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
376pub enum ExecutionResult<HaltReasonTy = HaltReason> {
377    /// Returned successfully
378    Success {
379        /// Reason for the success.
380        reason: SuccessReason,
381        /// Gas accounting for the transaction.
382        gas: ResultGas,
383        /// Logs emitted by the transaction.
384        logs: Vec<Log>,
385        /// Output of the transaction.
386        output: Output,
387    },
388    /// Reverted by `REVERT` opcode that doesn't spend all gas
389    Revert {
390        /// Gas accounting for the transaction.
391        gas: ResultGas,
392        /// Logs emitted before the revert.
393        logs: Vec<Log>,
394        /// Output of the transaction.
395        output: Bytes,
396    },
397    /// Reverted for various reasons and spend all gas
398    Halt {
399        /// Reason for the halt.
400        reason: HaltReasonTy,
401        /// Gas accounting for the transaction.
402        ///
403        /// For standard EVM halts, gas used typically equals the gas limit.
404        /// Some system- or L2-specific halts may intentionally report less gas used.
405        gas: ResultGas,
406        /// Logs emitted before the halt.
407        logs: Vec<Log>,
408    },
409}
410
411impl<HaltReasonTy> ExecutionResult<HaltReasonTy> {
412    /// Returns if transaction execution is successful.
413    ///
414    /// 1 indicates success, 0 indicates revert.
415    ///
416    /// <https://eips.ethereum.org/EIPS/eip-658>
417    pub const fn is_success(&self) -> bool {
418        matches!(self, Self::Success { .. })
419    }
420
421    /// Maps a `DBError` to a new error type using the provided closure, leaving other variants unchanged.
422    pub fn map_haltreason<F, OHR>(self, op: F) -> ExecutionResult<OHR>
423    where
424        F: FnOnce(HaltReasonTy) -> OHR,
425    {
426        match self {
427            Self::Success {
428                reason,
429                gas,
430                logs,
431                output,
432            } => ExecutionResult::Success {
433                reason,
434                gas,
435                logs,
436                output,
437            },
438            Self::Revert { gas, logs, output } => ExecutionResult::Revert { gas, logs, output },
439            Self::Halt { reason, gas, logs } => ExecutionResult::Halt {
440                reason: op(reason),
441                gas,
442                logs,
443            },
444        }
445    }
446
447    /// Returns created address if execution is Create transaction
448    /// and Contract was created.
449    pub fn created_address(&self) -> Option<Address> {
450        match self {
451            Self::Success { output, .. } => output.address().cloned(),
452            _ => None,
453        }
454    }
455
456    /// Returns true if execution result is a Halt.
457    pub const fn is_halt(&self) -> bool {
458        matches!(self, Self::Halt { .. })
459    }
460
461    /// Returns the output data of the execution.
462    ///
463    /// Returns [`None`] if the execution was halted.
464    pub const fn output(&self) -> Option<&Bytes> {
465        match self {
466            Self::Success { output, .. } => Some(output.data()),
467            Self::Revert { output, .. } => Some(output),
468            _ => None,
469        }
470    }
471
472    /// Consumes the type and returns the output data of the execution.
473    ///
474    /// Returns [`None`] if the execution was halted.
475    pub fn into_output(self) -> Option<Bytes> {
476        match self {
477            Self::Success { output, .. } => Some(output.into_data()),
478            Self::Revert { output, .. } => Some(output),
479            _ => None,
480        }
481    }
482
483    /// Returns the logs emitted during execution.
484    pub const fn logs(&self) -> &[Log] {
485        match self {
486            Self::Success { logs, .. } | Self::Revert { logs, .. } | Self::Halt { logs, .. } => {
487                logs.as_slice()
488            }
489        }
490    }
491
492    /// Consumes [`self`] and returns the logs emitted during execution.
493    pub fn into_logs(self) -> Vec<Log> {
494        match self {
495            Self::Success { logs, .. } | Self::Revert { logs, .. } | Self::Halt { logs, .. } => {
496                logs
497            }
498        }
499    }
500
501    /// Returns the gas accounting information.
502    pub const fn gas(&self) -> &ResultGas {
503        match self {
504            Self::Success { gas, .. } | Self::Revert { gas, .. } | Self::Halt { gas, .. } => gas,
505        }
506    }
507
508    /// Returns the gas used needed for the transaction receipt.
509    pub const fn tx_gas_used(&self) -> u64 {
510        self.gas().tx_gas_used()
511    }
512
513    /// Returns the gas used.
514    #[inline]
515    #[deprecated(
516        since = "32.0.0",
517        note = "Use `tx_gas_used()` instead, `gas_used` is ambiguous after EIP-8037 state gas split"
518    )]
519    pub const fn gas_used(&self) -> u64 {
520        self.tx_gas_used()
521    }
522}
523
524impl<HaltReasonTy: fmt::Display> fmt::Display for ExecutionResult<HaltReasonTy> {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        match self {
527            Self::Success {
528                reason,
529                gas,
530                logs,
531                output,
532            } => {
533                write!(f, "Success ({reason}): {gas}")?;
534                if !logs.is_empty() {
535                    write!(
536                        f,
537                        ", {} log{}",
538                        logs.len(),
539                        if logs.len() == 1 { "" } else { "s" }
540                    )?;
541                }
542                write!(f, ", {output}")
543            }
544            Self::Revert { gas, logs, output } => {
545                write!(f, "Revert: {gas}")?;
546                if !logs.is_empty() {
547                    write!(
548                        f,
549                        ", {} log{}",
550                        logs.len(),
551                        if logs.len() == 1 { "" } else { "s" }
552                    )?;
553                }
554                if !output.is_empty() {
555                    write!(f, ", {} bytes output", output.len())?;
556                }
557                Ok(())
558            }
559            Self::Halt { reason, gas, logs } => {
560                write!(f, "Halted: {reason} ({gas})")?;
561                if !logs.is_empty() {
562                    write!(
563                        f,
564                        ", {} log{}",
565                        logs.len(),
566                        if logs.len() == 1 { "" } else { "s" }
567                    )?;
568                }
569                Ok(())
570            }
571        }
572    }
573}
574
575/// Output of a transaction execution
576#[derive(Debug, Clone, PartialEq, Eq, Hash)]
577#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
578pub enum Output {
579    /// Output of a call.
580    Call(Bytes),
581    /// Output of a create.
582    Create(Bytes, Option<Address>),
583}
584
585impl Output {
586    /// Returns the output data of the execution output.
587    pub fn into_data(self) -> Bytes {
588        match self {
589            Output::Call(data) => data,
590            Output::Create(data, _) => data,
591        }
592    }
593
594    /// Returns the output data of the execution output.
595    pub const fn data(&self) -> &Bytes {
596        match self {
597            Output::Call(data) => data,
598            Output::Create(data, _) => data,
599        }
600    }
601
602    /// Returns the created address, if any.
603    pub const fn address(&self) -> Option<&Address> {
604        match self {
605            Output::Call(_) => None,
606            Output::Create(_, address) => address.as_ref(),
607        }
608    }
609}
610
611impl fmt::Display for Output {
612    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613        match self {
614            Output::Call(data) => {
615                if data.is_empty() {
616                    write!(f, "no output")
617                } else {
618                    write!(f, "{} bytes output", data.len())
619                }
620            }
621            Output::Create(data, Some(addr)) => {
622                if data.is_empty() {
623                    write!(f, "contract created at {}", addr)
624                } else {
625                    write!(f, "contract created at {} ({} bytes)", addr, data.len())
626                }
627            }
628            Output::Create(data, None) => {
629                if data.is_empty() {
630                    write!(f, "contract creation (no address)")
631                } else {
632                    write!(f, "contract creation (no address, {} bytes)", data.len())
633                }
634            }
635        }
636    }
637}
638
639/// Type-erased error type.
640#[derive(Debug, Clone)]
641pub struct AnyError(Arc<dyn core::error::Error + Send + Sync>);
642impl AnyError {
643    /// Creates a new [`AnyError`] from any error type.
644    pub fn new(err: impl core::error::Error + Send + Sync + 'static) -> Self {
645        Self(Arc::new(err))
646    }
647}
648
649impl PartialEq for AnyError {
650    fn eq(&self, other: &Self) -> bool {
651        Arc::ptr_eq(&self.0, &other.0)
652    }
653}
654impl Eq for AnyError {}
655impl core::hash::Hash for AnyError {
656    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
657        (Arc::as_ptr(&self.0) as *const ()).hash(state);
658    }
659}
660impl fmt::Display for AnyError {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        fmt::Display::fmt(&self.0, f)
663    }
664}
665impl core::error::Error for AnyError {
666    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
667        self.0.source()
668    }
669}
670
671#[cfg(feature = "serde")]
672impl serde::Serialize for AnyError {
673    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
674        serializer.collect_str(self)
675    }
676}
677
678#[derive(Debug)]
679struct StringError(String);
680impl fmt::Display for StringError {
681    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682        f.write_str(&self.0)
683    }
684}
685impl core::error::Error for StringError {}
686
687impl From<String> for AnyError {
688    fn from(value: String) -> Self {
689        Self::new(StringError(value))
690    }
691}
692impl From<&'static str> for AnyError {
693    fn from(s: &'static str) -> Self {
694        Self::new(StringError(s.into()))
695    }
696}
697
698#[cfg(feature = "serde")]
699impl<'de> serde::Deserialize<'de> for AnyError {
700    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
701        let s = String::deserialize(deserializer)?;
702        Ok(s.into())
703    }
704}
705
706/// Main EVM error
707#[derive(Debug, Clone, PartialEq, Eq)]
708#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
709pub enum EVMError<DBError, TransactionError = InvalidTransaction> {
710    /// Transaction validation error
711    Transaction(TransactionError),
712    /// Header validation error
713    Header(InvalidHeader),
714    /// Database error
715    Database(DBError),
716    /// Custom error for non-standard EVM failures.
717    ///
718    /// This includes fatal precompile errors (`PrecompileError::Fatal` and `PrecompileError::FatalAny`)
719    /// errors as well as any custom errors returned by handler registers.
720    Custom(String),
721    /// Custom error for non-standard EVM failures.
722    ///
723    /// This includes fatal precompile errors (`PrecompileError::Fatal` and `PrecompileError::FatalAny`)
724    /// errors as well as any custom errors returned by handler registers.
725    CustomAny(AnyError),
726}
727
728impl<DBError, TransactionValidationErrorT> From<ContextError<DBError>>
729    for EVMError<DBError, TransactionValidationErrorT>
730{
731    fn from(value: ContextError<DBError>) -> Self {
732        match value {
733            ContextError::Db(e) => Self::Database(e),
734            ContextError::Custom(e) => Self::Custom(e),
735        }
736    }
737}
738
739impl<DBError: DBErrorMarker, TX> From<DBError> for EVMError<DBError, TX> {
740    fn from(value: DBError) -> Self {
741        Self::Database(value)
742    }
743}
744
745/// Trait for converting a string to an [`EVMError::Custom`] error.
746pub trait FromStringError {
747    /// Converts a string to an [`EVMError::Custom`] error.
748    fn from_string(value: String) -> Self;
749}
750
751impl<DB, TX> FromStringError for EVMError<DB, TX> {
752    fn from_string(value: String) -> Self {
753        Self::Custom(value)
754    }
755}
756
757impl<DB, TXE: From<InvalidTransaction>> From<InvalidTransaction> for EVMError<DB, TXE> {
758    fn from(value: InvalidTransaction) -> Self {
759        Self::Transaction(TXE::from(value))
760    }
761}
762
763impl<DBError, TransactionValidationErrorT> EVMError<DBError, TransactionValidationErrorT> {
764    /// Maps a `DBError` to a new error type using the provided closure, leaving other variants unchanged.
765    pub fn map_db_err<F, E>(self, op: F) -> EVMError<E, TransactionValidationErrorT>
766    where
767        F: FnOnce(DBError) -> E,
768    {
769        match self {
770            Self::Transaction(e) => EVMError::Transaction(e),
771            Self::Header(e) => EVMError::Header(e),
772            Self::Database(e) => EVMError::Database(op(e)),
773            Self::Custom(e) => EVMError::Custom(e),
774            Self::CustomAny(e) => EVMError::CustomAny(e),
775        }
776    }
777}
778
779impl<DBError, TransactionValidationErrorT> core::error::Error
780    for EVMError<DBError, TransactionValidationErrorT>
781where
782    DBError: core::error::Error + 'static,
783    TransactionValidationErrorT: core::error::Error + 'static,
784{
785    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
786        match self {
787            Self::Transaction(e) => Some(e),
788            Self::Header(e) => Some(e),
789            Self::Database(e) => Some(e),
790            Self::Custom(_) => None,
791            Self::CustomAny(e) => Some(e.0.as_ref()),
792        }
793    }
794}
795
796impl<DBError, TransactionValidationErrorT> fmt::Display
797    for EVMError<DBError, TransactionValidationErrorT>
798where
799    DBError: fmt::Display,
800    TransactionValidationErrorT: fmt::Display,
801{
802    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
803        match self {
804            Self::Transaction(e) => write!(f, "transaction validation error: {e}"),
805            Self::Header(e) => write!(f, "header validation error: {e}"),
806            Self::Database(e) => write!(f, "database error: {e}"),
807            Self::Custom(e) => f.write_str(e),
808            Self::CustomAny(e) => write!(f, "{e}"),
809        }
810    }
811}
812
813impl<DBError, TransactionValidationErrorT> From<InvalidHeader>
814    for EVMError<DBError, TransactionValidationErrorT>
815{
816    fn from(value: InvalidHeader) -> Self {
817        Self::Header(value)
818    }
819}
820
821/// Transaction validation error.
822#[derive(Debug, Clone, PartialEq, Eq, Hash)]
823#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
824pub enum InvalidTransaction {
825    /// When using the EIP-1559 fee model introduced in the London upgrade, transactions specify two primary fee fields:
826    /// - `gas_max_fee`: The maximum total fee a user is willing to pay, inclusive of both base fee and priority fee.
827    /// - `gas_priority_fee`: The extra amount a user is willing to give directly to the miner, often referred to as the "tip".
828    ///
829    /// Provided `gas_priority_fee` exceeds the total `gas_max_fee`.
830    PriorityFeeGreaterThanMaxFee,
831    /// EIP-1559: `gas_price` is less than `basefee`.
832    GasPriceLessThanBasefee,
833    /// `gas_limit` in the tx is bigger than `block_gas_limit`.
834    CallerGasLimitMoreThanBlock,
835    /// Initial gas for a Call is bigger than `gas_limit`.
836    ///
837    /// Initial gas for a Call contains:
838    /// - initial stipend gas
839    /// - gas for access list and input data
840    CallGasCostMoreThanGasLimit {
841        /// Initial gas for a Call.
842        initial_gas: u64,
843        /// Gas limit for the transaction.
844        gas_limit: u64,
845    },
846    /// Gas floor calculated from EIP-7623 Increase calldata cost
847    /// is more than the gas limit.
848    ///
849    /// Tx data is too large to be executed.
850    GasFloorMoreThanGasLimit {
851        /// Gas floor calculated from EIP-7623 Increase calldata cost.
852        gas_floor: u64,
853        /// Gas limit for the transaction.
854        gas_limit: u64,
855    },
856    /// EIP-3607 Reject transactions from senders with deployed code
857    RejectCallerWithCode,
858    /// Transaction account does not have enough amount of ether to cover transferred value and gas_limit*gas_price.
859    LackOfFundForMaxFee {
860        /// Fee for the transaction.
861        fee: Box<U256>,
862        /// Balance of the sender.
863        balance: Box<U256>,
864    },
865    /// Overflow payment in transaction.
866    OverflowPaymentInTransaction,
867    /// Nonce overflows in transaction.
868    NonceOverflowInTransaction,
869    /// Nonce is too high.
870    NonceTooHigh {
871        /// Nonce of the transaction.
872        tx: u64,
873        /// Nonce of the state.
874        state: u64,
875    },
876    /// Nonce is too low.
877    NonceTooLow {
878        /// Nonce of the transaction.
879        tx: u64,
880        /// Nonce of the state.
881        state: u64,
882    },
883    /// EIP-3860: Limit and meter initcode
884    CreateInitCodeSizeLimit,
885    /// Transaction chain id does not match the config chain id.
886    InvalidChainId,
887    /// Missing chain id.
888    MissingChainId,
889    /// Transaction gas limit is greater than the cap.
890    TxGasLimitGreaterThanCap {
891        /// Transaction gas limit.
892        gas_limit: u64,
893        /// Gas limit cap.
894        cap: u64,
895    },
896    /// Access list is not supported for blocks before the Berlin hardfork.
897    AccessListNotSupported,
898    /// `max_fee_per_blob_gas` is not supported for blocks before the Cancun hardfork.
899    MaxFeePerBlobGasNotSupported,
900    /// `blob_hashes`/`blob_versioned_hashes` is not supported for blocks before the Cancun hardfork.
901    BlobVersionedHashesNotSupported,
902    /// Block `blob_gas_price` is greater than tx-specified `max_fee_per_blob_gas` after Cancun.
903    BlobGasPriceGreaterThanMax {
904        /// Block `blob_gas_price`.
905        block_blob_gas_price: u128,
906        /// Tx-specified `max_fee_per_blob_gas`.
907        tx_max_fee_per_blob_gas: u128,
908    },
909    /// There should be at least one blob in Blob transaction.
910    EmptyBlobs,
911    /// Blob transaction can't be a create transaction.
912    ///
913    /// `to` must be present
914    BlobCreateTransaction,
915    /// Transaction has more then `max` blobs
916    TooManyBlobs {
917        /// Maximum number of blobs allowed.
918        max: usize,
919        /// Number of blobs in the transaction.
920        have: usize,
921    },
922    /// Blob transaction contains a versioned hash with an incorrect version
923    BlobVersionNotSupported,
924    /// EIP-7702 is not enabled.
925    AuthorizationListNotSupported,
926    /// EIP-7702 transaction has invalid fields set.
927    AuthorizationListInvalidFields,
928    /// Empty Authorization List is not allowed.
929    EmptyAuthorizationList,
930    /// EIP-2930 is not supported.
931    Eip2930NotSupported,
932    /// EIP-1559 is not supported.
933    Eip1559NotSupported,
934    /// EIP-4844 is not supported.
935    Eip4844NotSupported,
936    /// EIP-7702 is not supported.
937    Eip7702NotSupported,
938    /// EIP-7873 is not supported.
939    Eip7873NotSupported,
940    /// EIP-7873 initcode transaction should have `to` address.
941    Eip7873MissingTarget,
942    /// Custom string error for flexible error handling.
943    Str(Cow<'static, str>),
944}
945
946impl TransactionError for InvalidTransaction {}
947
948impl core::error::Error for InvalidTransaction {}
949
950impl fmt::Display for InvalidTransaction {
951    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
952        match self {
953            Self::PriorityFeeGreaterThanMaxFee => {
954                write!(f, "priority fee is greater than max fee")
955            }
956            Self::GasPriceLessThanBasefee => {
957                write!(f, "gas price is less than basefee")
958            }
959            Self::CallerGasLimitMoreThanBlock => {
960                write!(f, "caller gas limit exceeds the block gas limit")
961            }
962            Self::TxGasLimitGreaterThanCap { gas_limit, cap } => {
963                write!(
964                    f,
965                    "transaction gas limit ({gas_limit}) is greater than the cap ({cap})"
966                )
967            }
968            Self::CallGasCostMoreThanGasLimit {
969                initial_gas,
970                gas_limit,
971            } => {
972                write!(
973                    f,
974                    "call gas cost ({initial_gas}) exceeds the gas limit ({gas_limit})"
975                )
976            }
977            Self::GasFloorMoreThanGasLimit {
978                gas_floor,
979                gas_limit,
980            } => {
981                write!(
982                    f,
983                    "gas floor ({gas_floor}) exceeds the gas limit ({gas_limit})"
984                )
985            }
986            Self::RejectCallerWithCode => {
987                write!(f, "reject transactions from senders with deployed code")
988            }
989            Self::LackOfFundForMaxFee { fee, balance } => {
990                write!(f, "lack of funds ({balance}) for max fee ({fee})")
991            }
992            Self::OverflowPaymentInTransaction => {
993                write!(f, "overflow payment in transaction")
994            }
995            Self::NonceOverflowInTransaction => {
996                write!(f, "nonce overflow in transaction")
997            }
998            Self::NonceTooHigh { tx, state } => {
999                write!(f, "nonce {tx} too high, expected {state}")
1000            }
1001            Self::NonceTooLow { tx, state } => {
1002                write!(f, "nonce {tx} too low, expected {state}")
1003            }
1004            Self::CreateInitCodeSizeLimit => {
1005                write!(f, "create initcode size limit")
1006            }
1007            Self::InvalidChainId => write!(f, "invalid chain ID"),
1008            Self::MissingChainId => write!(f, "missing chain ID"),
1009            Self::AccessListNotSupported => write!(f, "access list not supported"),
1010            Self::MaxFeePerBlobGasNotSupported => {
1011                write!(f, "max fee per blob gas not supported")
1012            }
1013            Self::BlobVersionedHashesNotSupported => {
1014                write!(f, "blob versioned hashes not supported")
1015            }
1016            Self::BlobGasPriceGreaterThanMax {
1017                block_blob_gas_price,
1018                tx_max_fee_per_blob_gas,
1019            } => {
1020                write!(
1021                    f,
1022                    "blob gas price ({block_blob_gas_price}) is greater than max fee per blob gas ({tx_max_fee_per_blob_gas})"
1023                )
1024            }
1025            Self::EmptyBlobs => write!(f, "empty blobs"),
1026            Self::BlobCreateTransaction => write!(f, "blob create transaction"),
1027            Self::TooManyBlobs { max, have } => {
1028                write!(f, "too many blobs, have {have}, max {max}")
1029            }
1030            Self::BlobVersionNotSupported => write!(f, "blob version not supported"),
1031            Self::AuthorizationListNotSupported => write!(f, "authorization list not supported"),
1032            Self::AuthorizationListInvalidFields => {
1033                write!(f, "authorization list tx has invalid fields")
1034            }
1035            Self::EmptyAuthorizationList => write!(f, "empty authorization list"),
1036            Self::Eip2930NotSupported => write!(f, "Eip2930 is not supported"),
1037            Self::Eip1559NotSupported => write!(f, "Eip1559 is not supported"),
1038            Self::Eip4844NotSupported => write!(f, "Eip4844 is not supported"),
1039            Self::Eip7702NotSupported => write!(f, "Eip7702 is not supported"),
1040            Self::Eip7873NotSupported => write!(f, "Eip7873 is not supported"),
1041            Self::Eip7873MissingTarget => {
1042                write!(f, "Eip7873 initcode transaction should have `to` address")
1043            }
1044            Self::Str(msg) => f.write_str(msg),
1045        }
1046    }
1047}
1048
1049/// Errors related to misconfiguration of a [`crate::Block`].
1050#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1051#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1052pub enum InvalidHeader {
1053    /// `prevrandao` is not set for Merge and above.
1054    PrevrandaoNotSet,
1055    /// `excess_blob_gas` is not set for Cancun and above.
1056    ExcessBlobGasNotSet,
1057}
1058
1059impl core::error::Error for InvalidHeader {}
1060
1061impl fmt::Display for InvalidHeader {
1062    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1063        match self {
1064            Self::PrevrandaoNotSet => write!(f, "`prevrandao` not set"),
1065            Self::ExcessBlobGasNotSet => write!(f, "`excess_blob_gas` not set"),
1066        }
1067    }
1068}
1069
1070/// Reason a transaction successfully completed.
1071#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1072#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1073pub enum SuccessReason {
1074    /// Stop [`state::bytecode::opcode::STOP`] opcode.
1075    Stop,
1076    /// Return [`state::bytecode::opcode::RETURN`] opcode.
1077    Return,
1078    /// Self destruct opcode.
1079    SelfDestruct,
1080}
1081
1082impl fmt::Display for SuccessReason {
1083    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1084        match self {
1085            Self::Stop => write!(f, "Stop"),
1086            Self::Return => write!(f, "Return"),
1087            Self::SelfDestruct => write!(f, "SelfDestruct"),
1088        }
1089    }
1090}
1091
1092/// Indicates that the EVM has experienced an exceptional halt.
1093///
1094/// This causes execution to immediately end with all gas being consumed.
1095#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1096#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1097pub enum HaltReason {
1098    /// Out of gas error.
1099    OutOfGas(OutOfGasError),
1100    /// Opcode not found error.
1101    OpcodeNotFound,
1102    /// Invalid FE opcode error.
1103    InvalidFEOpcode,
1104    /// Invalid jump destination.
1105    InvalidJump,
1106    /// The feature or opcode is not activated in hardfork.
1107    NotActivated,
1108    /// Attempting to pop a value from an empty stack.
1109    StackUnderflow,
1110    /// Attempting to push a value onto a full stack.
1111    StackOverflow,
1112    /// Invalid memory or storage offset for [`state::bytecode::opcode::RETURNDATACOPY`].
1113    OutOfOffset,
1114    /// Address collision during contract creation.
1115    CreateCollision,
1116    /// Precompile error.
1117    PrecompileError,
1118    /// Precompile error with message from context.
1119    PrecompileErrorWithContext(String),
1120    /// Nonce overflow.
1121    NonceOverflow,
1122    /// Create init code size exceeds limit (runtime).
1123    CreateContractSizeLimit,
1124    /// Error on created contract that begins with EF
1125    CreateContractStartingWithEF,
1126    /// EIP-3860: Limit and meter initcode. Initcode size limit exceeded.
1127    CreateInitCodeSizeLimit,
1128
1129    /* Internal Halts that can be only found inside Inspector */
1130    /// Overflow payment. Not possible to happen on mainnet.
1131    OverflowPayment,
1132    /// State change during static call.
1133    StateChangeDuringStaticCall,
1134    /// Call not allowed inside static call.
1135    CallNotAllowedInsideStatic,
1136    /// Out of funds to pay for the call.
1137    OutOfFunds,
1138    /// Call is too deep.
1139    CallTooDeep,
1140}
1141
1142impl core::error::Error for HaltReason {}
1143
1144impl fmt::Display for HaltReason {
1145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1146        match self {
1147            Self::OutOfGas(err) => write!(f, "{err}"),
1148            Self::OpcodeNotFound => write!(f, "opcode not found"),
1149            Self::InvalidFEOpcode => write!(f, "invalid 0xFE opcode"),
1150            Self::InvalidJump => write!(f, "invalid jump destination"),
1151            Self::NotActivated => write!(f, "feature or opcode not activated"),
1152            Self::StackUnderflow => write!(f, "stack underflow"),
1153            Self::StackOverflow => write!(f, "stack overflow"),
1154            Self::OutOfOffset => write!(f, "out of offset"),
1155            Self::CreateCollision => write!(f, "create collision"),
1156            Self::PrecompileError => write!(f, "precompile error"),
1157            Self::PrecompileErrorWithContext(msg) => write!(f, "precompile error: {msg}"),
1158            Self::NonceOverflow => write!(f, "nonce overflow"),
1159            Self::CreateContractSizeLimit => write!(f, "create contract size limit"),
1160            Self::CreateContractStartingWithEF => {
1161                write!(f, "create contract starting with 0xEF")
1162            }
1163            Self::CreateInitCodeSizeLimit => write!(f, "create initcode size limit"),
1164            Self::OverflowPayment => write!(f, "overflow payment"),
1165            Self::StateChangeDuringStaticCall => write!(f, "state change during static call"),
1166            Self::CallNotAllowedInsideStatic => write!(f, "call not allowed inside static call"),
1167            Self::OutOfFunds => write!(f, "out of funds"),
1168            Self::CallTooDeep => write!(f, "call too deep"),
1169        }
1170    }
1171}
1172
1173/// Out of gas errors.
1174#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1176pub enum OutOfGasError {
1177    /// Basic OOG error. Not enough gas to execute the opcode.
1178    Basic,
1179    /// Tried to expand past memory limit.
1180    MemoryLimit,
1181    /// Basic OOG error from memory expansion
1182    Memory,
1183    /// Precompile threw OOG error
1184    Precompile,
1185    /// When performing something that takes a U256 and casts down to a u64, if its too large this would fire
1186    /// i.e. in `as_usize_or_fail`
1187    InvalidOperand,
1188    /// When performing SSTORE the gasleft is less than or equal to 2300
1189    ReentrancySentry,
1190}
1191
1192impl core::error::Error for OutOfGasError {}
1193
1194impl fmt::Display for OutOfGasError {
1195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1196        match self {
1197            Self::Basic => write!(f, "out of gas"),
1198            Self::MemoryLimit => write!(f, "out of gas: memory limit exceeded"),
1199            Self::Memory => write!(f, "out of gas: memory expansion"),
1200            Self::Precompile => write!(f, "out of gas: precompile"),
1201            Self::InvalidOperand => write!(f, "out of gas: invalid operand"),
1202            Self::ReentrancySentry => write!(f, "out of gas: reentrancy sentry"),
1203        }
1204    }
1205}
1206
1207/// Error that includes transaction index for batch transaction processing.
1208#[derive(Debug, Clone, PartialEq, Eq)]
1209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1210pub struct TransactionIndexedError<Error> {
1211    /// The original error that occurred.
1212    pub error: Error,
1213    /// The index of the transaction that failed.
1214    pub transaction_index: usize,
1215}
1216
1217impl<Error> TransactionIndexedError<Error> {
1218    /// Create a new `TransactionIndexedError` with the given error and transaction index.
1219    #[must_use]
1220    pub const fn new(error: Error, transaction_index: usize) -> Self {
1221        Self {
1222            error,
1223            transaction_index,
1224        }
1225    }
1226
1227    /// Get a reference to the underlying error.
1228    pub const fn error(&self) -> &Error {
1229        &self.error
1230    }
1231
1232    /// Convert into the underlying error.
1233    #[must_use]
1234    pub fn into_error(self) -> Error {
1235        self.error
1236    }
1237}
1238
1239impl<Error: fmt::Display> fmt::Display for TransactionIndexedError<Error> {
1240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241        write!(
1242            f,
1243            "transaction {} failed: {}",
1244            self.transaction_index, self.error
1245        )
1246    }
1247}
1248
1249impl<Error: core::error::Error + 'static> core::error::Error for TransactionIndexedError<Error> {
1250    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
1251        Some(&self.error)
1252    }
1253}
1254
1255impl From<&'static str> for InvalidTransaction {
1256    fn from(s: &'static str) -> Self {
1257        Self::Str(Cow::Borrowed(s))
1258    }
1259}
1260
1261impl From<String> for InvalidTransaction {
1262    fn from(s: String) -> Self {
1263        Self::Str(Cow::Owned(s))
1264    }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::*;
1270
1271    #[test]
1272    fn test_execution_result_display() {
1273        let result: ExecutionResult<HaltReason> = ExecutionResult::Success {
1274            reason: SuccessReason::Return,
1275            gas: ResultGas::default()
1276                .with_total_gas_spent(100000)
1277                .with_refunded(26000)
1278                .with_floor_gas(5000),
1279            logs: vec![Log::default(), Log::default()],
1280            output: Output::Call(Bytes::from(vec![1, 2, 3])),
1281        };
1282        assert_eq!(
1283            result.to_string(),
1284            "Success (Return): Gas used: 74000, total spent: 100000, refunded: 26000, floor: 5000, 2 logs, 3 bytes output"
1285        );
1286
1287        let result: ExecutionResult<HaltReason> = ExecutionResult::Revert {
1288            gas: ResultGas::default()
1289                .with_total_gas_spent(100000)
1290                .with_refunded(100000),
1291            logs: vec![],
1292            output: Bytes::from(vec![1, 2, 3, 4]),
1293        };
1294        assert_eq!(
1295            result.to_string(),
1296            "Revert: Gas used: 0, total spent: 100000, refunded: 100000, 4 bytes output"
1297        );
1298
1299        let result: ExecutionResult<HaltReason> = ExecutionResult::Halt {
1300            reason: HaltReason::OutOfGas(OutOfGasError::Basic),
1301            gas: ResultGas::default()
1302                .with_total_gas_spent(1000000)
1303                .with_refunded(1000000),
1304            logs: vec![],
1305        };
1306        assert_eq!(
1307            result.to_string(),
1308            "Halted: out of gas (Gas used: 0, total spent: 1000000, refunded: 1000000)"
1309        );
1310    }
1311
1312    #[test]
1313    fn test_result_gas_display() {
1314        // No refund, no floor
1315        assert_eq!(
1316            ResultGas::default().with_total_gas_spent(21000).to_string(),
1317            "Gas used: 21000, total spent: 21000"
1318        );
1319        // With refund
1320        assert_eq!(
1321            ResultGas::default()
1322                .with_total_gas_spent(50000)
1323                .with_refunded(10000)
1324                .to_string(),
1325            "Gas used: 40000, total spent: 50000, refunded: 10000"
1326        );
1327        // With refund and floor
1328        assert_eq!(
1329            ResultGas::default()
1330                .with_total_gas_spent(50000)
1331                .with_refunded(10000)
1332                .with_floor_gas(30000)
1333                .to_string(),
1334            "Gas used: 40000, total spent: 50000, refunded: 10000, floor: 30000"
1335        );
1336    }
1337
1338    #[test]
1339    fn test_result_gas_used_and_remaining() {
1340        let gas = ResultGas::default()
1341            .with_total_gas_spent(100)
1342            .with_refunded(30);
1343        assert_eq!(gas.total_gas_spent(), 100);
1344        assert_eq!(gas.inner_refunded(), 30);
1345        assert_eq!(gas.spent_sub_refunded(), 70);
1346
1347        // Saturating: refunded > spent
1348        let gas = ResultGas::default()
1349            .with_total_gas_spent(10)
1350            .with_refunded(50);
1351        assert_eq!(gas.spent_sub_refunded(), 0);
1352    }
1353
1354    #[test]
1355    fn test_final_refunded_with_floor_gas() {
1356        // No floor gas: final_refunded == refunded
1357        let gas = ResultGas::default()
1358            .with_total_gas_spent(50000)
1359            .with_refunded(10000);
1360        assert_eq!(gas.tx_gas_used(), 40000);
1361        assert_eq!(gas.final_refunded(), 10000);
1362
1363        // Floor gas active (spent_sub_refunded < floor_gas): final_refunded == 0
1364        // spent=50000, refunded=10000, spent_sub_refunded=40000 < floor_gas=45000
1365        let gas = ResultGas::default()
1366            .with_total_gas_spent(50000)
1367            .with_refunded(10000)
1368            .with_floor_gas(45000);
1369        assert_eq!(gas.tx_gas_used(), 45000);
1370        assert_eq!(gas.final_refunded(), 0);
1371
1372        // Floor gas inactive (spent_sub_refunded >= floor_gas): final_refunded == refunded
1373        // spent=50000, refunded=10000, spent_sub_refunded=40000 >= floor_gas=30000
1374        let gas = ResultGas::default()
1375            .with_total_gas_spent(50000)
1376            .with_refunded(10000)
1377            .with_floor_gas(30000);
1378        assert_eq!(gas.tx_gas_used(), 40000);
1379        assert_eq!(gas.final_refunded(), 10000);
1380
1381        // Edge case: spent_sub_refunded == floor_gas exactly
1382        // spent=50000, refunded=10000, spent_sub_refunded=40000 == floor_gas=40000
1383        let gas = ResultGas::default()
1384            .with_total_gas_spent(50000)
1385            .with_refunded(10000)
1386            .with_floor_gas(40000);
1387        assert_eq!(gas.tx_gas_used(), 40000);
1388        assert_eq!(gas.final_refunded(), 10000);
1389    }
1390
1391    #[test]
1392    fn test_block_regular_gas_used_no_floor_no_refund() {
1393        // block_regular_gas_used = total_gas_spent - state_gas_spent.
1394        // Refund and floor only affect tx_gas_used, never block_regular.
1395
1396        // No state, no refund, no floor.
1397        let gas = ResultGas::default().with_total_gas_spent(100_000);
1398        assert_eq!(gas.block_regular_gas_used(), 100_000);
1399
1400        // With state gas.
1401        let gas = ResultGas::default()
1402            .with_total_gas_spent(100_000)
1403            .with_state_gas_spent(30_000);
1404        assert_eq!(gas.block_regular_gas_used(), 70_000);
1405        assert_eq!(gas.block_state_gas_used(), 30_000);
1406
1407        // Refund present: tx_gas_used drops, block_regular does not.
1408        let gas = ResultGas::default()
1409            .with_total_gas_spent(100_000)
1410            .with_refunded(10_000)
1411            .with_state_gas_spent(30_000);
1412        assert_eq!(gas.tx_gas_used(), 90_000);
1413        assert_eq!(gas.block_regular_gas_used(), 70_000);
1414
1415        // Floor active: tx_gas_used floors up, block_regular does not.
1416        let gas = ResultGas::default()
1417            .with_total_gas_spent(100_000)
1418            .with_refunded(90_000)
1419            .with_floor_gas(50_000)
1420            .with_state_gas_spent(30_000);
1421        assert_eq!(gas.tx_gas_used(), 50_000);
1422        assert_eq!(gas.block_regular_gas_used(), 70_000);
1423
1424        // State gas exceeds total → saturates to 0.
1425        let gas = ResultGas::default()
1426            .with_total_gas_spent(20_000)
1427            .with_state_gas_spent(30_000);
1428        assert_eq!(gas.block_regular_gas_used(), 0);
1429    }
1430}