thruput_sdk/contract_op.rs
1//! System functions
2//!
3//! The functions are used by the [`contract`](macro@crate::contract) macro,
4//! and are also available for lower-level code.
5
6use crate::re_throw::OrSyscallThrow;
7use crate::sys::{self, SysResult, SyscallNo};
8use crate::types::{ContractAddress, RuntimeMemoryLength, SHA256Hash};
9use crate::xdr::*;
10use crate::{FaultString, HandlingError};
11use core::mem::MaybeUninit;
12
13pub fn contract_abort(buf: &[u8]) -> ! {
14 let revert = true;
15
16 sys::raw_Return(buf, revert, &[])
17}
18
19/// Terminate the current call frame immediately, reverting it.
20///
21/// The caller observes a deterministic error with no return buffer, the
22/// same outcome as an illegal instruction trap. To abort with a serialized
23/// exception payload instead, use [`contract_abort`].
24pub fn contract_revert() -> ! {
25 sys::raw_Exit()
26}
27
28/// Print a debugging breadcrumb before an abort/re-throw: the verb (e.g.
29/// "Throwing"/"Re-throwing"), the exception's `Debug` form, the caller's
30/// location (labeled `location_label`, since call sites vary — plain
31/// "Location" vs. "Interface location"), and a backtrace.
32///
33/// `#[track_caller]` here reports whichever site called into *this*
34/// function; every call site above it in the chain also carries
35/// `#[cfg_attr(not(target_arch = "riscv32"), track_caller)]` so that location is
36/// the original user call, not an intermediate SDK frame.
37#[cfg(all(not(target_arch = "riscv32"), feature = "std"))]
38#[track_caller]
39pub(crate) fn abort_breadcrumb(
40 verb: &str,
41 location_label: &str,
42 exn: &dyn core::fmt::Debug,
43) {
44 std::println!();
45 std::println!(" {verb} exception {:?}", exn);
46 std::println!(" {location_label}: {}", core::panic::Location::caller());
47 // Safety: captured and displayed in one expression on an abort path
48 // (every caller of this helper never returns), so every
49 // with_debug_context frame is still on the stack.
50 std::println!(" Backtrace:\n{}", unsafe { crate::Backtrace::capture() });
51}
52
53#[inline]
54#[cfg_attr(not(target_arch = "riscv32"), track_caller)]
55pub fn contract_abort_encoded<T>(exn: &T) -> !
56where
57 T: WriteXDR + ?Sized + core::fmt::Debug,
58{
59 #[cfg(all(not(target_arch = "riscv32"), feature = "std"))]
60 abort_breadcrumb("Throwing", "Location", &exn);
61
62 _contract_abort_encoded(exn)
63}
64
65// Inlining this leads to significant code size bloat.
66#[inline(never)]
67fn _contract_abort_encoded<T>(exn: &T) -> !
68where
69 T: WriteXDR + ?Sized + core::fmt::Debug,
70{
71 crate::call::with_scratch(|buf| {
72 crate::internal::encode_scratch(buf, exn)
73 .unwrap_or_else(|e| exception_encoding_error(e));
74 contract_abort(buf.as_ref());
75 })
76}
77
78// A user exception failed to encode in `_contract_abort_encoded`:
79// report it as a typed library exception instead of trapping. Reachable
80// only from the generic `throw<E>` path — library exceptions go through
81// the hand-rolled `abort_encode` encoder, which is infallible (fixed
82// buffer sized to the worst-case variant) and does not touch the shared
83// scratch, so no second-failure fallback is needed here anymore.
84#[cold]
85#[inline(never)]
86fn exception_encoding_error(err: WriteError) -> ! {
87 use crate::exn::LibraryExn;
88
89 crate::exn::abort_encode::library_abort(&LibraryExn::from(
90 HandlingError::EncodingException(FaultString::via_display(&err)),
91 ))
92}
93
94pub fn get_caller() -> ContractAddress {
95 let mut out_addr: MaybeUninit<ContractAddress> =
96 MaybeUninit::<ContractAddress>::uninit();
97
98 sys::uninit_GetMsgSender(&mut out_addr)
99 .or_syscall_throw(SyscallNo::kGetMsgSender);
100
101 unsafe { out_addr.assume_init() }
102}
103
104/// The hash of the transaction that initiated the current execution.
105///
106/// This is `thruput-xdr`'s `TransactionHash`: sha256 of the XDR-encoded
107/// `Transaction` — the same object the signatures are over, and the
108/// identifier the transaction carries network-wide. It deliberately covers
109/// neither the signatures wrapped around it nor the nondeterministic data a
110/// proposer attaches, so a submitter can compute it at signing time and use
111/// it to find its own transaction.
112pub fn get_source_transaction_hash() -> SHA256Hash {
113 let mut out_hash: MaybeUninit<SHA256Hash> =
114 MaybeUninit::<SHA256Hash>::uninit();
115 sys::uninit_GetSourceTransactionHash(&mut out_hash)
116 .or_syscall_throw(SyscallNo::kGetSourceTransactionHash);
117 unsafe { out_hash.assume_init() }
118}
119
120pub fn get_current_address() -> ContractAddress {
121 let mut out_addr: MaybeUninit<ContractAddress> =
122 MaybeUninit::<ContractAddress>::uninit();
123 sys::uninit_GetCurrentAddress(&mut out_addr)
124 .or_syscall_throw(SyscallNo::kGetCurrentAddress);
125 unsafe { out_addr.assume_init() }
126}
127
128pub fn try_get_calldata_len() -> SysResult<RuntimeMemoryLength> {
129 let mut out: RuntimeMemoryLength = 0;
130 let mut _auth_token_count: u32 = 0;
131 sys::raw_GetCalldataLen(&mut out, &mut _auth_token_count)?;
132 Ok(out)
133}
134
135pub fn get_calldata_len() -> RuntimeMemoryLength {
136 try_get_calldata_len().or_syscall_throw(SyscallNo::kGetCalldataLen)
137}
138
139/// The calldata buffer — the SDK's single
140/// [`ScratchBuf`](crate::internal::ScratchBuf). With the `alloc` feature this
141/// is a heap `Vec` sized exactly to the calldata (any length); without it, a
142/// fixed [`SCRATCH_MAX`](crate::internal::SCRATCH_MAX)-byte buffer.
143///
144/// Distinct from the write scratch: the decoded args borrow from this buffer
145/// (`ReadXDR<'b>`) for the whole frame, so it stays pinned and cannot be
146/// reused for outbound encoding.
147pub type Calldata = crate::internal::ScratchBuf;
148
149/// Fetch the current frame's calldata into the caller-provided buffer
150/// (cleared first), returning the number N of auth tokens supplied with the
151/// invocation. They are the frame's auth token ids 0..N-1.
152///
153/// This is the fallible core of [`fetch_calldata_and_auth_tokens`]: the
154/// buffer is filled in place, never moved by value. It is heap-allocated
155/// with its exact size, queried up front via `GetCalldataLen`, so calldata
156/// of any length is supported.
157#[cfg(feature = "alloc")]
158fn fetch_calldata_and_auth_tokens_into(
159 calldata: &mut Calldata,
160) -> SysResult<u32> {
161 let mut len: RuntimeMemoryLength = 0;
162 let mut _auth_token_count: u32 = 0;
163 sys::raw_GetCalldataLen(&mut len, &mut _auth_token_count)?;
164
165 let len = len as usize;
166
167 calldata.clear();
168 calldata.reserve(len);
169
170 let mut written: MaybeUninit<RuntimeMemoryLength> = MaybeUninit::uninit();
171 let mut auth_token_count: MaybeUninit<u32> = MaybeUninit::uninit();
172
173 sys::uninit_GetCalldata(
174 // `reserve` may over-allocate; pass exactly the span we prove
175 // initialized below.
176 &mut calldata.spare_capacity_mut()[..len],
177 &mut written,
178 &mut auth_token_count,
179 )?;
180
181 // The buffer was sized from `GetCalldataLen` in this same frame, so the
182 // length `GetCalldata` reports must match exactly; a mismatch means the
183 // host broke the syscall contract, and trusting it would let `set_len`
184 // expose uninitialized bytes.
185 let written = unsafe { written.assume_init() } as usize;
186 if written != len {
187 return Err(
188 sys::SyscallErrorCode::kSyscallErrorCodeInvalidSyscallInput.into(),
189 );
190 }
191 unsafe { calldata.set_len(written) };
192
193 Ok(unsafe { auth_token_count.assume_init() })
194}
195
196/// Fetch the current frame's calldata into the caller-provided buffer
197/// (cleared first), returning the number N of auth tokens supplied with the
198/// invocation. They are the frame's auth token ids 0..N-1.
199///
200/// This is the fallible core of [`fetch_calldata_and_auth_tokens`]: the
201/// buffer is filled in place, never moved by value. Without `alloc`,
202/// calldata lands in the fixed
203/// [`SCRATCH_MAX`](crate::internal::SCRATCH_MAX)-byte buffer; larger
204/// calldata returns `InvalidSyscallInput` (thrown as a
205/// `LibraryExn::Syscall` for `kGetCalldata`).
206#[cfg(not(feature = "alloc"))]
207fn fetch_calldata_and_auth_tokens_into(
208 calldata: &mut Calldata,
209) -> SysResult<u32> {
210 calldata.clear();
211
212 let mut written: MaybeUninit<RuntimeMemoryLength> = MaybeUninit::uninit();
213 let mut auth_token_count: MaybeUninit<u32> = MaybeUninit::uninit();
214
215 sys::uninit_GetCalldata(
216 calldata.spare_capacity_mut(),
217 &mut written,
218 &mut auth_token_count,
219 )?;
220
221 // `GetCalldata` copies at most the destination span's length but reports
222 // the *full* source length, which can exceed the fixed buffer here
223 // (protocol calldata is unbounded). Applying an oversized length via
224 // `set_len` would let `as_ref` read past the initialized region, so an
225 // oversized report is surfaced as InvalidSyscallInput: the span this
226 // wrapper supplied was too small for the data.
227 let written = unsafe { written.assume_init() } as usize;
228 if written > calldata.capacity() {
229 return Err(
230 sys::SyscallErrorCode::kSyscallErrorCodeInvalidSyscallInput.into(),
231 );
232 }
233 unsafe { calldata.set_len(written) };
234
235 Ok(unsafe { auth_token_count.assume_init() })
236}
237
238/// Fetch this frame's calldata into the caller's buffer (cleared first),
239/// returning the number N of auth tokens supplied with the invocation —
240/// the frame's auth token ids 0..N-1, in the order the caller listed them —
241/// and throwing a `LibraryExn::Syscall` on fetch failure. The single
242/// calldata API: generated entrypoint wrappers and raw-calldata contracts
243/// both use it. The throw is guest-reachable and not limited to an
244/// adversarial host: in the no-alloc guest it is exactly what an ordinary
245/// transaction gets if its calldata exceeds `SCRATCH_MAX` (4096) bytes.
246///
247/// The buffer lives in the caller's frame and is filled in place — never
248/// moved by value. Generated entrypoint wrappers declare it as a local and
249/// fetch exactly once, at the root of the frame; the decoded args borrow
250/// the calldata buffer for the entire handler frame. In the no-alloc guest
251/// that costs `SCRATCH_MAX` (4096) bytes of stack per live fetch. There is
252/// no shared cell, so nested fetches (the native mock's in-process frames,
253/// or a hand-written re-fetch) each get their own buffer and cannot alias.
254///
255/// Non-generic and `#[inline(never)]`, both load-bearing for codesize:
256/// every entrypoint reaches this single copy of the fetch machinery by
257/// direct call — nothing monomorphizes per entrypoint, and no handler's
258/// address is ever taken. Without the attribute, LTO inlines the body back
259/// into each export, recreating the per-export copies.
260///
261/// `doc(hidden)`: raw calldata access is plumbing for the generated
262/// entrypoint wrappers, not a surface contracts should discover and
263/// build on.
264#[doc(hidden)]
265#[inline(never)]
266pub fn fetch_calldata_and_auth_tokens(calldata: &mut Calldata) -> u32 {
267 fetch_calldata_and_auth_tokens_into(calldata)
268 .or_syscall_throw(SyscallNo::kGetCalldata)
269}