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}
399
400impl fmt::Display for BugVariant {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        use strum::EnumMessage;
403        if let Some(msg) = self.get_message() {
404            write!(f, "{}", msg)
405        } else {
406            write!(f, "{:?}", self)
407        }
408    }
409}
410
411/// VM encountered unexpected state. This is a bug.
412/// The execution must terminate since the VM is in an invalid state.
413///
414/// The bug it self is identified by the caller location.
415#[derive(Debug, Clone)]
416#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
417#[must_use]
418pub struct Bug {
419    /// Source code location of the bug, in `path/to/file.rs:line:column` notation
420    location: String,
421
422    /// Type of bug
423    variant: BugVariant,
424
425    /// Additional error message for the bug, if it's caused by a runtime error
426    inner_message: Option<String>,
427
428    /// Optionally include a backtrace for the instruction triggering this bug.
429    /// This is only available when the `backtrace` feature is enabled.
430    #[cfg(feature = "backtrace")]
431    bt: backtrace::Backtrace,
432}
433
434impl Bug {
435    /// Construct a new bug with the specified variant, using caller location for
436    /// idenitfying the bug.
437    #[track_caller]
438    pub fn new(variant: BugVariant) -> Self {
439        let caller = core::panic::Location::caller();
440        let location = format!("{}:{}:{}", caller.file(), caller.line(), caller.column());
441        Self {
442            location,
443            variant,
444            inner_message: None,
445            #[cfg(feature = "backtrace")]
446            bt: backtrace::Backtrace::new(),
447        }
448    }
449
450    /// Set an additional error message.
451    pub fn with_message<E: ToString>(mut self, error: E) -> Self {
452        self.inner_message = Some(error.to_string());
453        self
454    }
455}
456
457impl PartialEq for Bug {
458    fn eq(&self, other: &Self) -> bool {
459        self.location == other.location
460    }
461}
462
463#[cfg(feature = "backtrace")]
464mod bt {
465    use super::*;
466    use backtrace::Backtrace;
467
468    impl Bug {
469        /// Backtrace data
470        pub const fn bt(&self) -> &Backtrace {
471            &self.bt
472        }
473    }
474}
475
476impl fmt::Display for Bug {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        use percent_encoding::{
479            NON_ALPHANUMERIC,
480            utf8_percent_encode,
481        };
482
483        let issue_title = format!("Bug report: {:?} in {}", self.variant, self.location);
484
485        let issue_body = format!(
486            "Error: {:?} {}\nLocation: {}\nVersion: {} {}\n",
487            self.variant,
488            self.inner_message
489                .as_ref()
490                .map(|msg| format!("({msg})"))
491                .unwrap_or_default(),
492            self.location,
493            env!("CARGO_PKG_NAME"),
494            env!("CARGO_PKG_VERSION")
495        );
496
497        write!(
498            f,
499            concat!(
500                "Encountered a bug! Please report this using the following link: ",
501                "https://github.com/FuelLabs/fuel-vm/issues/new",
502                "?title={}",
503                "&body={}",
504                "\n\n",
505                "{:?} error in {}: {} {}\n",
506            ),
507            utf8_percent_encode(&issue_title, NON_ALPHANUMERIC),
508            utf8_percent_encode(&issue_body, NON_ALPHANUMERIC),
509            self.variant,
510            self.location,
511            self.variant,
512            self.inner_message
513                .as_ref()
514                .map(|msg| format!("({msg})"))
515                .unwrap_or_default(),
516        )?;
517
518        #[cfg(feature = "backtrace")]
519        {
520            write!(f, "\nBacktrace:\n{:?}\n", self.bt)?;
521        }
522
523        Ok(())
524    }
525}
526
527/// Runtime error description that should either be specified in the protocol or
528/// halt the execution.
529#[derive(Debug, Clone, PartialEq)]
530#[must_use]
531pub enum PanicOrBug {
532    /// VM panic
533    Panic(PanicReason),
534    /// Invalid interpreter state reached unexpectedly, this is a bug
535    Bug(Bug),
536}
537
538impl From<PanicReason> for PanicOrBug {
539    fn from(panic: PanicReason) -> Self {
540        Self::Panic(panic)
541    }
542}
543
544impl From<Bug> for PanicOrBug {
545    fn from(bug: Bug) -> Self {
546        Self::Bug(bug)
547    }
548}
549
550impl<StorageError> From<PanicOrBug> for RuntimeError<StorageError> {
551    fn from(value: PanicOrBug) -> Self {
552        match value {
553            PanicOrBug::Panic(reason) => Self::Recoverable(reason),
554            PanicOrBug::Bug(bug) => Self::Bug(bug),
555        }
556    }
557}
558
559impl<StorageError> From<PanicOrBug> for InterpreterError<StorageError> {
560    fn from(value: PanicOrBug) -> Self {
561        match value {
562            PanicOrBug::Panic(reason) => Self::Panic(reason),
563            PanicOrBug::Bug(bug) => Self::Bug(bug),
564        }
565    }
566}
567
568/// Result of a operation that doesn't access storage
569pub type SimpleResult<T> = Result<T, PanicOrBug>;
570
571/// Result of a operation that accesses storage
572pub type IoResult<T, S> = Result<T, RuntimeError<S>>;
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn bug_report_message() {
580        let bug = Bug::new(BugVariant::ContextGasOverflow).with_message("Test message");
581        let text = format!("{}", bug);
582        assert!(text.contains(file!()));
583        assert!(text.contains("https://github.com/FuelLabs/fuel-vm/issues/new"));
584        assert!(text.contains("ContextGasOverflow"));
585        assert!(text.contains("Test message"));
586    }
587}