gear_core_errors/
simple.rs

1// Copyright (C) 2023-2025 Gear Technologies Inc.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Simple errors being used for status codes
18
19use enum_iterator::Sequence;
20#[cfg(feature = "codec")]
21use scale_info::{
22    scale::{self, Decode, Encode},
23    TypeInfo,
24};
25
26#[repr(u8)]
27#[derive(
28    Clone,
29    Copy,
30    Debug,
31    PartialEq,
32    Eq,
33    PartialOrd,
34    Ord,
35    Hash,
36    Default,
37    derive_more::Display,
38    derive_more::From,
39    Sequence,
40)]
41#[cfg_attr(feature = "codec", derive(Encode, Decode, TypeInfo), codec(crate = scale), allow(clippy::unnecessary_cast))]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43/// Enum representing reply code with reason of its creation.
44pub enum ReplyCode {
45    /// Success reply.
46    #[display("Success reply sent due to {_0}")]
47    Success(SuccessReplyReason) = 0,
48
49    /// Error reply.
50    #[display("Error reply sent due to {_0}")]
51    Error(ErrorReplyReason) = 1,
52
53    /// Unsupported code.
54    /// Variant exists for backward compatibility.
55    #[default]
56    #[display("<unsupported reply code>")]
57    Unsupported = 255,
58}
59
60impl ReplyCode {
61    fn discriminant(&self) -> u8 {
62        // SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)` `union`
63        // between `repr(C)` structs, each of which has the `u8` discriminant as its first
64        // field, so we can read the discriminant without offsetting the pointer.
65        unsafe { *<*const _>::from(self).cast::<u8>() }
66    }
67
68    /// Converts `ReplyCode` to 4 bytes array.
69    pub fn to_bytes(self) -> [u8; 4] {
70        let mut bytes = [self.discriminant(), 0, 0, 0];
71
72        match self {
73            Self::Success(reason) => bytes[1..].copy_from_slice(&reason.to_bytes()),
74            Self::Error(reason) => bytes[1..].copy_from_slice(&reason.to_bytes()),
75            Self::Unsupported => {}
76        }
77
78        bytes
79    }
80
81    /// Parses 4 bytes array to `ReplyCode`.
82    pub fn from_bytes(bytes: [u8; 4]) -> Self {
83        match bytes[0] {
84            b if Self::Success(Default::default()).discriminant() == b => {
85                let reason_bytes = bytes[1..].try_into().unwrap_or_else(|_| unreachable!());
86                Self::Success(SuccessReplyReason::from_bytes(reason_bytes))
87            }
88            b if Self::Error(Default::default()).discriminant() == b => {
89                let reason_bytes = bytes[1..].try_into().unwrap_or_else(|_| unreachable!());
90                Self::Error(ErrorReplyReason::from_bytes(reason_bytes))
91            }
92            _ => Self::Unsupported,
93        }
94    }
95
96    /// Constructs `ReplyCode::Error(_)` variant from underlying reason.
97    pub fn error(reason: impl Into<ErrorReplyReason>) -> Self {
98        Self::Error(reason.into())
99    }
100
101    /// Returns bool, defining if `ReplyCode` represents success reply.
102    pub fn is_success(&self) -> bool {
103        matches!(self, Self::Success(_))
104    }
105
106    /// Returns bool, defining if `ReplyCode` represents error reply.
107    pub fn is_error(&self) -> bool {
108        matches!(self, Self::Error(_))
109    }
110
111    /// Returns bool, defining if `ReplyCode` represents unsupported reason.
112    pub fn is_unsupported(&self) -> bool {
113        matches!(self, Self::Unsupported)
114    }
115}
116
117#[repr(u8)]
118#[derive(
119    Clone,
120    Copy,
121    Debug,
122    PartialEq,
123    Eq,
124    PartialOrd,
125    Ord,
126    Hash,
127    Default,
128    derive_more::Display,
129    Sequence,
130)]
131#[cfg_attr(feature = "codec", derive(Encode, Decode, TypeInfo), codec(crate = scale), allow(clippy::unnecessary_cast))]
132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
133/// Reason of success reply creation.
134pub enum SuccessReplyReason {
135    /// Success reply was created by system automatically.
136    #[display("automatic sending")]
137    Auto = 0,
138
139    /// Success reply was created by actor manually.
140    #[display("manual sending")]
141    Manual = 1,
142
143    /// Unsupported reason of success reply.
144    /// Variant exists for backward compatibility.
145    #[default]
146    #[display("<unsupported reason>")]
147    Unsupported = 255,
148}
149
150impl SuccessReplyReason {
151    fn to_bytes(self) -> [u8; 3] {
152        [self as u8, 0, 0]
153    }
154
155    fn from_bytes(bytes: [u8; 3]) -> Self {
156        match bytes[0] {
157            b if Self::Auto as u8 == b => Self::Auto,
158            b if Self::Manual as u8 == b => Self::Manual,
159            _ => Self::Unsupported,
160        }
161    }
162}
163
164#[repr(u8)]
165#[derive(
166    Clone,
167    Copy,
168    Debug,
169    PartialEq,
170    Eq,
171    PartialOrd,
172    Ord,
173    Hash,
174    Default,
175    derive_more::Display,
176    derive_more::From,
177    Sequence,
178)]
179#[cfg_attr(feature = "codec", derive(Encode, Decode, TypeInfo), codec(crate = scale), allow(clippy::unnecessary_cast))]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181/// Reason of error reply creation.
182// NOTE: Adding new variants to this enum you must also update `ErrorReplyReason::to_bytes` and
183// `ErrorReplyReason::from_bytes` methods.
184pub enum ErrorReplyReason {
185    /// Error reply was created due to underlying execution error.
186    #[display("execution error ({_0})")]
187    Execution(SimpleExecutionError) = 0,
188
189    /// Destination actor is unavailable, so it can't process the message.
190    #[display("destination actor is unavailable ({_0})")]
191    UnavailableActor(SimpleUnavailableActorError) = 2,
192
193    /// Message has died in Waitlist as out of rent one.
194    #[display("removal from waitlist")]
195    RemovedFromWaitlist = 3,
196
197    /// Unsupported reason of error reply.
198    /// Variant exists for backward compatibility.
199    #[default]
200    #[display("<unsupported reason>")]
201    Unsupported = 255,
202}
203
204impl ErrorReplyReason {
205    /// Returns bool indicating if self is UnavailableActor::ProgramExited variant.
206    pub fn is_exited(&self) -> bool {
207        matches!(
208            self,
209            Self::UnavailableActor(SimpleUnavailableActorError::ProgramExited)
210        )
211    }
212
213    /// Returns bool indicating if self is Execution::UserspacePanic variant.
214    pub fn is_userspace_panic(&self) -> bool {
215        matches!(self, Self::Execution(SimpleExecutionError::UserspacePanic))
216    }
217
218    fn discriminant(&self) -> u8 {
219        // SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)` `union`
220        // between `repr(C)` structs, each of which has the `u8` discriminant as its first
221        // field, so we can read the discriminant without offsetting the pointer.
222        unsafe { *<*const _>::from(self).cast::<u8>() }
223    }
224
225    fn to_bytes(self) -> [u8; 3] {
226        let mut bytes = [self.discriminant(), 0, 0];
227
228        match self {
229            Self::Execution(error) => bytes[1..].copy_from_slice(&error.to_bytes()),
230            Self::UnavailableActor(error) => bytes[1..].copy_from_slice(&error.to_bytes()),
231            Self::RemovedFromWaitlist | Self::Unsupported => {}
232        }
233
234        bytes
235    }
236
237    fn from_bytes(bytes: [u8; 3]) -> Self {
238        match bytes[0] {
239            1 /* removed `FailedToCreateProgram` variant */ |
240            4 /* moved `ReinstrumentationFailure` variant */ => Self::Unsupported,
241            b if Self::Execution(Default::default()).discriminant() == b => {
242                let err_bytes = bytes[1..].try_into().unwrap_or_else(|_| unreachable!());
243                Self::Execution(SimpleExecutionError::from_bytes(err_bytes))
244            }
245            b if Self::UnavailableActor(Default::default()).discriminant() == b => {
246                let err_bytes = bytes[1..].try_into().unwrap_or_else(|_| unreachable!());
247                Self::UnavailableActor(SimpleUnavailableActorError::from_bytes(err_bytes))
248            }
249            b if Self::RemovedFromWaitlist.discriminant() == b => Self::RemovedFromWaitlist,
250            _ => Self::Unsupported,
251        }
252    }
253}
254
255#[repr(u8)]
256#[derive(
257    Clone,
258    Copy,
259    Debug,
260    PartialEq,
261    Eq,
262    PartialOrd,
263    Ord,
264    Hash,
265    Default,
266    derive_more::Display,
267    Sequence,
268)]
269#[cfg_attr(feature = "codec", derive(Encode, Decode, TypeInfo), codec(crate = scale), allow(clippy::unnecessary_cast))]
270#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
271/// Simplified error occurred during execution.
272pub enum SimpleExecutionError {
273    /// Message ran out of gas while executing.
274    #[display("Message ran out of gas")]
275    RanOutOfGas = 0,
276
277    /// Program has reached memory limit while executing.
278    #[display("Program reached memory limit")]
279    MemoryOverflow = 1,
280
281    /// Execution failed with backend error that couldn't been caught.
282    #[display("Message ran into uncatchable error")]
283    BackendError = 2,
284
285    /// Execution failed with userspace panic.
286    ///
287    /// **PAYLOAD**: Arbitrary payload given by the program as `gr_panic` argument.
288    #[display("Message panicked")]
289    UserspacePanic = 3,
290
291    /// Execution failed with `unreachable` instruction call.
292    #[display("Program called WASM `unreachable` instruction")]
293    UnreachableInstruction = 4,
294
295    /// Program has reached stack limit while executing.
296    #[display("Program reached stack limit")]
297    StackLimitExceeded = 5,
298
299    /// Unsupported reason of execution error.
300    /// Variant exists for backward compatibility.
301    #[default]
302    #[display("<unsupported error>")]
303    Unsupported = 255,
304}
305
306impl SimpleExecutionError {
307    fn to_bytes(self) -> [u8; 2] {
308        [self as u8, 0]
309    }
310
311    fn from_bytes(bytes: [u8; 2]) -> Self {
312        match bytes[0] {
313            b if Self::RanOutOfGas as u8 == b => Self::RanOutOfGas,
314            b if Self::MemoryOverflow as u8 == b => Self::MemoryOverflow,
315            b if Self::BackendError as u8 == b => Self::BackendError,
316            b if Self::UserspacePanic as u8 == b => Self::UserspacePanic,
317            b if Self::UnreachableInstruction as u8 == b => Self::UnreachableInstruction,
318            b if Self::StackLimitExceeded as u8 == b => Self::StackLimitExceeded,
319            _ => Self::Unsupported,
320        }
321    }
322}
323
324#[repr(u8)]
325#[derive(
326    Clone,
327    Copy,
328    Debug,
329    PartialEq,
330    Eq,
331    PartialOrd,
332    Ord,
333    Hash,
334    Default,
335    derive_more::Display,
336    Sequence,
337)]
338#[cfg_attr(feature = "codec", derive(Encode, Decode, TypeInfo), codec(crate = scale), allow(clippy::unnecessary_cast))]
339#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
340/// Simplified error occurred because of actor unavailability.
341pub enum SimpleUnavailableActorError {
342    /// Program called `gr_exit` syscall.
343    ///
344    /// **PAYLOAD**: `ActorId` of the exited program's inheritor (`gr_exit` argument).
345    #[display("Program exited")]
346    ProgramExited = 0,
347
348    /// Program was terminated due to failed initialization.
349    #[display("Program was terminated due failed initialization")]
350    InitializationFailure = 1,
351
352    /// Program is not initialized yet.
353    #[display("Program is not initialized yet")]
354    Uninitialized = 2,
355
356    /// Program was not created.
357    #[display("Program was not created")]
358    ProgramNotCreated = 3,
359
360    /// Program re-instrumentation failed.
361    #[display("Program re-instrumentation failed")]
362    ReinstrumentationFailure = 4,
363
364    /// Unsupported reason of inactive actor error.
365    /// Variant exists for backward compatibility.
366    #[default]
367    #[display("<unsupported error>")]
368    Unsupported = 255,
369}
370
371impl SimpleUnavailableActorError {
372    fn to_bytes(self) -> [u8; 2] {
373        [self as u8, 0]
374    }
375
376    fn from_bytes(bytes: [u8; 2]) -> Self {
377        match bytes[0] {
378            b if Self::ProgramExited as u8 == b => Self::ProgramExited,
379            b if Self::InitializationFailure as u8 == b => Self::InitializationFailure,
380            b if Self::Uninitialized as u8 == b => Self::Uninitialized,
381            b if Self::ProgramNotCreated as u8 == b => Self::ProgramNotCreated,
382            b if Self::ReinstrumentationFailure as u8 == b => Self::ReinstrumentationFailure,
383            _ => Self::Unsupported,
384        }
385    }
386}
387
388#[derive(
389    Clone,
390    Copy,
391    Debug,
392    PartialEq,
393    Eq,
394    PartialOrd,
395    Ord,
396    Hash,
397    Default,
398    derive_more::Display,
399    derive_more::From,
400    Sequence,
401)]
402#[cfg_attr(feature = "codec", derive(Encode, Decode, TypeInfo), codec(crate = scale), allow(clippy::unnecessary_cast))]
403#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404/// Enum representing signal code and reason of its creation.
405///
406/// # Testing
407/// See [this document](../signal-code-testing.md).
408pub enum SignalCode {
409    /// Signal was sent due to some execution errors.
410    #[display("Signal message sent due to execution error ({_0})")]
411    Execution(SimpleExecutionError),
412
413    /// Signal was sent due to removal from waitlist as out of rent.
414    #[default]
415    #[display("Signal message sent due to removal from waitlist")]
416    RemovedFromWaitlist,
417}
418
419impl SignalCode {
420    /// Converts `SignalCode` into `u32`.
421    pub const fn to_u32(self) -> u32 {
422        match self {
423            Self::Execution(SimpleExecutionError::UserspacePanic) => 100,
424            Self::Execution(SimpleExecutionError::RanOutOfGas) => 101,
425            Self::Execution(SimpleExecutionError::BackendError) => 102,
426            Self::Execution(SimpleExecutionError::MemoryOverflow) => 103,
427            Self::Execution(SimpleExecutionError::UnreachableInstruction) => 104,
428            Self::Execution(SimpleExecutionError::StackLimitExceeded) => 105,
429            Self::RemovedFromWaitlist => 200,
430            // Must be unreachable.
431            Self::Execution(SimpleExecutionError::Unsupported) => u32::MAX,
432        }
433    }
434
435    /// Parses `SignalCode` from `u32` if possible.
436    pub const fn from_u32(num: u32) -> Option<Self> {
437        let res = match num {
438            v if Self::Execution(SimpleExecutionError::UserspacePanic).to_u32() == v => {
439                Self::Execution(SimpleExecutionError::UserspacePanic)
440            }
441            v if Self::Execution(SimpleExecutionError::RanOutOfGas).to_u32() == v => {
442                Self::Execution(SimpleExecutionError::RanOutOfGas)
443            }
444            v if Self::Execution(SimpleExecutionError::BackendError).to_u32() == v => {
445                Self::Execution(SimpleExecutionError::BackendError)
446            }
447            v if Self::Execution(SimpleExecutionError::MemoryOverflow).to_u32() == v => {
448                Self::Execution(SimpleExecutionError::MemoryOverflow)
449            }
450            v if Self::Execution(SimpleExecutionError::UnreachableInstruction).to_u32() == v => {
451                Self::Execution(SimpleExecutionError::UnreachableInstruction)
452            }
453            v if Self::Execution(SimpleExecutionError::StackLimitExceeded).to_u32() == v => {
454                Self::Execution(SimpleExecutionError::StackLimitExceeded)
455            }
456            v if Self::Execution(SimpleExecutionError::Unsupported).to_u32() == v => {
457                Self::Execution(SimpleExecutionError::Unsupported)
458            }
459            v if Self::RemovedFromWaitlist.to_u32() == v => Self::RemovedFromWaitlist,
460            _ => return None,
461        };
462
463        Some(res)
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn test_forbidden_codes() {
473        let codes = [
474            1, // `FailedToCreateProgram` variant
475            4, // `ReinstrumentationFailure` variant
476        ];
477
478        // check forbidden code is `Unsupported` variant now
479        for code in codes {
480            let err = ErrorReplyReason::from_bytes([code, 0, 0]);
481            assert_eq!(err, ErrorReplyReason::Unsupported);
482        }
483
484        // check forbidden code is never produced
485        for code in enum_iterator::all::<ErrorReplyReason>() {
486            let bytes = code.to_bytes();
487            assert!(!codes.contains(&bytes[0]));
488        }
489    }
490
491    #[test]
492    fn test_reply_code_encode_decode() {
493        for code in enum_iterator::all::<ReplyCode>() {
494            let bytes = code.to_bytes();
495            assert_eq!(code, ReplyCode::from_bytes(bytes));
496        }
497    }
498
499    #[test]
500    fn test_signal_code_encode_decode() {
501        for signal in enum_iterator::all::<SignalCode>() {
502            let code = signal.to_u32();
503            assert_eq!(signal, SignalCode::from_u32(code).unwrap());
504        }
505    }
506}