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