fuel_vm/
error.rs

1//! Runtime interpreter error implementation
2
3use fuel_asm::{
4    PanicInstruction,
5    PanicReason,
6    RawInstruction,
7    Word,
8};
9use fuel_tx::ValidityError;
10
11use crate::checked_transaction::CheckError;
12use alloc::{
13    format,
14    string::{
15        String,
16        ToString,
17    },
18};
19use core::{
20    convert::Infallible,
21    fmt,
22};
23
24use crate::storage::predicate;
25
26/// Interpreter runtime error variants.
27#[derive(Debug, derive_more::Display)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum InterpreterError<StorageError> {
30    /// The instructions execution resulted in a well-formed panic, caused by an
31    /// explicit instruction.
32    #[display(fmt = "Execution error: {_0:?}")]
33    PanicInstruction(PanicInstruction),
34    /// The VM execution resulted in a well-formed panic. This panic wasn't
35    /// caused by an instruction contained in the transaction or a called
36    /// contract.
37    #[display(fmt = "Execution error: {_0:?}")]
38    Panic(PanicReason),
39    /// Failed while checking the transaction.
40    #[display(fmt = "Failed to check the transaction: {_0:?}")]
41    CheckError(CheckError),
42    /// No transaction was initialized in the interpreter. It cannot provide
43    /// state transitions.
44    #[display(fmt = "Execution error")]
45    NoTransactionInitialized,
46    #[display(fmt = "Execution error")]
47    /// The debug state is not initialized; debug routines can't be called.
48    DebugStateNotInitialized,
49    /// Storage I/O error
50    #[display(fmt = "Storage error: {}", _0)]
51    Storage(StorageError),
52    /// Encountered a bug
53    #[display(fmt = "Bug: {_0}")]
54    Bug(Bug),
55    /// The `Ready` transaction provided to `Interpreter` doesn't have expected gas price
56    #[display(
57        fmt = "The transaction's gas price is wrong: expected {expected}, got {actual}"
58    )]
59    ReadyTransactionWrongGasPrice {
60        /// Expected gas price
61        expected: Word,
62        /// Actual gas price
63        actual: Word,
64    },
65}
66
67impl<StorageError> InterpreterError<StorageError> {
68    /// Describe the error as recoverable or halt.
69    pub fn from_runtime(
70        error: RuntimeError<StorageError>,
71        instruction: RawInstruction,
72    ) -> Self {
73        match error {
74            RuntimeError::Recoverable(reason) => {
75                Self::PanicInstruction(PanicInstruction::error(reason, instruction))
76            }
77            _ => Self::from(error),
78        }
79    }
80
81    /// Return the specified panic reason that caused this error, if applicable.
82    pub const fn panic_reason(&self) -> Option<PanicReason> {
83        match self {
84            Self::PanicInstruction(result) => Some(*result.reason()),
85            Self::Panic(reason) => Some(*reason),
86            _ => None,
87        }
88    }
89
90    /// Return the instruction that caused this error, if applicable.
91    pub const fn instruction(&self) -> Option<&RawInstruction> {
92        match self {
93            Self::PanicInstruction(result) => Some(result.instruction()),
94            _ => None,
95        }
96    }
97
98    /// Return the underlying `InstructionResult` if this instance is
99    /// `PanicInstruction`; returns `None` otherwise.
100    pub fn instruction_result(&self) -> Option<PanicInstruction> {
101        match self {
102            Self::PanicInstruction(r) => Some(*r),
103            _ => None,
104        }
105    }
106}
107
108impl<StorageError> InterpreterError<StorageError>
109where
110    StorageError: fmt::Debug,
111{
112    /// Make non-generic by converting the storage error to a string.
113    pub fn erase_generics(&self) -> InterpreterError<String> {
114        match self {
115            Self::Storage(e) => InterpreterError::Storage(format!("{e:?}")),
116            Self::PanicInstruction(e) => InterpreterError::PanicInstruction(*e),
117            Self::Panic(e) => InterpreterError::Panic(*e),
118            Self::NoTransactionInitialized => InterpreterError::NoTransactionInitialized,
119            Self::DebugStateNotInitialized => InterpreterError::DebugStateNotInitialized,
120            Self::Bug(e) => InterpreterError::Bug(e.clone()),
121            Self::CheckError(e) => InterpreterError::CheckError(e.clone()),
122            InterpreterError::ReadyTransactionWrongGasPrice { expected, actual } => {
123                InterpreterError::ReadyTransactionWrongGasPrice {
124                    expected: *expected,
125                    actual: *actual,
126                }
127            }
128        }
129    }
130}
131
132impl<StorageError> From<RuntimeError<StorageError>> for InterpreterError<StorageError> {
133    fn from(error: RuntimeError<StorageError>) -> Self {
134        match error {
135            RuntimeError::Recoverable(e) => Self::Panic(e),
136            RuntimeError::Bug(e) => Self::Bug(e),
137            RuntimeError::Storage(e) => Self::Storage(e),
138        }
139    }
140}
141
142impl<StorageError> PartialEq for InterpreterError<StorageError>
143where
144    StorageError: PartialEq,
145{
146    fn eq(&self, other: &Self) -> bool {
147        match (self, other) {
148            (Self::PanicInstruction(s), Self::PanicInstruction(o)) => s == o,
149            (Self::Panic(s), Self::Panic(o)) => s == o,
150            (Self::NoTransactionInitialized, Self::NoTransactionInitialized) => true,
151            (Self::Storage(a), Self::Storage(b)) => a == b,
152            (Self::DebugStateNotInitialized, Self::DebugStateNotInitialized) => true,
153
154            _ => false,
155        }
156    }
157}
158
159impl<StorageError> From<Bug> for InterpreterError<StorageError> {
160    fn from(bug: Bug) -> Self {
161        Self::Bug(bug)
162    }
163}
164
165impl<StorageError> From<Infallible> for InterpreterError<StorageError> {
166    fn from(_: Infallible) -> Self {
167        unreachable!()
168    }
169}
170
171impl<StorageError> From<ValidityError> for InterpreterError<StorageError> {
172    fn from(err: ValidityError) -> Self {
173        Self::CheckError(CheckError::Validity(err))
174    }
175}
176
177/// Runtime error description that should either be specified in the protocol or
178/// halt the execution.
179#[derive(Debug)]
180#[must_use]
181pub enum RuntimeError<StorageError> {
182    /// Specified error with well-formed fallback strategy, i.e. vm panics.
183    Recoverable(PanicReason),
184    /// Invalid interpreter state reached unexpectedly, this is a bug
185    Bug(Bug),
186    /// Storage io error
187    Storage(StorageError),
188}
189
190impl<StorageError> RuntimeError<StorageError> {
191    /// Flag whether the error is recoverable.
192    pub const fn is_recoverable(&self) -> bool {
193        matches!(self, Self::Recoverable(_))
194    }
195
196    /// Flag whether the error must halt the execution.
197    pub const fn must_halt(&self) -> bool {
198        !self.is_recoverable()
199    }
200}
201
202impl<StorageError: PartialEq> PartialEq for RuntimeError<StorageError> {
203    fn eq(&self, other: &Self) -> bool {
204        match (self, other) {
205            (RuntimeError::Recoverable(a), RuntimeError::Recoverable(b)) => a == b,
206            (RuntimeError::Bug(a), RuntimeError::Bug(b)) => a == b,
207            (RuntimeError::Storage(a), RuntimeError::Storage(b)) => a == b,
208            _ => false,
209        }
210    }
211}
212
213impl<StorageError: core::fmt::Debug> fmt::Display for RuntimeError<StorageError> {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            Self::Recoverable(reason) => write!(f, "Recoverable error: {}", reason),
217            Self::Bug(err) => write!(f, "Bug: {}", err),
218            Self::Storage(err) => write!(f, "Unrecoverable storage error: {:?}", err),
219        }
220    }
221}
222
223impl<StorageError> From<PanicReason> for RuntimeError<StorageError> {
224    fn from(value: PanicReason) -> Self {
225        Self::Recoverable(value)
226    }
227}
228
229impl<StorageError> From<core::array::TryFromSliceError> for RuntimeError<StorageError> {
230    fn from(value: core::array::TryFromSliceError) -> Self {
231        Self::Recoverable(value.into())
232    }
233}
234
235impl<StorageError> From<Bug> for RuntimeError<StorageError> {
236    fn from(bug: Bug) -> Self {
237        Self::Bug(bug)
238    }
239}
240
241impl<StorageError> From<Infallible> for RuntimeError<StorageError> {
242    fn from(_: Infallible) -> Self {
243        unreachable!()
244    }
245}
246
247/// Predicates checking failed
248#[derive(Debug, Clone, PartialEq, derive_more::Display)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250pub enum PredicateVerificationFailed {
251    /// The predicate did not use the amount of gas provided
252    #[display(fmt = "Predicate {index} used less than the required amount of gas")]
253    GasMismatch {
254        /// Input index of the predicate
255        index: usize,
256    },
257    /// The transaction doesn't contain enough gas to evaluate the predicate
258    #[display(fmt = "Insufficient gas available for predicate {index}")]
259    OutOfGas {
260        /// Input index of the predicate
261        index: usize,
262    },
263    /// The predicate owner does not correspond to the predicate code
264    #[display(fmt = "Predicate {index} owner invalid, doesn't match code root")]
265    InvalidOwner {
266        /// Input index of the predicate
267        index: usize,
268    },
269    /// The predicate wasn't successfully evaluated to true
270    #[display(fmt = "Predicate {index} failed to evaluate")]
271    False {
272        /// Input index of the predicate
273        index: usize,
274    },
275    /// The predicate gas used was not specified before execution
276    #[display(fmt = "Predicate {index} failed to evaluate")]
277    GasNotSpecified {
278        /// Input index of the predicate
279        index: usize,
280    },
281    /// The transaction's `max_gas` is greater than the global gas limit.
282    #[display(fmt = "Transaction exceeds total gas allowance {_0:?}")]
283    TransactionExceedsTotalGasAllowance(Word),
284    /// The cumulative gas overflowed the u64 accumulator
285    #[display(fmt = "Cumulative gas computation overflowed the u64 accumulator")]
286    GasOverflow,
287    /// Invalid interpreter state reached unexpectedly, this is a bug
288    #[display(fmt = "Invalid interpreter state reached unexpectedly")]
289    Bug(Bug),
290    /// The VM execution resulted in a well-formed panic, caused by an instruction.
291    #[display(fmt = "Execution error: {instruction:?} in input predicate {index}")]
292    PanicInstruction {
293        /// Input index of the predicate
294        index: usize,
295        /// Instruction that caused the panic
296        instruction: PanicInstruction,
297    },
298    /// The VM execution resulted in a well-formed panic not caused by an instruction.
299    #[display(fmt = "Execution error: {reason:?} in input predicate {index}")]
300    Panic {
301        /// Input index of the predicate
302        index: usize,
303        /// Panic reason
304        reason: PanicReason,
305    },
306    /// Predicate verification failed since it attempted to access storage
307    #[display(fmt = "Predicate {index} attempted to access storage")]
308    Storage {
309        /// Input index of the predicate
310        index: usize,
311    },
312}
313
314impl PredicateVerificationFailed {
315    /// Construct a new `PredicateVerificationFailed` from the given
316    /// `InterpreterError` and the index of the predicate that caused it.
317    pub(crate) fn interpreter_error(
318        index: usize,
319        error: InterpreterError<predicate::PredicateStorageError>,
320    ) -> Self {
321        match error {
322            error if error.panic_reason() == Some(PanicReason::OutOfGas) => {
323                Self::OutOfGas { index }
324            }
325            InterpreterError::Panic(reason) => Self::Panic { index, reason },
326            InterpreterError::PanicInstruction(instruction) => {
327                Self::PanicInstruction { index, instruction }
328            }
329            InterpreterError::Bug(bug) => Self::Bug(bug),
330            InterpreterError::Storage(_) => Self::Storage { index },
331            _ => Self::False { index },
332        }
333    }
334}
335
336impl From<Bug> for PredicateVerificationFailed {
337    fn from(bug: Bug) -> Self {
338        Self::Bug(bug)
339    }
340}
341
342/// Traceable bug variants
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumMessage)]
344#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
345pub enum BugVariant {
346    /// Context gas increase has overflow
347    #[strum(
348        message = "The context gas cannot overflow since it was created by a valid transaction and the total gas does not increase - hence, it always fits a word."
349    )]
350    ContextGasOverflow,
351
352    /// Context gas increase has underflow
353    #[strum(
354        message = "The context gas cannot underflow since any script should halt upon gas exhaustion."
355    )]
356    ContextGasUnderflow,
357
358    /// Global gas subtraction has underflow
359    #[strum(
360        message = "The gas consumption cannot exceed the gas context since it is capped by the transaction gas limit."
361    )]
362    GlobalGasUnderflow,
363
364    /// The global gas is less than the context gas.
365    #[strum(message = "The global gas cannot ever be less than the context gas. ")]
366    GlobalGasLessThanContext,
367
368    /// The stack point has overflow
369    #[strum(message = "The stack pointer cannot overflow under checked operations.")]
370    StackPointerOverflow,
371
372    /// Code size of a contract doesn't fit into a Word. This is prevented by tx size
373    /// limit.
374    #[strum(message = "Contract size doesn't fit into a word.")]
375    CodeSizeOverflow,
376
377    /// Refund cannot be computed in the current vm state.
378    #[strum(message = "Refund cannot be computed in the current vm state.")]
379    UncomputableRefund,
380
381    /// Receipts context is full, but there's an attempt to add more receipts.
382    #[strum(message = "Receipts context is full, cannot add new receipts.")]
383    ReceiptsCtxFull,
384
385    /// Witness index is out of bounds.
386    #[strum(message = "Witness index is out of bounds.")]
387    WitnessIndexOutOfBounds,
388
389    /// The witness subsection index is higher than the total number of parts.
390    #[strum(
391        message = "The witness subsection index is higher than the total number of parts."
392    )]
393    NextSubsectionIndexIsHigherThanTotalNumberOfParts,
394
395    /// Input index more than u16::MAX was used internally.
396    #[strum(message = "Input index more than u16::MAX was used internally.")]
397    InputIndexMoreThanU16Max,
398    #[strum(message = "Transaction owner index is out of bounds.")]
399    /// The transaction owner index is out of bounds.
400    TransactionOwnerIndexOutOfBounds,
401    #[strum(message = "The `Input::Owner` at the given index is missing an owner.")]
402    /// The `Input::Owner` at the given index is missing an owner.
403    TransactionOwnerInputHasNoOwner {
404        /// Input index used in the policy
405        index: usize,
406    },
407}
408
409impl fmt::Display for BugVariant {
410    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411        use strum::EnumMessage;
412        if let Some(msg) = self.get_message() {
413            write!(f, "{}", msg)
414        } else {
415            write!(f, "{:?}", self)
416        }
417    }
418}
419
420/// VM encountered unexpected state. This is a bug.
421/// The execution must terminate since the VM is in an invalid state.
422///
423/// The bug it self is identified by the caller location.
424#[derive(Debug, Clone)]
425#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
426#[must_use]
427pub struct Bug {
428    /// Source code location of the bug, in `path/to/file.rs:line:column` notation
429    location: String,
430
431    /// Type of bug
432    variant: BugVariant,
433
434    /// Additional error message for the bug, if it's caused by a runtime error
435    inner_message: Option<String>,
436
437    /// Optionally include a backtrace for the instruction triggering this bug.
438    /// This is only available when the `backtrace` feature is enabled.
439    #[cfg(feature = "backtrace")]
440    bt: backtrace::Backtrace,
441}
442
443impl Bug {
444    /// Construct a new bug with the specified variant, using caller location for
445    /// idenitfying the bug.
446    #[track_caller]
447    pub fn new(variant: BugVariant) -> Self {
448        let caller = core::panic::Location::caller();
449        let location = format!("{}:{}:{}", caller.file(), caller.line(), caller.column());
450        Self {
451            location,
452            variant,
453            inner_message: None,
454            #[cfg(feature = "backtrace")]
455            bt: backtrace::Backtrace::new(),
456        }
457    }
458
459    /// Set an additional error message.
460    pub fn with_message<E: ToString>(mut self, error: E) -> Self {
461        self.inner_message = Some(error.to_string());
462        self
463    }
464}
465
466impl PartialEq for Bug {
467    fn eq(&self, other: &Self) -> bool {
468        self.location == other.location
469    }
470}
471
472#[cfg(feature = "backtrace")]
473mod bt {
474    use super::*;
475    use backtrace::Backtrace;
476
477    impl Bug {
478        /// Backtrace data
479        pub const fn bt(&self) -> &Backtrace {
480            &self.bt
481        }
482    }
483}
484
485impl fmt::Display for Bug {
486    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487        use percent_encoding::{
488            NON_ALPHANUMERIC,
489            utf8_percent_encode,
490        };
491
492        let issue_title = format!("Bug report: {:?} in {}", self.variant, self.location);
493
494        let issue_body = format!(
495            "Error: {:?} {}\nLocation: {}\nVersion: {} {}\n",
496            self.variant,
497            self.inner_message
498                .as_ref()
499                .map(|msg| format!("({msg})"))
500                .unwrap_or_default(),
501            self.location,
502            env!("CARGO_PKG_NAME"),
503            env!("CARGO_PKG_VERSION")
504        );
505
506        write!(
507            f,
508            concat!(
509                "Encountered a bug! Please report this using the following link: ",
510                "https://github.com/FuelLabs/fuel-vm/issues/new",
511                "?title={}",
512                "&body={}",
513                "\n\n",
514                "{:?} error in {}: {} {}\n",
515            ),
516            utf8_percent_encode(&issue_title, NON_ALPHANUMERIC),
517            utf8_percent_encode(&issue_body, NON_ALPHANUMERIC),
518            self.variant,
519            self.location,
520            self.variant,
521            self.inner_message
522                .as_ref()
523                .map(|msg| format!("({msg})"))
524                .unwrap_or_default(),
525        )?;
526
527        #[cfg(feature = "backtrace")]
528        {
529            write!(f, "\nBacktrace:\n{:?}\n", self.bt)?;
530        }
531
532        Ok(())
533    }
534}
535
536/// Runtime error description that should either be specified in the protocol or
537/// halt the execution.
538#[derive(Debug, Clone, PartialEq)]
539#[must_use]
540pub enum PanicOrBug {
541    /// VM panic
542    Panic(PanicReason),
543    /// Invalid interpreter state reached unexpectedly, this is a bug
544    Bug(Bug),
545}
546
547impl From<PanicReason> for PanicOrBug {
548    fn from(panic: PanicReason) -> Self {
549        Self::Panic(panic)
550    }
551}
552
553impl From<Bug> for PanicOrBug {
554    fn from(bug: Bug) -> Self {
555        Self::Bug(bug)
556    }
557}
558
559impl<StorageError> From<PanicOrBug> for RuntimeError<StorageError> {
560    fn from(value: PanicOrBug) -> Self {
561        match value {
562            PanicOrBug::Panic(reason) => Self::Recoverable(reason),
563            PanicOrBug::Bug(bug) => Self::Bug(bug),
564        }
565    }
566}
567
568impl<StorageError> From<PanicOrBug> for InterpreterError<StorageError> {
569    fn from(value: PanicOrBug) -> Self {
570        match value {
571            PanicOrBug::Panic(reason) => Self::Panic(reason),
572            PanicOrBug::Bug(bug) => Self::Bug(bug),
573        }
574    }
575}
576
577/// Result of a operation that doesn't access storage
578pub type SimpleResult<T> = Result<T, PanicOrBug>;
579
580/// Result of a operation that accesses storage
581pub type IoResult<T, S> = Result<T, RuntimeError<S>>;
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[test]
588    fn bug_report_message() {
589        let bug = Bug::new(BugVariant::ContextGasOverflow).with_message("Test message");
590        let text = format!("{}", bug);
591        assert!(text.contains(file!()));
592        assert!(text.contains("https://github.com/FuelLabs/fuel-vm/issues/new"));
593        assert!(text.contains("ContextGasOverflow"));
594        assert!(text.contains("Test message"));
595    }
596}