Skip to main content

thruput_sdk/exn/
library.rs

1use super::fault::*;
2use super::{AuthTokenError, CallingError, HandlingError};
3use crate::ContractTypeSpec;
4use crate::call::CallFault;
5use crate::sys::{SysErr, SyscallErrorCode, SyscallNo};
6use crate::xdr::*;
7use core::error::Error;
8use core::fmt::{self, Display};
9#[cfg(not(target_arch = "riscv32"))]
10use thiserror::Error;
11
12/// An exception generated by SDK internals.
13///
14/// The wire layout is fixed regardless of crate features -- discriminants
15/// and payloads alike. Both halves of that have been broken before:
16/// discriminant 8 was once a feature-gated `Storage(StorageError)` nested
17/// enum (now the flat `StorageSerialization`, with the other storage
18/// conditions at 9 and 10), and the variants carrying a
19/// [`FaultString`](crate::exn::FaultString) once encoded its message under
20/// `debugging`, so an abort payload differed between a host test and the
21/// shipped artifact. `FaultString` now keeps its message host-side and
22/// encodes to nothing. Keep new variants payload-stable the same way.
23#[derive(Debug, PartialEq, ReadXDR, WriteXDR, ConstXDRSize)]
24#[cfg_attr(not(target_arch = "riscv32"), derive(Error))]
25#[derive(ContractTypeSpec)]
26// `wire_crate` fixes the crate label in the selector preimage at `sdk`:
27// every contract throws and matches `LibraryExn`, so its selector is wire
28// ABI and must not follow the package name.
29#[thruput_macros::exn(version = "1.0.0", wire_crate = "sdk")]
30#[repr(i32)]
31#[non_exhaustive]
32pub enum LibraryExn {
33    #[cfg_attr(not(target_arch = "riscv32"), error(transparent))]
34    Syscall(SyscallExn) = 0,
35
36    #[cfg_attr(
37        not(target_arch = "riscv32"),
38        error("Handling a contract call: {0}")
39    )]
40    Handling(HandlingError) = 1,
41
42    #[cfg_attr(
43        not(target_arch = "riscv32"),
44        error("Making a contract call: {0}")
45    )]
46    Calling(CallingError) = 2,
47
48    #[cfg_attr(
49        not(target_arch = "riscv32"),
50        error("Called contract reverted: {0}")
51    )]
52    Called(#[cfg_attr(not(target_arch = "riscv32"), source)] FaultString) = 3,
53
54    #[cfg_attr(
55        not(target_arch = "riscv32"),
56        error("Encoding auth token: {0}")
57    )]
58    AuthToken(AuthTokenError) = 4,
59
60    #[cfg_attr(
61        not(target_arch = "riscv32"),
62        error("bytestring_get_sized: expected {expected}, found {found}")
63    )]
64    BytestringWrongSize { expected: u32, found: u32 } = 5,
65
66    #[cfg_attr(
67        not(target_arch = "riscv32"),
68        error("non-negative int went negative")
69    )]
70    NonNegativeIntUnderflow = 6,
71
72    #[cfg_attr(
73        not(target_arch = "riscv32"),
74        error("non-negative int overflow")
75    )]
76    NonNegativeIntOverflow = 7,
77
78    #[cfg_attr(
79        not(target_arch = "riscv32"),
80        error("storage value failed to (de)serialize")
81    )]
82    StorageSerialization = 8,
83
84    #[cfg_attr(
85        not(target_arch = "riscv32"),
86        error("storage index out of bounds: len={len}, access={access}")
87    )]
88    StorageIndexOutOfBounds { len: u64, access: u64 } = 9,
89
90    #[cfg_attr(
91        not(target_arch = "riscv32"),
92        error("storage keypath component is too long to encode")
93    )]
94    StorageKeypathOverflow = 10,
95
96    #[cfg_attr(
97        not(target_arch = "riscv32"),
98        error(
99            "retrocausal queue entry: expected {expected} bytes, \
100               found {found}"
101        )
102    )]
103    RetrocausalQueueEntryWrongSize { expected: u32, found: u32 } = 11,
104
105    #[cfg_attr(
106        not(target_arch = "riscv32"),
107        error(
108            "capacity constraint: set_capacity conflicts with a prior \
109               write this transaction"
110        )
111    )]
112    CapacityConstraintAlreadyWritten = 12,
113
114    /// The invoked method id matched no entrypoint on this contract.
115    ///
116    /// Thrown by guest dispatch (and by the native mock, for a contract
117    /// registered through `register_mock_package`) in place of the bare
118    /// trap it used to answer with, which a caller could not tell apart
119    /// from an OOB access or a bad instruction. Carries no method id —
120    /// the caller picked the id it invoked.
121    #[cfg_attr(
122        not(target_arch = "riscv32"),
123        error("no entrypoint on this contract matches the invoked method")
124    )]
125    UnknownMethod = 13,
126}
127
128impl From<HandlingError> for LibraryExn {
129    fn from(e: HandlingError) -> Self {
130        Self::Handling(e)
131    }
132}
133
134impl From<CallingError> for LibraryExn {
135    fn from(e: CallingError) -> Self {
136        Self::Calling(e)
137    }
138}
139
140impl From<AuthTokenError> for LibraryExn {
141    fn from(e: AuthTokenError) -> Self {
142        Self::AuthToken(e)
143    }
144}
145
146/// `Result<T, LibraryExn>::or_re_throw()` aborts with the contained
147/// library exception (typed, decodable by selector) rather than a fault.
148impl crate::re_throw::ReThrow for LibraryExn {
149    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
150    fn re_throw(self) -> ! {
151        library_throw(self)
152    }
153}
154
155impl LibraryExn {
156    pub fn as_syscall_exn(&self) -> Option<&SyscallExn> {
157        if let LibraryExn::Syscall(se) = self {
158            Some(se)
159        } else {
160            None
161        }
162    }
163
164    /// True if this is a syscall error saying nothing is stored where a
165    /// storage read looked: a missing key
166    /// ([`kSyscallErrorCodeStorageKeyNotExist`](SyscallErrorCode::kSyscallErrorCodeStorageKeyNotExist))
167    /// or an empty retro-causal queue
168    /// ([`kSyscallErrorCodeNoRetrocausalQueueEntry`](SyscallErrorCode::kSyscallErrorCodeNoRetrocausalQueueEntry)).
169    ///
170    /// This is the "graceful" arm of the storage `try_` getters: a `try_get`
171    /// / `try_consume` `Err` for which this returns `false` means the read
172    /// failed for a reason other than absence (wrong storage type at the
173    /// key, a corrupt encoding, a host fault).
174    pub fn is_storage_missing(&self) -> bool {
175        match self.as_syscall_exn() {
176            Some(se) => {
177                se.has_code(
178                    SyscallErrorCode::kSyscallErrorCodeStorageKeyNotExist,
179                ) || se.has_code(
180                    SyscallErrorCode::kSyscallErrorCodeNoRetrocausalQueueEntry,
181                )
182            }
183            None => false,
184        }
185    }
186}
187
188/// Recover the pre-`Result` storage-getter semantics from a `try_` read:
189/// "absent is an ordinary outcome, anything else is not my problem".
190pub trait NoneIfMissing<T> {
191    /// `Ok(v)` → `Some(v)`; an [`is_storage_missing`](LibraryExn::is_storage_missing)
192    /// error → `None`; any other error re-throws (aborts, typed).
193    fn none_if_missing(self) -> Option<T>;
194}
195
196impl<T> NoneIfMissing<T> for Result<T, LibraryExn> {
197    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
198    fn none_if_missing(self) -> Option<T> {
199        use crate::re_throw::ReThrow;
200        match self {
201            Ok(v) => Some(v),
202            Err(e) if e.is_storage_missing() => None,
203            Err(e) => e.re_throw(),
204        }
205    }
206}
207
208impl crate::__contract_spec::ContractTypeName for LibraryExn {
209    const NAME: crate::__contract_spec::ContractSpecIdView<'static> =
210        crate::__contract_spec::ContractSpecIdView {
211            crate_name: ::core::env!("CARGO_PKG_NAME"),
212            major: crate::exn::EXN_ABI_MAJOR,
213            module_path: ::core::module_path!(),
214            declared: "LibraryExn",
215        };
216}
217
218#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
219pub fn library_throw<E: Into<LibraryExn> + core::fmt::Debug>(e: E) -> ! {
220    super::abort_encode::library_abort(&e.into())
221}
222
223#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
224pub fn client_library_throw<E: Into<LibraryExn> + core::fmt::Debug>(
225    e: E,
226) -> ! {
227    super::abort_encode::client_library_abort(&e.into())
228}
229
230/// Throw an exception because a syscall produced an error.
231///
232#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
233pub fn syscall_throw(callno: SyscallNo, errno: SysErr) -> ! {
234    library_throw(syscall_exn(callno, errno))
235}
236
237pub fn syscall_exn(callno: SyscallNo, errno: SysErr) -> LibraryExn {
238    LibraryExn::Syscall(SyscallExn::new(callno, errno))
239}
240
241#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
242pub fn nnint_overflow() -> ! {
243    library_throw(LibraryExn::NonNegativeIntOverflow)
244}
245
246#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
247pub fn nnint_underflow() -> ! {
248    library_throw(LibraryExn::NonNegativeIntUnderflow)
249}
250
251#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
252pub fn decoding_args_throw(e: &CallFault) -> ! {
253    library_throw(HandlingError::DecodingArgs(FaultString::from_display(e)))
254}
255
256#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
257pub fn encoding_success_throw<E: Error>(e: E) -> ! {
258    library_throw(HandlingError::EncodingSuccess(FaultString::from_display(e)))
259}
260
261#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
262pub fn encoding_exception_throw<E: Error>(e: E) -> ! {
263    library_throw(HandlingError::EncodingException(FaultString::from_display(
264        e,
265    )))
266}
267
268#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
269pub fn encoding_event_throw<E: Display>(e: E) -> ! {
270    library_throw(HandlingError::EncodingEvent(FaultString::from_display(e)))
271}
272
273#[derive(Debug, Clone, Copy, ReadXDR, WriteXDR, PartialEq, ConstXDRSize)]
274#[cfg_attr(not(target_arch = "riscv32"), derive(Error))]
275#[derive(ContractTypeSpec)]
276pub struct SyscallExn {
277    callno: SyscallNo,
278    errno: SysErr,
279}
280
281impl SyscallExn {
282    pub fn new(callno: SyscallNo, errno: SysErr) -> Self {
283        Self { callno, errno }
284    }
285
286    pub fn callno(&self) -> SyscallNo {
287        self.callno
288    }
289
290    pub fn has_code(&self, err_code: SyscallErrorCode) -> bool {
291        self.errno.is_code(err_code)
292    }
293
294    pub fn is(&self, call_no: SyscallNo, err_code: SyscallErrorCode) -> bool {
295        self.callno == call_no && self.errno == err_code.into()
296    }
297}
298
299impl Display for SyscallExn {
300    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
301        write!(f, "Syscall {:?} failed with {:?}", self.callno, self.errno)
302    }
303}
304
305impl crate::__contract_spec::ContractTypeName for SyscallExn {
306    const NAME: crate::__contract_spec::ContractSpecIdView<'static> =
307        crate::__contract_spec::ContractSpecIdView {
308            crate_name: ::core::env!("CARGO_PKG_NAME"),
309            major: crate::exn::EXN_ABI_MAJOR,
310            module_path: ::core::module_path!(),
311            declared: "SyscallExn",
312        };
313}
314
315#[cfg(test)]
316mod selector_tests {
317    use super::*;
318    use crate::exn::Exn;
319
320    #[test]
321    fn library_exn_selector_matches_sha256() {
322        // Spelled out, not `env!("CARGO_PKG_NAME")`: the point is that the
323        // selector stays put when the package is renamed.
324        let expected = thruput_contract_util::exception_selector(
325            "1.0.0",
326            "sdk",
327            "LibraryExn",
328        );
329        assert_eq!(LibraryExn::SELECTOR, expected);
330    }
331
332    #[test]
333    fn library_exn_selector_nonzero() {
334        assert_ne!(LibraryExn::SELECTOR, 0);
335    }
336}