ab_system_contract_simple_wallet_base/payload/caller.rs
1//! Calling methods of a payload one by one
2
3#[cfg(test)]
4mod tests;
5
6use crate::payload::decoder::{DecodedInput, DecodedOutput, DecodedSlot};
7use crate::payload::decoder_core::{
8 MethodSink, OutputAllocator, PayloadDecoderCore, check_slot_output_size,
9};
10use crate::payload::{
11 DeclaredOutput, FfiDataSizeCapacityRo, FfiDataSizeCapacityRw, TransactionMethodContext,
12 TransactionOutputKind, TransactionPayloadDecoderError,
13};
14use crate::{EXTERNAL_ARGS_BUFFER_SIZE, MAX_OUTPUTS};
15use ab_contracts_common::env::{Env, MethodContext, PreparedMethod};
16use ab_contracts_common::{ContractError, MAX_TOTAL_METHOD_ARGS};
17use ab_core_primitives::address::Address;
18use ab_io_type::alignment::Alignment;
19use core::ffi::c_void;
20use core::marker::PhantomData;
21use core::mem::{MaybeUninit, offset_of};
22use core::ptr::NonNull;
23
24const {
25 // Offsets into `external_args_buffer` fit into `PendingOutputSize`
26 assert!(size_of::<[*mut c_void; EXTERNAL_ARGS_BUFFER_SIZE]>() <= usize::from(u8::MAX));
27 // Outputs are allocated at offsets that are multiples of `Alignment::MAX`, so their memory is
28 // aligned to `Alignment::MAX` when the output buffer is, which makes it aligned for every
29 // output type
30 assert!(align_of::<OutputChunk>() >= usize::from(Alignment::MAX.get()));
31}
32
33/// Element of the output buffer of [`TransactionPayloadCaller`]: 16 bytes aligned to
34/// [`Alignment::MAX`]
35#[derive(Debug, Copy, Clone)]
36#[repr(C, align(16))]
37pub struct OutputChunk([u8; 16]);
38
39/// Calls prepared methods for [`TransactionPayloadCaller`]: [`Env`], or something that stands in
40/// for the host in tests.
41///
42/// # Safety
43/// When a call returns `Ok(())`, the method's outputs and return value must have been written the
44/// way `ExecutorContext` requires, as a successful [`Env::call_prepared()`] does (with the `guest`
45/// feature, the host is trusted to write them the same way).
46pub(crate) unsafe trait CallPrepared {
47 /// Call `method`
48 fn call_prepared(&self, method: PreparedMethod<'_>) -> Result<(), ContractError>;
49}
50
51// SAFETY: Executors write outputs and return values of methods that returned `Ok(())` the way
52// `ExecutorContext` requires, and with the `guest` feature the host is trusted to do the same
53unsafe impl CallPrepared for Env<'_> {
54 #[inline(always)]
55 // TODO: `no_panic` can't prove lack of panics in methods of the executor called through `Env`
56 // #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
57 fn call_prepared(&self, method: PreparedMethod<'_>) -> Result<(), ContractError> {
58 Env::call_prepared(self, method)
59 }
60}
61
62/// Allocates the memory of declared outputs one after another in an output buffer
63#[derive(Debug)]
64struct OutputBufferAllocator {
65 /// Size of the output buffer in bytes, at most `u32::MAX`, so that offsets fit into
66 /// [`DeclaredOutput`]
67 size: usize,
68 /// Number of bytes before the end of the last output
69 used: usize,
70}
71
72impl OutputAllocator for OutputBufferAllocator {
73 /// Allocate memory of `capacity` bytes after all memory allocated before, at the next multiple
74 /// of [`Alignment::MAX`].
75 ///
76 /// [`Alignment::MAX`] is the largest alignment of any type that methods accept, so the memory
77 /// is aligned for every output type.
78 #[inline(always)]
79 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
80 fn alloc(&mut self, capacity: u32) -> Result<u32, TransactionPayloadDecoderError> {
81 let offset = self
82 .used
83 .checked_next_multiple_of(usize::from(Alignment::MAX.get()))
84 .ok_or(TransactionPayloadDecoderError::OutputBufferTooSmall)?;
85 let used = offset
86 .checked_add(capacity as usize)
87 .ok_or(TransactionPayloadDecoderError::OutputBufferTooSmall)?;
88
89 if used > self.size {
90 return Err(TransactionPayloadDecoderError::OutputBufferTooSmall);
91 }
92 self.used = used;
93
94 // Doesn't truncate, `offset` is at most `self.size`, which is at most `u32::MAX`
95 Ok(offset as u32)
96 }
97}
98
99/// Size of an `#[output]` in `external_args_buffer`, which is read back after its method returned
100/// `Ok(())`
101#[derive(Debug, Copy, Clone)]
102struct PendingOutputSize {
103 output_index: u8,
104 /// Offset of the size in bytes from the start of `external_args_buffer`
105 external_args_offset: u8,
106}
107
108/// Sizes of the `#[output]`s of a method, with room for one per argument
109type PendingOutputSizes = [MaybeUninit<PendingOutputSize>; MAX_TOTAL_METHOD_ARGS as usize];
110
111/// Write to an external arguments pointer and move it forward.
112///
113/// # Safety
114/// `external_args` must have enough capacity for the written value, and the current offset must
115/// have the correct alignment for the type being written.
116#[inline(always)]
117#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
118unsafe fn write_external_args<T>(external_args: &mut NonNull<c_void>, value: T) {
119 // SAFETY: guaranteed by this function signature
120 unsafe {
121 external_args.cast::<T>().write(value);
122 *external_args = external_args.byte_add(size_of::<T>());
123 }
124}
125
126/// Writes `ExternalArgs` of a method into `external_args_buffer` as its arguments are decoded
127#[derive(Debug)]
128struct ExternalArgsWriter<'pending> {
129 /// Start of `external_args_buffer`
130 external_args: NonNull<c_void>,
131 /// Where the next argument is written to
132 external_args_cursor: NonNull<c_void>,
133 /// Start of the output buffer, see [`TransactionPayloadCaller`]
134 output_buffer: NonNull<u8>,
135 /// Entries before `num_pending_output_sizes` are initialized.
136 ///
137 /// The array is borrowed, not owned. It is indexed at runtime, so as a field it would make the
138 /// compiler keep the whole writer in memory instead of registers. The state of the caller,
139 /// which the writer borrows, would then stay in memory too, and every argument would cost
140 /// extra loads and stores.
141 pending_output_sizes: &'pending mut PendingOutputSizes,
142 /// Never exceeds the length of `pending_output_sizes`
143 num_pending_output_sizes: usize,
144}
145
146impl<'payload> MethodSink<'payload> for ExternalArgsWriter<'_> {
147 #[inline(always)]
148 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
149 fn slot(&mut self, slot: DecodedSlot<'payload>) -> Result<(), TransactionPayloadDecoderError> {
150 let address = match slot {
151 DecodedSlot::Address(address) => NonNull::from_ref(address),
152 DecodedSlot::Output(output) => {
153 // Methods that wrote outputs were called already, so the size of an `#[output]` is
154 // known. A return value, whose size the core checked already, is checked again, so
155 // that this is a single comparison.
156 check_slot_output_size(&output)?;
157
158 // SAFETY: Declared outputs are allocated within the output buffer in
159 // `TransactionPayloadCaller::new()` at multiples of `Alignment::MAX`, the output
160 // buffer is aligned to `Alignment::MAX` by its element type, and an address, like
161 // every `TrivialType`, has at most that alignment
162 let address = unsafe { self.output_pointer(output.offset) }.cast::<Address>();
163 debug_assert!(address.is_aligned());
164
165 address
166 }
167 };
168
169 // SAFETY: The core gives at most `MAX_TOTAL_METHOD_ARGS` arguments per method to the sink,
170 // which `external_args_buffer` is sized for, and the address is aligned for a pointer
171 unsafe {
172 write_external_args(
173 &mut self.external_args_cursor,
174 address.as_ptr().cast_const(),
175 );
176 }
177
178 Ok(())
179 }
180
181 #[inline(always)]
182 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
183 fn input(&mut self, input: DecodedInput<'payload>) {
184 let (data_ptr, size) = match input {
185 DecodedInput::Value {
186 bytes,
187 alignment: _,
188 } => (
189 NonNull::from_ref(bytes).as_non_null_ptr(),
190 // Doesn't truncate, the size was read from the payload as `u32`
191 bytes.len() as u32,
192 ),
193 DecodedInput::Output(output) => (
194 // SAFETY: Declared outputs are allocated within the output buffer in
195 // `TransactionPayloadCaller::new()`
196 unsafe { self.output_pointer(output.offset) },
197 output.size,
198 ),
199 };
200
201 // SAFETY: The core gives at most `MAX_TOTAL_METHOD_ARGS` arguments per method to the sink,
202 // which `external_args_buffer` is sized for, and the buffer is correctly aligned
203 unsafe {
204 write_external_args(
205 &mut self.external_args_cursor,
206 FfiDataSizeCapacityRo {
207 data_ptr,
208 size,
209 capacity: size,
210 },
211 );
212 }
213 }
214
215 #[inline(always)]
216 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
217 fn output(&mut self, output: DecodedOutput, offset: u32) {
218 // The size of a return value is known already. There is always room for the size, since
219 // the core gives at most `MAX_TOTAL_METHOD_ARGS` arguments per method to the sink.
220 if matches!(output.kind, TransactionOutputKind::Output)
221 && let Some(pending_output_size) = self
222 .pending_output_sizes
223 .get_mut(self.num_pending_output_sizes)
224 {
225 // SAFETY: `external_args_cursor` is created from `external_args` and is within the
226 // same allocation
227 let external_args_offset = unsafe {
228 self.external_args_cursor
229 .byte_add(offset_of!(FfiDataSizeCapacityRw, size))
230 .byte_offset_from_unsigned(self.external_args)
231 };
232 pending_output_size.write(PendingOutputSize {
233 output_index: output.output_index,
234 // Doesn't truncate, `external_args_buffer` has at most `u8::MAX` bytes
235 external_args_offset: external_args_offset as u8,
236 });
237 self.num_pending_output_sizes += 1;
238 }
239
240 // SAFETY: Declared outputs are allocated within the output buffer in
241 // `TransactionPayloadCaller::new()`, which is where `offset` comes from
242 let data_ptr = unsafe { self.output_pointer(offset) }.as_ptr();
243 // SAFETY: The core gives at most `MAX_TOTAL_METHOD_ARGS` arguments per method to the sink,
244 // which `external_args_buffer` is sized for, and the buffer is correctly aligned
245 unsafe {
246 write_external_args(
247 &mut self.external_args_cursor,
248 FfiDataSizeCapacityRw {
249 data_ptr,
250 size: output.size,
251 capacity: output.capacity,
252 },
253 );
254 }
255 }
256}
257
258impl<'pending> ExternalArgsWriter<'pending> {
259 #[inline(always)]
260 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
261 fn new(
262 external_args_buffer: &mut [*mut c_void; EXTERNAL_ARGS_BUFFER_SIZE],
263 output_buffer: NonNull<u8>,
264 pending_output_sizes: &'pending mut PendingOutputSizes,
265 ) -> Self {
266 let external_args = NonNull::from_mut(external_args_buffer).cast::<c_void>();
267
268 Self {
269 external_args,
270 external_args_cursor: external_args,
271 output_buffer,
272 pending_output_sizes,
273 num_pending_output_sizes: 0,
274 }
275 }
276
277 /// Pointer to the memory at `offset` in the output buffer.
278 ///
279 /// # Safety
280 /// `offset` must not exceed the size of the output buffer, which is true for offsets of
281 /// declared outputs.
282 #[inline(always)]
283 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
284 unsafe fn output_pointer(&self, offset: u32) -> NonNull<u8> {
285 // SAFETY: Guaranteed by the function contract
286 unsafe { self.output_buffer.byte_add(offset as usize) }
287 }
288
289 /// Sizes of `#[output]` arguments of the method
290 #[inline(always)]
291 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
292 fn pending_output_sizes(&self) -> &[PendingOutputSize] {
293 // SAFETY: Entries before `num_pending_output_sizes` are initialized, and it never exceeds
294 // the length of `pending_output_sizes`
295 unsafe {
296 self.pending_output_sizes
297 .get_unchecked(..self.num_pending_output_sizes)
298 .assume_init_ref()
299 }
300 }
301}
302
303/// Errors for [`TransactionPayloadCaller::call_next_method()`]
304#[derive(Debug, thiserror::Error)]
305pub enum TransactionPayloadCallError {
306 /// Decoder error
307 #[error("Decoder error: {0}")]
308 Decoder(TransactionPayloadDecoderError),
309 /// Method call error
310 #[error("Method call error: {0}")]
311 Method(ContractError),
312}
313
314/// Caller of methods in a transaction payload created using `TransactionPayloadBuilder`.
315///
316/// The payload must be trusted (see [`SimpleWalletBase::execute()`]): what it declares about the
317/// arguments of each method can't be checked without the method's metadata. The size of a return
318/// value must be the size of the return type, and the capacity of a declared output must be enough
319/// for the type of every `#[output]` that writes it.
320/// [`TransactionPayloadDecoder`] applies the same rules without calling methods, so a payload can
321/// be checked and shown to the user before it is signed.
322///
323/// All outputs that the payload declares are allocated in the output buffer when the caller is
324/// created, so a payload whose outputs don't fit is rejected before any method is called.
325///
326/// Outputs are given to methods empty, with size `0`, whatever was written into their memory
327/// before, so methods called from transactions must accept empty outputs, see
328/// `TransactionPayloadBuilder::with_method_call()`.
329///
330/// Return values are given their size when a method is decoded, before it is called, since
331/// executors only write the value (see [`TransactionOutput`]). Each method is called right after
332/// it is decoded and calling stops at the first method that fails, so later methods can only refer
333/// to return values of methods that succeeded and wrote them.
334///
335/// [`SimpleWalletBase::execute()`]: crate::SimpleWalletBase::execute
336/// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
337/// [`TransactionOutput`]: crate::payload::TransactionOutput
338#[derive(Debug)]
339pub struct TransactionPayloadCaller<'a> {
340 core: PayloadDecoderCore<'a, 'a>,
341 external_args_buffer: &'a mut [*mut c_void; EXTERNAL_ARGS_BUFFER_SIZE],
342 /// Start of the output buffer. Every pointer to an output is derived from it, so using one of
343 /// them doesn't invalidate the others.
344 output_buffer: NonNull<u8>,
345 output_buffer_lifetime: PhantomData<&'a mut [MaybeUninit<OutputChunk>]>,
346 map_context: fn(TransactionMethodContext) -> MethodContext,
347}
348
349impl<'a> TransactionPayloadCaller<'a> {
350 /// Create a new instance, read the outputs that the payload declares, at most
351 /// [`MAX_OUTPUTS`], into `declared_outputs` and allocate them in `output_buffer`.
352 ///
353 /// The size of `external_args_buffer` defines the max number of bytes allocated for
354 /// `ExternalArgs`, which impacts the number of arguments that can be represented by
355 /// `ExternalArgs`. The size is specified in pointers with `#[slot]` argument using one
356 /// pointer, while `#[input]` and `#[output]` use one pointer and size + capacity each.
357 ///
358 /// The size of `output_buffer` defines how large the outputs that the payload declares can be
359 /// together. Each of them starts at the next multiple of [`Alignment::MAX`] after the previous
360 /// one.
361 ///
362 /// Returns an error if the payload is not aligned to [`Alignment::MAX`], or if the outputs that
363 /// the payload declares can't be read or don't fit, so that no method of such a payload is
364 /// called. Every payload starts with the outputs that it declares, so an empty payload is too
365 /// small.
366 #[inline]
367 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
368 pub fn new(
369 payload: &'a [u128],
370 external_args_buffer: &'a mut [*mut c_void; EXTERNAL_ARGS_BUFFER_SIZE],
371 output_buffer: &'a mut [MaybeUninit<OutputChunk>],
372 declared_outputs: &'a mut [MaybeUninit<DeclaredOutput>; MAX_OUTPUTS],
373 map_context: fn(TransactionMethodContext) -> MethodContext,
374 ) -> Result<Self, TransactionPayloadDecoderError> {
375 let mut allocator = OutputBufferAllocator {
376 size: size_of_val(output_buffer).min(u32::MAX as usize),
377 used: 0,
378 };
379
380 Ok(Self {
381 core: PayloadDecoderCore::new(payload, declared_outputs, &mut allocator)?,
382 external_args_buffer,
383 output_buffer: NonNull::from_mut(output_buffer).cast::<u8>(),
384 output_buffer_lifetime: PhantomData,
385 map_context,
386 })
387 }
388
389 /// Decode the next method (if any) in the payload and call it.
390 ///
391 /// Returns the caller for the remaining methods if the method succeeded, or `None` if there
392 /// are no more methods in the payload. The caller is consumed when decoding or the method
393 /// fails, since later methods might refer to return values that the failed method didn't
394 /// write.
395 // TODO: `no_panic` can't prove lack of panics in methods of the executor called through `Env`
396 // #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
397 pub fn call_next_method(
398 self,
399 env: &Env<'_>,
400 ) -> Result<Option<Self>, TransactionPayloadCallError> {
401 self.call_next_method_with(env)
402 }
403
404 /// The same as [`Self::call_next_method()`], but with `env` that can stand in for [`Env`]
405 // Not annotated with `no_panic`, since tests call it with stand-ins for `Env` that panic
406 #[inline(always)]
407 pub(crate) fn call_next_method_with<Environment>(
408 mut self,
409 env: &Environment,
410 ) -> Result<Option<Self>, TransactionPayloadCallError>
411 where
412 Environment: CallPrepared,
413 {
414 // Outside the writer, see `ExternalArgsWriter::pending_output_sizes`
415 let mut pending_output_sizes: PendingOutputSizes =
416 [MaybeUninit::uninit(); MAX_TOTAL_METHOD_ARGS as usize];
417 let mut writer = ExternalArgsWriter::new(
418 &mut *self.external_args_buffer,
419 self.output_buffer,
420 &mut pending_output_sizes,
421 );
422 let maybe_method_header = self.core.decode_next_method(&mut writer);
423 let Some(method_header) =
424 maybe_method_header.map_err(TransactionPayloadCallError::Decoder)?
425 else {
426 return Ok(None);
427 };
428
429 // SAFETY: Bounds and alignment are checked by the core and the writer, and for as long as
430 // the returned value borrows the caller:
431 // * `external_args_buffer` starts with arguments written by the writer in the order and
432 // layout of `ExternalArgs` for the slots, inputs and outputs the payload declares: a
433 // pointer for each slot, then pointer, size and capacity for each input and output, where
434 // a return value is written like the last output, with size and capacity after its
435 // pointer that `ExternalArgs` doesn't have
436 // * slot addresses are aligned, and slot addresses and inputs point into the payload or
437 // into declared outputs, up to the size of what an earlier method wrote into them last:
438 // the size that a method wrote into an `#[output]`, which executors keep within the
439 // capacity with that many bytes initialized, as `ExecutorContext` requires, or the size
440 // that the payload declares for a return value, which is the size of the return type, has
441 // no padding (`TrivialType`) and doesn't exceed the capacity (the core checks it): every
442 // earlier method returned `Ok(())`, since the caller is consumed otherwise, so it wrote
443 // its outputs and return value as `CallPrepared` requires, sizes of `#[output]`s are only
444 // stored after that, and slots and inputs only refer to declared outputs that earlier
445 // methods wrote (see `MethodSink`)
446 // * a slot only refers to a declared output whose size is that of an address, which the
447 // writer checks for every slot that refers to a declared output
448 // * an `#[output]` is given size `0` whatever its memory held before, and a return value
449 // replaces the size of what its memory held before, so later methods only read bytes that
450 // the last write initialized
451 // * declared outputs are allocated one after another with their capacities in
452 // `Self::new()`, so they don't overlap each other (one with capacity `0` has no memory
453 // that could overlap), while the payload and `external_args_buffer` are borrowed
454 // separately from the output buffer
455 // * the outputs of a method are different declared outputs, and its slots and inputs don't
456 // refer to declared outputs that it writes (see `MethodSink`), so the memory that a
457 // method writes doesn't overlap with any other argument, while slots and inputs may share
458 // a declared output, which they only read
459 // * the payload is trusted to declare the size of the return type as the size of the return
460 // value, and the core checked that it doesn't exceed the capacity of its declared output,
461 // so executors write the return value within that declared output, while otherwise they
462 // could write into other declared outputs, including ones that the same method reads
463 // * declared outputs are allocated at multiples of `Alignment::MAX` in the output buffer,
464 // which is aligned to `Alignment::MAX` too, and no type that methods accept has a larger
465 // alignment, so the memory of every output is aligned for its type
466 // * pointers into the output buffer are all derived from one pointer to the whole buffer,
467 // so creating one doesn't invalidate those created before
468 // * the payload, `external_args_buffer` and the output buffer stay borrowed by the caller,
469 // so nothing else accesses them, and the payload and outputs of earlier methods are only
470 // read
471 // * contents of outputs and return values are only given to later methods as slot
472 // addresses, which accept every bit pattern, and as inputs, which methods parse
473 // * the payload is trusted to declare the arguments of the method with its fingerprint, and
474 // to declare as the return value only one that executors write (not the state returned by
475 // `#[init]`, which they store instead), which can't be checked without the method's
476 // metadata, the same way `NativeExecutor` trusts transaction payloads (see
477 // `SimpleWalletBase::execute()` for what makes them trusted)
478 let prepared_method = unsafe {
479 PreparedMethod::new_untyped(
480 *method_header.contract,
481 *method_header.fingerprint,
482 &mut *self.external_args_buffer,
483 (self.map_context)(method_header.context),
484 )
485 };
486
487 env.call_prepared(prepared_method)
488 .map_err(TransactionPayloadCallError::Method)?;
489
490 for pending_output_size in writer.pending_output_sizes() {
491 // SAFETY: The offset points to the size of an `#[output]` within
492 // `external_args_buffer`, which is aligned for `u32`. It is read through a new pointer,
493 // since the one the writer has was created before the buffer was borrowed for the
494 // call.
495 let size = unsafe {
496 NonNull::from_ref(&*self.external_args_buffer)
497 .cast::<u8>()
498 .byte_add(usize::from(pending_output_size.external_args_offset))
499 .cast::<u32>()
500 .read()
501 };
502 // SAFETY: The writer records only outputs that the core gave to it while decoding
503 // this method, which succeeded, so the output exists. The method returned `Ok(())`, so
504 // it wrote a size within the capacity of the output with that many bytes initialized,
505 // as `CallPrepared` requires.
506 unsafe {
507 self.core
508 .set_output_size(pending_output_size.output_index, size);
509 }
510 }
511
512 Ok(Some(self))
513 }
514}