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 used less than the required amount of gas")]
253    GasMismatch,
254    /// The transaction doesn't contain enough gas to evaluate the predicate
255    #[display(fmt = "Insufficient gas available for single predicate")]
256    OutOfGas,
257    /// The predicate owner does not correspond to the predicate code
258    #[display(fmt = "Predicate owner invalid, doesn't match code root")]
259    InvalidOwner,
260    /// The predicate wasn't successfully evaluated to true
261    #[display(fmt = "Predicate failed to evaluate")]
262    False,
263    /// The predicate gas used was not specified before execution
264    #[display(fmt = "Predicate failed to evaluate")]
265    GasNotSpecified,
266    /// The transaction's `max_gas` is greater than the global gas limit.
267    #[display(fmt = "Transaction exceeds total gas allowance {_0:?}")]
268    TransactionExceedsTotalGasAllowance(Word),
269    /// The cumulative gas overflowed the u64 accumulator
270    #[display(fmt = "Cumulative gas computation overflowed the u64 accumulator")]
271    GasOverflow,
272    /// Invalid interpreter state reached unexpectedly, this is a bug
273    #[display(fmt = "Invalid interpreter state reached unexpectedly")]
274    Bug(Bug),
275    /// The VM execution resulted in a well-formed panic, caused by an instruction.
276    #[display(fmt = "Execution error: {_0:?}")]
277    PanicInstruction(PanicInstruction),
278    /// The VM execution resulted in a well-formed panic not caused by an instruction.
279    #[display(fmt = "Execution error: {_0:?}")]
280    Panic(PanicReason),
281    /// Predicate verification failed since it attempted to access storage
282    #[display(
283        fmt = "Predicate verification failed since it attempted to access storage"
284    )]
285    Storage,
286}
287
288impl From<InterpreterError<predicate::PredicateStorageError>>
289    for PredicateVerificationFailed
290{
291    fn from(error: InterpreterError<predicate::PredicateStorageError>) -> Self {
292        match error {
293            error if error.panic_reason() == Some(PanicReason::OutOfGas) => {
294                PredicateVerificationFailed::OutOfGas
295            }
296            InterpreterError::Panic(reason) => PredicateVerificationFailed::Panic(reason),
297            InterpreterError::PanicInstruction(result) => {
298                PredicateVerificationFailed::PanicInstruction(result)
299            }
300            InterpreterError::Bug(bug) => PredicateVerificationFailed::Bug(bug),
301            InterpreterError::Storage(_) => PredicateVerificationFailed::Storage,
302            _ => PredicateVerificationFailed::False,
303        }
304    }
305}
306
307impl From<Bug> for PredicateVerificationFailed {
308    fn from(bug: Bug) -> Self {
309        Self::Bug(bug)
310    }
311}
312
313impl From<PanicReason> for PredicateVerificationFailed {
314    fn from(reason: PanicReason) -> Self {
315        Self::Panic(reason)
316    }
317}
318
319impl From<PanicOrBug> for PredicateVerificationFailed {
320    fn from(err: PanicOrBug) -> Self {
321        match err {
322            PanicOrBug::Panic(reason) => Self::from(reason),
323            PanicOrBug::Bug(bug) => Self::Bug(bug),
324        }
325    }
326}
327
328/// Traceable bug variants
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumMessage)]
330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
331pub enum BugVariant {
332    /// Context gas increase has overflow
333    #[strum(
334        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."
335    )]
336    ContextGasOverflow,
337
338    /// Context gas increase has underflow
339    #[strum(
340        message = "The context gas cannot underflow since any script should halt upon gas exhaustion."
341    )]
342    ContextGasUnderflow,
343
344    /// Global gas subtraction has underflow
345    #[strum(
346        message = "The gas consumption cannot exceed the gas context since it is capped by the transaction gas limit."
347    )]
348    GlobalGasUnderflow,
349
350    /// The global gas is less than the context gas.
351    #[strum(message = "The global gas cannot ever be less than the context gas. ")]
352    GlobalGasLessThanContext,
353
354    /// The stack point has overflow
355    #[strum(message = "The stack pointer cannot overflow under checked operations.")]
356    StackPointerOverflow,
357
358    /// Code size of a contract doesn't fit into a Word. This is prevented by tx size
359    /// limit.
360    #[strum(message = "Contract size doesn't fit into a word.")]
361    CodeSizeOverflow,
362
363    /// Refund cannot be computed in the current vm state.
364    #[strum(message = "Refund cannot be computed in the current vm state.")]
365    UncomputableRefund,
366
367    /// Receipts context is full, but there's an attempt to add more receipts.
368    #[strum(message = "Receipts context is full, cannot add new receipts.")]
369    ReceiptsCtxFull,
370
371    /// Witness index is out of bounds.
372    #[strum(message = "Witness index is out of bounds.")]
373    WitnessIndexOutOfBounds,
374
375    /// The witness subsection index is higher than the total number of parts.
376    #[strum(
377        message = "The witness subsection index is higher than the total number of parts."
378    )]
379    NextSubsectionIndexIsHigherThanTotalNumberOfParts,
380}
381
382impl fmt::Display for BugVariant {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        use strum::EnumMessage;
385        if let Some(msg) = self.get_message() {
386            write!(f, "{}", msg)
387        } else {
388            write!(f, "{:?}", self)
389        }
390    }
391}
392
393/// VM encountered unexpected state. This is a bug.
394/// The execution must terminate since the VM is in an invalid state.
395///
396/// The bug it self is identified by the caller location.
397#[derive(Debug, Clone)]
398#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
399#[must_use]
400pub struct Bug {
401    /// Source code location of the bug, in `path/to/file.rs:line:column` notation
402    location: String,
403
404    /// Type of bug
405    variant: BugVariant,
406
407    /// Additional error message for the bug, if it's caused by a runtime error
408    inner_message: Option<String>,
409
410    /// Optionally include a backtrace for the instruction triggering this bug.
411    /// This is only available when the `backtrace` feature is enabled.
412    #[cfg(feature = "backtrace")]
413    bt: backtrace::Backtrace,
414}
415
416impl Bug {
417    /// Construct a new bug with the specified variant, using caller location for
418    /// idenitfying the bug.
419    #[track_caller]
420    pub fn new(variant: BugVariant) -> Self {
421        let caller = core::panic::Location::caller();
422        let location = format!("{}:{}:{}", caller.file(), caller.line(), caller.column());
423        Self {
424            location,
425            variant,
426            inner_message: None,
427            #[cfg(feature = "backtrace")]
428            bt: backtrace::Backtrace::new(),
429        }
430    }
431
432    /// Set an additional error message.
433    pub fn with_message<E: ToString>(mut self, error: E) -> Self {
434        self.inner_message = Some(error.to_string());
435        self
436    }
437}
438
439impl PartialEq for Bug {
440    fn eq(&self, other: &Self) -> bool {
441        self.location == other.location
442    }
443}
444
445#[cfg(feature = "backtrace")]
446mod bt {
447    use super::*;
448    use backtrace::Backtrace;
449
450    impl Bug {
451        /// Backtrace data
452        pub const fn bt(&self) -> &Backtrace {
453            &self.bt
454        }
455    }
456}
457
458impl fmt::Display for Bug {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460        use percent_encoding::{
461            utf8_percent_encode,
462            NON_ALPHANUMERIC,
463        };
464
465        let issue_title = format!("Bug report: {:?} in {}", self.variant, self.location);
466
467        let issue_body = format!(
468            "Error: {:?} {}\nLocation: {}\nVersion: {} {}\n",
469            self.variant,
470            self.inner_message
471                .as_ref()
472                .map(|msg| format!("({msg})"))
473                .unwrap_or_default(),
474            self.location,
475            env!("CARGO_PKG_NAME"),
476            env!("CARGO_PKG_VERSION")
477        );
478
479        write!(
480            f,
481            concat!(
482                "Encountered a bug! Please report this using the following link: ",
483                "https://github.com/FuelLabs/fuel-vm/issues/new",
484                "?title={}",
485                "&body={}",
486                "\n\n",
487                "{:?} error in {}: {} {}\n",
488            ),
489            utf8_percent_encode(&issue_title, NON_ALPHANUMERIC),
490            utf8_percent_encode(&issue_body, NON_ALPHANUMERIC),
491            self.variant,
492            self.location,
493            self.variant,
494            self.inner_message
495                .as_ref()
496                .map(|msg| format!("({msg})"))
497                .unwrap_or_default(),
498        )?;
499
500        #[cfg(feature = "backtrace")]
501        {
502            write!(f, "\nBacktrace:\n{:?}\n", self.bt)?;
503        }
504
505        Ok(())
506    }
507}
508
509/// Runtime error description that should either be specified in the protocol or
510/// halt the execution.
511#[derive(Debug, Clone, PartialEq)]
512#[must_use]
513pub enum PanicOrBug {
514    /// VM panic
515    Panic(PanicReason),
516    /// Invalid interpreter state reached unexpectedly, this is a bug
517    Bug(Bug),
518}
519
520impl From<PanicReason> for PanicOrBug {
521    fn from(panic: PanicReason) -> Self {
522        Self::Panic(panic)
523    }
524}
525
526impl From<Bug> for PanicOrBug {
527    fn from(bug: Bug) -> Self {
528        Self::Bug(bug)
529    }
530}
531
532impl<StorageError> From<PanicOrBug> for RuntimeError<StorageError> {
533    fn from(value: PanicOrBug) -> Self {
534        match value {
535            PanicOrBug::Panic(reason) => Self::Recoverable(reason),
536            PanicOrBug::Bug(bug) => Self::Bug(bug),
537        }
538    }
539}
540
541impl<StorageError> From<PanicOrBug> for InterpreterError<StorageError> {
542    fn from(value: PanicOrBug) -> Self {
543        match value {
544            PanicOrBug::Panic(reason) => Self::Panic(reason),
545            PanicOrBug::Bug(bug) => Self::Bug(bug),
546        }
547    }
548}
549
550/// Result of a operation that doesn't access storage
551pub type SimpleResult<T> = Result<T, PanicOrBug>;
552
553/// Result of a operation that accesses storage
554pub type IoResult<T, S> = Result<T, RuntimeError<S>>;
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559
560    #[test]
561    fn bug_report_message() {
562        let bug = Bug::new(BugVariant::ContextGasOverflow).with_message("Test message");
563        let text = format!("{}", bug);
564        assert!(text.contains(file!()));
565        assert!(text.contains("https://github.com/FuelLabs/fuel-vm/issues/new"));
566        assert!(text.contains("ContextGasOverflow"));
567        assert!(text.contains("Test message"));
568    }
569}