thruput_sdk/call/raw_call_error.rs
1//! The raw, untyped error from a contract call.
2
3use crate::exn::{
4 CallingError, FaultInfo, FaultString, RawException,
5 client_library_throw as library_throw, syscall_exn,
6};
7use crate::sys::{SysErr, SyscallNo};
8
9/// The raw, untyped error from a contract call.
10///
11/// Generic over the exception representation `X`, which
12/// [`try_call_raw_with`](super::try_call_raw_with)'s `on_exn` closure chooses:
13/// the default [`RawException`] (one copy, via `to_owned`), a typed error
14/// enum, or `Infallible` if the closure re-throws unconditionally.
15///
16/// The variants divide by how far the call got.
17/// [`Invalid`](Self::Invalid), [`Reentrance`](Self::Reentrance),
18/// [`DepthExceeded`](Self::DepthExceeded) and
19/// [`InvokeRejected`](Self::InvokeRejected) all mean the invoke syscall was
20/// refused before any child frame ran, so the call was a complete no-op.
21/// [`Exception`](Self::Exception) and [`Fault`](Self::Fault) mean a child
22/// frame ran and then aborted or died. The rest are encode/decode failures
23/// in this contract, on either side of a call that otherwise completed.
24#[derive(Debug, PartialEq)]
25pub enum RawCallError<X = RawException> {
26 /// The invoke syscall rejected the call as malformed: no contract at the
27 /// target address, invalid invoke flags, or an auth token id that is
28 /// malformed, duplicated, or not owned by this frame.
29 Invalid,
30
31 /// The target address already has a frame open on the invocation stack,
32 /// so the runtime refused the call as reentrant.
33 ///
34 /// The call can never succeed anywhere in this call chain — unlike
35 /// [`DepthExceeded`](Self::DepthExceeded), retrying from a shallower
36 /// frame does not help while this address is still on the stack.
37 Reentrance,
38
39 /// The transaction already holds the network's maximum number of open
40 /// invocation frames, so admitting another would exceed the cross-invoke
41 /// depth bound.
42 ///
43 /// A resource limit rather than a malformed call: the same invocation
44 /// succeeds when made from a shallower frame.
45 DepthExceeded,
46
47 /// The invoke syscall was refused with a code this SDK does not model.
48 ///
49 /// Carries the raw code rather than collapsing an unrecognized rejection
50 /// into a neighbouring variant's meaning.
51 InvokeRejected(SysErr),
52
53 /// The called contract threw an exception with a selector-tagged error.
54 Exception(X),
55
56 /// A child frame ran and terminated without producing a result: a panic,
57 /// a trap, or exhausting its gas.
58 ///
59 /// The message is *host-side state recorded beside the call*, never
60 /// something the callee sent: the runtime reports a faulted child as a
61 /// bare frame status with neither a return nor an abort buffer, so no
62 /// message crosses the call boundary and none ever will. That splits the
63 /// two cases:
64 ///
65 /// - **A frame the native harness ran itself** — the root frame of an
66 /// `env.transact`, reaching here through `into_root_call_result`. The
67 /// harness caught that panic, so the message is the panicking body's
68 /// own. A frame that died with no ordinary panic to catch leaves it
69 /// empty: running out of gas unwinds an `InvokeError`, which carries
70 /// no message.
71 /// - **A `raw_Invoke`ed callee** — caught inside the runtime's own
72 /// frame, where there is nothing to attribute a message against, so
73 /// the payload is [`FaultInfo::empty`] and `as_str()` reads
74 /// `"<fault>"`. Reporting anything else here would mean reporting some
75 /// other frame's message.
76 ///
77 /// See [`FaultInfo`] for what the payload holds and where.
78 ///
79 /// A callee that wants its failure to be legible to its caller — on
80 /// chain, not just in a host test — must abort with an exception rather
81 /// than fault.
82 Fault(FaultInfo),
83
84 /// Decode error: the called contract returned some unparseable return result
85 UnparseableReturn(FaultString),
86 /// Decode error: the called contract aborted with some unparseable result
87 UnparseableAbort(FaultString),
88 /// Decode error: the called contract aborted with some unparseable result
89 UnparseableAuthHandles(FaultString),
90
91 /// Encode error: the arguments supplied failed to encode
92 EncodingArgs(FaultString),
93}
94
95impl<X> RawCallError<X> {
96 /// Whether the invoke syscall was refused before any child frame ran.
97 ///
98 /// True for [`Invalid`](Self::Invalid), [`Reentrance`](Self::Reentrance),
99 /// [`DepthExceeded`](Self::DepthExceeded) and
100 /// [`InvokeRejected`](Self::InvokeRejected). When this holds the call was
101 /// a complete no-op: the callee did not run, observed nothing, and wrote
102 /// nothing.
103 pub fn is_rejected_before_call(&self) -> bool {
104 matches!(
105 self,
106 RawCallError::Invalid
107 | RawCallError::Reentrance
108 | RawCallError::DepthExceeded
109 | RawCallError::InvokeRejected(_)
110 )
111 }
112}
113
114/// Classify the `Err` from the Invoke syscall, which the runtime returns only
115/// when it refused the call before running a child frame.
116///
117/// The codes are those `Invoke` documents in the syscall spec; anything else
118/// keeps its raw code in [`RawCallError::InvokeRejected`] rather than being
119/// folded into one of the modelled meanings.
120pub(crate) fn classify_invoke_rejection<X>(syserr: SysErr) -> RawCallError<X> {
121 use crate::sys::SyscallErrorCode as Code;
122
123 if syserr.is_invalid_input() {
124 RawCallError::Invalid
125 } else if syserr.is_code(Code::kSyscallErrorCodeReentrance) {
126 RawCallError::Reentrance
127 } else if syserr.is_code(Code::kSyscallErrorCodeInvokeDepthExceeded) {
128 RawCallError::DepthExceeded
129 } else {
130 RawCallError::InvokeRejected(syserr)
131 }
132}
133
134/// The error type of ordinary generated `try_*` client methods: a call failure
135/// that may carry a caught exception, kept untyped.
136///
137/// Exceptions stay as [`RawException`] rather than being pre-decoded into a
138/// per-client enum, because the wire form already identifies the type by
139/// selector, independently of what any interface declared it `throws`. Match
140/// one with [`match_exn!`](crate::match_exn); everything else here is
141/// ordinary `match` material.
142pub type CallError = RawCallError;
143
144/// A call failure with no exception payload: the call was refused before the
145/// target ran (see [`is_rejected_before_call`](RawCallError::is_rejected_before_call)),
146/// the target faulted, or the host's returned
147/// data failed validation. The `Exception` variant is statically
148/// uninhabited ([`Infallible`](core::convert::Infallible)) — on these
149/// paths an abort is not an error; it surfaces as
150/// [`CallOutcome::Abort`](crate::call::CallOutcome::Abort) with the
151/// payload in the caller's buffer. (Crate-internal code widens into a
152/// `RawCallError<X>` with `widen_uninhabited_exn`.)
153pub type CallFault = RawCallError<core::convert::Infallible>;
154
155#[cfg(not(target_arch = "riscv32"))]
156impl<X: core::fmt::Debug> core::fmt::Display for RawCallError<X> {
157 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
158 match self {
159 RawCallError::Invalid => {
160 write!(f, "contract not found (or invalid auth tokens)")
161 }
162 RawCallError::Reentrance => {
163 write!(f, "call refused: target already has an open frame")
164 }
165 RawCallError::DepthExceeded => {
166 write!(f, "call refused: cross-invoke depth bound reached")
167 }
168 RawCallError::InvokeRejected(code) => {
169 write!(f, "call refused: {code:?}")
170 }
171 RawCallError::Exception(exn) => write!(f, "exception: {exn:?}"),
172 RawCallError::Fault(fault) => write!(f, "fault: {fault}"),
173 RawCallError::UnparseableReturn(error) => {
174 write!(f, "unparseable return: {error}")
175 }
176 RawCallError::UnparseableAbort(error) => {
177 write!(f, "unparseable abort: {error}")
178 }
179 RawCallError::UnparseableAuthHandles(error) => {
180 write!(f, "unparseable auth handles: {error}")
181 }
182 RawCallError::EncodingArgs(error) => {
183 write!(f, "encoding args: {error}")
184 }
185 }
186 }
187}
188
189impl<X: crate::re_throw::ReThrow> crate::re_throw::ReThrow
190 for RawCallError<X>
191{
192 #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
193 fn re_throw(self) -> ! {
194 match self {
195 RawCallError::Invalid => library_throw(CallingError::Invalid),
196 RawCallError::Reentrance => {
197 library_throw(CallingError::Reentrance)
198 }
199 RawCallError::DepthExceeded => {
200 library_throw(CallingError::DepthExceeded)
201 }
202 // An unmodelled rejection keeps its raw code by riding the
203 // existing syscall exception rather than earning a `CallingError`
204 // variant of its own: there is no meaning to name yet, and the
205 // code is what a caller would want to see.
206 RawCallError::InvokeRejected(code) => {
207 library_throw(syscall_exn(SyscallNo::kInvoke, code))
208 }
209 RawCallError::Exception(exn) => {
210 crate::re_throw::ReThrow::re_throw(exn)
211 }
212 // Renders whatever host-side description was recorded beside
213 // the call, or `"<fault>"` when none was. A guest has no message
214 // to render and no way to surface one: its `#[panic_handler]`
215 // ignores `PanicInfo` and issues `ebreak`.
216 #[cfg(not(target_arch = "riscv32"))]
217 RawCallError::Fault(f) => panic!("{f}"),
218 #[cfg(target_arch = "riscv32")]
219 RawCallError::Fault(_) => panic!(),
220 RawCallError::UnparseableReturn(error) => {
221 library_throw(CallingError::DecodingSuccess(error))
222 }
223 RawCallError::UnparseableAbort(error) => {
224 library_throw(CallingError::DecodingException(error))
225 }
226 RawCallError::UnparseableAuthHandles(error) => {
227 library_throw(CallingError::DecodingSuccess(error))
228 }
229 RawCallError::EncodingArgs(error) => {
230 library_throw(CallingError::EncodingArgs(error))
231 }
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::re_throw::OrReThrow;
240
241 #[test]
242 fn or_re_throw_ok_returns_value() {
243 let result: Result<i32, RawCallError> = Ok(42);
244 assert_eq!(result.or_re_throw(), 42);
245 }
246
247 /// Every rejection `Invoke` documents keeps its own identity. Reentrance
248 /// and the depth bound used to land in `Fault`, which told the caller its
249 /// callee had misbehaved when in fact the callee never ran — and on rv32,
250 /// where the old `Fault` payload compiled out, nothing distinguished them
251 /// at all.
252 #[test]
253 fn documented_rejections_map_to_their_own_variants() {
254 use crate::sys::SyscallErrorCode as Code;
255
256 let cases = [
257 (
258 Code::kSyscallErrorCodeInvalidSyscallInput,
259 RawCallError::Invalid,
260 ),
261 (Code::kSyscallErrorCodeReentrance, RawCallError::Reentrance),
262 (
263 Code::kSyscallErrorCodeInvokeDepthExceeded,
264 RawCallError::DepthExceeded,
265 ),
266 ];
267
268 for (code, expected) in cases {
269 let got: RawCallError = classify_invoke_rejection(code.into());
270 assert_eq!(got, expected, "rejection code {code:?}");
271 }
272 }
273
274 /// A rejection this SDK does not model keeps its raw code instead of
275 /// being folded into a neighbouring variant's meaning.
276 #[test]
277 fn unmodelled_rejection_keeps_its_code() {
278 let unknown = SysErr(0x7fff_0000);
279 let got: RawCallError = classify_invoke_rejection(unknown);
280 assert_eq!(got, RawCallError::InvokeRejected(unknown));
281 }
282
283 /// `Fault` means a child frame ran and died; every rejection means it
284 /// never ran at all. Contracts that must not retry a call whose side
285 /// effects may have landed depend on that split.
286 #[test]
287 fn rejections_are_distinguished_from_a_ran_and_faulted_callee() {
288 let rejected: [RawCallError; 4] = [
289 RawCallError::Invalid,
290 RawCallError::Reentrance,
291 RawCallError::DepthExceeded,
292 RawCallError::InvokeRejected(SysErr(1234)),
293 ];
294 for e in &rejected {
295 assert!(e.is_rejected_before_call(), "{e:?}");
296 assert_ne!(*e, RawCallError::Fault(FaultInfo::empty()));
297 }
298
299 let ran: RawCallError = RawCallError::Fault(FaultInfo::empty());
300 assert!(!ran.is_rejected_before_call());
301 }
302}