Skip to main content

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}