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}