Skip to main content

ab_system_contract_simple_wallet_base/payload/
builder.rs

1//! Transaction payload creation utilities
2
3#[cfg(test)]
4mod tests;
5
6use crate::MAX_OUTPUTS;
7use crate::payload::decoder_core::{output_ref, slot_output_ref, writes_output};
8use crate::payload::{
9    DeclaredOutput, FfiDataSizeCapacityRo, OutputState, TransactionInput, TransactionMethodContext,
10    TransactionOutput, TransactionOutputKind, TransactionPayloadDecoderError, TransactionSlot,
11};
12use ab_aligned_buffer::OwnedAlignedBuffer;
13use ab_contracts_common::MAX_TOTAL_METHOD_ARGS;
14use ab_contracts_common::metadata::decode::{
15    ArgumentKind, MetadataDecodingError, MethodKind, MethodMetadataDecoder, MethodMetadataItem,
16    MethodsContainerKind,
17};
18use ab_contracts_common::method::{ExternalArgs, MethodFingerprint};
19use ab_core_primitives::address::Address;
20use ab_io_type::alignment::Alignment;
21use ab_io_type::metadata::IoTypeDetails;
22use ab_io_type::trivial_type::TrivialType;
23use core::ffi::c_void;
24use core::mem::MaybeUninit;
25use core::ptr::NonNull;
26use core::{hint, slice};
27
28const {
29    // Make sure bit flags for all arguments will fit into a single u8 below
30    assert!(MAX_TOTAL_METHOD_ARGS as u32 == u8::BITS);
31}
32
33/// Read from an external arguments pointer and move it forward.
34///
35/// # Safety
36/// `external_args` must have enough capacity for the read value, and the current offset must
37/// have the correct alignment for the type being read.
38#[inline(always)]
39#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
40unsafe fn read_external_args<T>(external_args: &mut NonNull<c_void>) -> T {
41    // SAFETY: guaranteed by this function signature
42    unsafe {
43        let value = external_args.cast::<T>().read();
44        *external_args = external_args.byte_add(size_of::<T>());
45        value
46    }
47}
48
49/// Errors for [`TransactionPayloadBuilder`]
50#[derive(Debug, thiserror::Error)]
51pub enum TransactionPayloadBuilderError<'a> {
52    /// Metadata decoding error
53    #[error("Metadata decoding error: {0}")]
54    MetadataDecodingError(MetadataDecodingError<'a>),
55    /// Too many arguments
56    #[error("Too many arguments")]
57    TooManyArguments(u8),
58    /// An output index of [`MAX_OUTPUTS`] or more, which tags can't hold
59    #[error("Invalid output index: {0}")]
60    InvalidOutputIndex(u8),
61    /// The payload doesn't fit into the maximum size given to [`TransactionPayloadBuilder::new()`]
62    #[error("The payload is too large")]
63    PayloadTooLarge,
64    /// More than [`MAX_OUTPUTS`] outputs declared
65    #[error("Too many outputs")]
66    TooManyOutputs,
67    /// [`MethodCallReferences::outputs`] doesn't have exactly one entry for each output of the
68    /// method
69    #[error("The method has {expected} outputs, but {actual} output indices are given")]
70    NumberOfOutputsMismatch { expected: u8, actual: usize },
71    /// [`MethodCallReferences::slots`] has more entries than the method has slots
72    #[error("The method has {num_slots} slots, but {num_references} slot references are given")]
73    TooManySlotReferences {
74        num_slots: u8,
75        num_references: usize,
76    },
77    /// [`MethodCallReferences::inputs`] has more entries than the method has inputs
78    #[error("The method has {num_inputs} inputs, but {num_references} input references are given")]
79    TooManyInputReferences {
80        num_inputs: u8,
81        num_references: usize,
82    },
83    /// No method call writes the declared output with this index, see
84    /// [`TransactionPayloadBuilder::into_aligned_bytes()`]
85    #[error("No method call writes the output with output index {0}")]
86    UnusedOutput(u8),
87    /// The method call breaks a rule of the payload format, with the error that
88    /// [`TransactionPayloadDecoder`] returns for such a payload, see
89    /// [`TransactionPayloadBuilder::with_method_call()`]
90    ///
91    /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
92    #[error("The method call breaks a rule of the payload format: {0}")]
93    PayloadRule(TransactionPayloadDecoderError),
94}
95
96/// Declared output that no method call wrote yet, with capacity `0`
97const UNWRITTEN_OUTPUT: DeclaredOutput = DeclaredOutput {
98    offset: 0,
99    capacity: 0,
100    size: 0,
101    state: OutputState::NotWritten,
102};
103
104/// References of a method call to declared outputs, by position among the method's arguments of
105/// each kind, see [`TransactionPayloadBuilder::with_method_call()`]
106#[derive(Debug, Default, Copy, Clone)]
107pub struct MethodCallReferences<'a> {
108    /// Output index for each `#[slot]`, whose value the slot gets instead of an explicit address.
109    /// `None` or no entry for an explicit address.
110    pub slots: &'a [Option<u8>],
111    /// Output index for each `#[input]`, whose value the input gets instead of a value in the
112    /// payload. `None` or no entry for a value in the payload.
113    pub inputs: &'a [Option<u8>],
114    /// Output index for each `#[output]` and the return value (last), which they write. Exactly
115    /// one entry for each output of the method.
116    pub outputs: &'a [u8],
117}
118
119/// Builder for payload to be used with [`TxHandler`] (primarily for [`SimpleWallet`]).
120///
121/// The number of outputs that the payload declares is given to [`Self::new()`]. Method calls,
122/// added with [`Self::with_method_call()`], write these outputs and refer to them by output index,
123/// and the builder computes the capacity of every declared output from the method calls that write
124/// it.
125///
126/// Methods in the payload are decoded and called with [`TransactionPayloadCaller`], and can be
127/// described without calling them with [`TransactionPayloadDecoder`].
128///
129/// [`TxHandler`]: ab_contracts_standards::tx_handler::TxHandler
130/// [`SimpleWallet`]: crate::SimpleWalletBase
131/// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
132/// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
133#[derive(Debug, Clone)]
134pub struct TransactionPayloadBuilder {
135    /// Starts with the header, which declares the outputs, with a capacity for each of them that
136    /// method calls raise, see [`Self::into_aligned_bytes()`]
137    payload: OwnedAlignedBuffer,
138    /// Entries before `num_outputs` are the outputs that the payload declares, with their
139    /// capacities and what the method calls added so far wrote into them last, as decoders see
140    /// them after those method calls
141    declared_outputs: [DeclaredOutput; MAX_OUTPUTS],
142    /// Number of outputs that the payload declares, at most [`MAX_OUTPUTS`]
143    num_outputs: u8,
144}
145
146impl TransactionPayloadBuilder {
147    /// Create a new instance for a payload of up to `max_size` bytes that declares `num_outputs`
148    /// outputs.
149    ///
150    /// Memory for the whole payload is allocated here and never reallocated, a method call that
151    /// doesn't fit returns [`TransactionPayloadBuilderError::PayloadTooLarge`]. The payload is
152    /// padded to a multiple of [`Alignment::MAX`], so `max_size` is rounded down to a multiple of
153    /// it.
154    ///
155    /// `#[output]` arguments and return values of method calls write the declared outputs, and
156    /// slots and inputs of later method calls can refer to what was written last, by output index
157    /// from `0` to `num_outputs - 1`, see [`MethodCallReferences`]. The header of the payload has
158    /// room for the capacity of every declared output, which starts at `0`, and every method call
159    /// that writes an output raises its capacity to what the method needs, so capacities don't
160    /// have to be known upfront. The memory of every declared output is aligned for every output
161    /// type. [`SimpleWalletBase::execute()`] only calls the methods of a payload whose declared
162    /// outputs fit into [`OUTPUT_BUFFER_SIZE`] together, see [`TransactionPayloadDecoder`].
163    ///
164    /// The header is written right away, so a payload without method calls is the header.
165    ///
166    /// Returns an error if `num_outputs` is larger than [`MAX_OUTPUTS`] or the header doesn't fit
167    /// into `max_size`.
168    ///
169    /// [`SimpleWalletBase::execute()`]: crate::SimpleWalletBase::execute
170    /// [`OUTPUT_BUFFER_SIZE`]: crate::OUTPUT_BUFFER_SIZE
171    /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
172    // TODO: Not annotated with `no_panic`, since allocating the payload can panic
173    pub fn new(
174        max_size: u32,
175        num_outputs: u8,
176    ) -> Result<Self, TransactionPayloadBuilderError<'static>> {
177        if usize::from(num_outputs) > MAX_OUTPUTS {
178            return Err(TransactionPayloadBuilderError::TooManyOutputs);
179        }
180
181        let mut builder = Self {
182            payload: OwnedAlignedBuffer::with_capacity(
183                max_size / u32::from(Alignment::MAX.get()) * u32::from(Alignment::MAX.get()),
184            ),
185            declared_outputs: [UNWRITTEN_OUTPUT; MAX_OUTPUTS],
186            num_outputs,
187        };
188        builder.push_payload_byte(num_outputs)?;
189        // Method calls that write an output raise its capacity
190        for _ in 0..num_outputs {
191            builder.extend_payload_with_alignment(&0u32.to_le_bytes(), Alignment::of::<u32>())?;
192        }
193
194        Ok(builder)
195    }
196
197    /// Add method call to the payload.
198    ///
199    /// The wallet will call this method in addition order.
200    ///
201    /// `references` refer to outputs that the payload declares (see [`Self::new()`]): the output
202    /// index for each `#[slot]` and `#[input]` that gets the value that an earlier method call
203    /// wrote last into that output instead of the value in `external_args`, and the output index
204    /// that each `#[output]` and the return value write. See [`TransactionInput`] for more details.
205    /// The capacity of every output that the method call writes is raised to the recommended
206    /// capacity of the `#[output]` or the size of the return value if it is smaller.
207    ///
208    /// Every output index must be below [`MAX_OUTPUTS`]
209    /// ([`TransactionPayloadBuilderError::InvalidOutputIndex`]). The method call is also checked
210    /// against the rules of the payload format about outputs, mostly with the code that decoders
211    /// use, so that [`TransactionPayloadDecoder`] accepts the payload, and a broken rule is
212    /// returned as [`TransactionPayloadBuilderError::PayloadRule`] with the error that the
213    /// decoder returns for it, in the order in which the decoder checks the rules:
214    /// * a slot or an input only refers to a declared output that an earlier method call wrote
215    ///   ([`TransactionPayloadDecoderError::OutputIndexNotFound`]) and that this method call
216    ///   doesn't write ([`TransactionPayloadDecoderError::OutputReferencedBySameMethod`])
217    /// * a slot only refers to a return value with the size of an address
218    ///   ([`TransactionPayloadDecoderError::InvalidSlotOutputIndexSize`]); an `#[output]` used as a
219    ///   slot gets its size when its method is called, so [`TransactionPayloadCaller`] checks it
220    /// * the method call only writes declared outputs
221    ///   ([`TransactionPayloadDecoderError::OutputIndexNotFound`]), each at most once
222    ///   ([`TransactionPayloadDecoderError::OutputUsedTwice`])
223    ///
224    /// The other rules about outputs hold by construction: the return value is the last output,
225    /// and every output gets a capacity that is enough for every method call that writes it.
226    ///
227    /// Outputs are given to the method empty, with size `0`, whatever an earlier method call wrote
228    /// into them, so methods called from transactions must accept empty outputs, like
229    /// [`MaybeData`] and [`VariableElements`]. A method with an `#[output]` of a [`TrivialType`]
230    /// can't be called directly from a transaction, since its value has to be initialized first,
231    /// for example by a separate method. Initializing values is domain-specific, so the payload
232    /// format doesn't do it. Executors that check arguments against the method's metadata reject
233    /// such calls, while the native executor trusts transactions, so with it such a call violates
234    /// the requirements of the called method.
235    ///
236    /// The return value is given the size of its type, since executors write the whole value, so
237    /// later method calls can refer to it as an input or a slot. The state returned by an `#[init]`
238    /// method is the exception: executors store it as the state of the contract instead of writing
239    /// it to the caller, so it is encoded as an `#[output]` that needs no capacity, like the state
240    /// in the last `#[output]` of `#[init]` without a return value, and later method calls
241    /// referring to it get it empty.
242    ///
243    /// On error, the builder is left unchanged, so other method calls can still be added.
244    ///
245    /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
246    /// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
247    /// [`MaybeData`]: ab_io_type::maybe_data::MaybeData
248    /// [`VariableElements`]: ab_io_type::variable_elements::VariableElements
249    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
250    pub fn with_method_call<Args>(
251        &mut self,
252        contract: &Address,
253        external_args: &Args,
254        method_context: TransactionMethodContext,
255        references: MethodCallReferences<'_>,
256    ) -> Result<(), TransactionPayloadBuilderError<'static>>
257    where
258        Args: ExternalArgs,
259    {
260        let external_args = NonNull::from_ref(external_args).cast::<*const c_void>();
261
262        // SAFETY: Called with statically valid data
263        unsafe {
264            self.with_method_call_untyped(
265                contract,
266                external_args,
267                Args::METADATA,
268                &Args::FINGERPRINT,
269                method_context,
270                references,
271            )
272        }
273    }
274
275    /// Other than unsafe API, this method is identical to [`Self::with_method_call()`].
276    ///
277    /// # Safety
278    /// `external_args` must correspond to `method_metadata` and `method_fingerprint`. Outputs are
279    /// never read from `external_args`. The pointers of slots and inputs that refer to declared
280    /// outputs in `references` are never dereferenced, so they may dangle, but they must not be
281    /// null, since external arguments hold every pointer as [`NonNull`].
282    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
283    pub unsafe fn with_method_call_untyped<'a>(
284        &mut self,
285        contract: &Address,
286        external_args: NonNull<*const c_void>,
287        method_metadata: &'a [u8],
288        method_fingerprint: &MethodFingerprint,
289        method_context: TransactionMethodContext,
290        references: MethodCallReferences<'_>,
291    ) -> Result<(), TransactionPayloadBuilderError<'a>> {
292        let payload_len = self.payload.len();
293        // SAFETY: Guaranteed by this function's contract
294        let result = unsafe {
295            self.add_method_call_untyped(
296                contract,
297                external_args,
298                method_metadata,
299                method_fingerprint,
300                method_context,
301                references,
302            )
303        };
304        if result.is_err() {
305            // Remove the part of a method call that didn't fit, declared outputs weren't changed
306            // SAFETY: The payload only grows, so its first `payload_len` bytes are still
307            // initialized
308            unsafe {
309                self.payload.set_len(payload_len);
310            }
311        }
312
313        result
314    }
315
316    /// Add the method call to the payload, see [`Self::with_method_call_untyped()`].
317    ///
318    /// Errors other than [`TransactionPayloadBuilderError::PayloadTooLarge`] are returned before
319    /// anything is changed. After that error, part of the method call might have been added to
320    /// the payload, while declared outputs are only changed after the whole method call was added.
321    ///
322    /// # Safety
323    /// The same as for [`Self::with_method_call_untyped()`].
324    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
325    unsafe fn add_method_call_untyped<'a>(
326        &mut self,
327        contract: &Address,
328        external_args: NonNull<*const c_void>,
329        mut method_metadata: &'a [u8],
330        method_fingerprint: &MethodFingerprint,
331        method_context: TransactionMethodContext,
332        references: MethodCallReferences<'_>,
333    ) -> Result<(), TransactionPayloadBuilderError<'a>> {
334        let external_args = &mut external_args.cast::<c_void>();
335        let MethodCallReferences {
336            slots: slot_output_indices,
337            inputs: input_output_indices,
338            outputs: output_indices,
339        } = references;
340
341        let (metadata_decoder, method_metadata_item) =
342            MethodMetadataDecoder::new(&mut method_metadata, MethodsContainerKind::Unknown)
343                .decode_next()
344                .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?;
345        let mut metadata_decoder = metadata_decoder.without_auto_drain();
346
347        #[expect(
348            clippy::rest_pattern_accessible_field,
349            reason = "Do not need other fields"
350        )]
351        let MethodMetadataItem {
352            method_kind,
353            num_arguments,
354            ..
355        } = method_metadata_item;
356        let number_of_arguments = num_arguments.saturating_add(u8::from(method_kind.has_self()));
357
358        if number_of_arguments > MAX_TOTAL_METHOD_ARGS {
359            return Err(TransactionPayloadBuilderError::TooManyArguments(
360                number_of_arguments,
361            ));
362        }
363
364        let mut num_slot_arguments = 0u8;
365        let mut num_input_arguments = 0u8;
366        let mut num_output_arguments = 0u8;
367        // Offset of the return value among outputs and its size if executors write it to the
368        // caller
369        let mut return_value_offset = None;
370        let mut return_value_size = None;
371
372        let mut input_output_type_details =
373            [MaybeUninit::<IoTypeDetails>::uninit(); MAX_TOTAL_METHOD_ARGS as usize];
374        // Collect information about all arguments, so that everything except whether the method
375        // call fits is checked before the payload is changed
376        while let Some(item) = metadata_decoder
377            .decode_next()
378            .transpose()
379            .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?
380        {
381            match item.argument_kind {
382                ArgumentKind::EnvRo
383                | ArgumentKind::EnvRw
384                | ArgumentKind::TmpRo
385                | ArgumentKind::TmpRw => {
386                    // Not represented in external args
387                }
388                ArgumentKind::SlotRo | ArgumentKind::SlotRw => {
389                    num_slot_arguments += 1;
390                }
391                ArgumentKind::Input => {
392                    // SAFETY: The counters only count arguments decoded before this one, so they
393                    // and their sum are below `num_arguments`, the number of items that the
394                    // metadata decoder returns (see `MethodMetadataItem::num_arguments`). The check
395                    // of the number of arguments above limits `num_arguments` to
396                    // `MAX_TOTAL_METHOD_ARGS`.
397                    unsafe {
398                        hint::assert_unchecked(num_input_arguments < MAX_TOTAL_METHOD_ARGS);
399                    }
400                    input_output_type_details[usize::from(num_input_arguments)]
401                        .write(item.type_details.unwrap_or(IoTypeDetails::bytes(0)));
402                    num_input_arguments += 1;
403                }
404                ArgumentKind::Output | ArgumentKind::Return => {
405                    // The state of `#[init]` has no type details, so it needs no capacity
406                    let type_details = item.type_details.unwrap_or(IoTypeDetails::bytes(0));
407                    // The return value is the last argument (see
408                    // `ArgumentsMetadataDecoder::decode_next()`), so it is also the last output. In
409                    // `#[init]` it is the state, which executors store instead of writing it to the
410                    // caller, so it is encoded as an output then, like the state in the last
411                    // `#[output]` of `#[init]` without a return value.
412                    if matches!(item.argument_kind, ArgumentKind::Return)
413                        && !matches!(method_kind, MethodKind::Init)
414                    {
415                        return_value_offset = Some(usize::from(num_output_arguments));
416                        // The size of the return value is the size of its type, which is its
417                        // recommended capacity
418                        return_value_size = Some(type_details.recommended_capacity);
419                    }
420                    // SAFETY: Same as for inputs above
421                    unsafe {
422                        hint::assert_unchecked(
423                            num_input_arguments + num_output_arguments < MAX_TOTAL_METHOD_ARGS,
424                        );
425                    }
426                    // After all inputs, which come before outputs
427                    input_output_type_details
428                        [usize::from(num_input_arguments + num_output_arguments)]
429                    .write(type_details);
430                    num_output_arguments += 1;
431                }
432            }
433        }
434        // SAFETY: As for the writes above, the counters and their sum are at most `num_arguments`,
435        // so they stay within the `MAX_TOTAL_METHOD_ARGS` elements of the array. The metadata
436        // decoder returns all inputs before outputs (see
437        // `ArgumentsMetadataDecoder::decode_next()`), so the first `num_input_arguments` elements
438        // were initialized above with inputs and the next `num_output_arguments` ones with
439        // outputs.
440        let (input_type_details, output_type_details) = unsafe {
441            let (input_type_details, output_type_details) =
442                input_output_type_details.split_at_unchecked(usize::from(num_input_arguments));
443            let output_type_details =
444                output_type_details.get_unchecked(..usize::from(num_output_arguments));
445
446            (
447                input_type_details.assume_init_ref(),
448                output_type_details.assume_init_ref(),
449            )
450        };
451
452        if slot_output_indices.len() > usize::from(num_slot_arguments) {
453            return Err(TransactionPayloadBuilderError::TooManySlotReferences {
454                num_slots: num_slot_arguments,
455                num_references: slot_output_indices.len(),
456            });
457        }
458        if input_output_indices.len() > usize::from(num_input_arguments) {
459            return Err(TransactionPayloadBuilderError::TooManyInputReferences {
460                num_inputs: num_input_arguments,
461                num_references: input_output_indices.len(),
462            });
463        }
464        if output_indices.len() != usize::from(num_output_arguments) {
465            return Err(TransactionPayloadBuilderError::NumberOfOutputsMismatch {
466                expected: num_output_arguments,
467                actual: output_indices.len(),
468            });
469        }
470        for &output_index in slot_output_indices
471            .iter()
472            .chain(input_output_indices)
473            .flatten()
474            .chain(output_indices)
475        {
476            if usize::from(output_index) >= MAX_OUTPUTS {
477                return Err(TransactionPayloadBuilderError::InvalidOutputIndex(
478                    output_index,
479                ));
480            }
481        }
482
483        // `TransactionOutput` for each output, the return value last
484        let mut output_tags = [0; MAX_TOTAL_METHOD_ARGS as usize];
485        for ((output_offset, &output_index), output_tag) in
486            output_indices.iter().enumerate().zip(&mut output_tags)
487        {
488            let output = if return_value_offset == Some(output_offset) {
489                TransactionOutput::new_return_value(output_index)
490            } else {
491                TransactionOutput::new_output(output_index)
492            }
493            .ok_or(TransactionPayloadBuilderError::InvalidOutputIndex(
494                output_index,
495            ))?;
496            *output_tag = output.into_u8();
497        }
498        // SAFETY: `num_output_arguments` is at most `MAX_TOTAL_METHOD_ARGS`, as for the details of
499        // outputs above, and the loop above wrote a tag for each of the as many output indices
500        let output_tags = unsafe { output_tags.get_unchecked(..usize::from(num_output_arguments)) };
501
502        // The rules of the payload format about outputs, checked in the same order as decoders do,
503        // with `declared_outputs` in the state that decoders have after the earlier method calls
504        for &output_index in slot_output_indices.iter().flatten() {
505            slot_output_ref(self.declared_outputs(), output_index, output_tags)
506                .map_err(TransactionPayloadBuilderError::PayloadRule)?;
507        }
508        for &output_index in input_output_indices.iter().flatten() {
509            output_ref(self.declared_outputs(), output_index, output_tags)
510                .map_err(TransactionPayloadBuilderError::PayloadRule)?;
511        }
512        // Every output writes a declared output, each at most once, as
513        // `PayloadDecoderCore::write_outputs()` checks. Its other rules hold by construction: the
514        // tags are valid, and the return value is the last output.
515        let mut remaining_output_tags = output_tags;
516        while let Some((&output_tag, later_output_tags)) = remaining_output_tags.split_first() {
517            remaining_output_tags = later_output_tags;
518
519            let output_index = TransactionOutput::tag_output_index(output_tag);
520            if output_index >= self.num_outputs {
521                return Err(TransactionPayloadBuilderError::PayloadRule(
522                    TransactionPayloadDecoderError::OutputIndexNotFound(output_index),
523                ));
524            }
525            if writes_output(later_output_tags, output_index) {
526                return Err(TransactionPayloadBuilderError::PayloadRule(
527                    TransactionPayloadDecoderError::OutputUsedTwice(output_index),
528                ));
529            }
530        }
531
532        self.extend_payload_with_alignment(contract.as_bytes(), Alignment::of::<Address>())?;
533        self.extend_payload_with_alignment(
534            method_fingerprint.as_bytes(),
535            Alignment::of::<MethodFingerprint>(),
536        )?;
537        self.push_payload_byte(method_context as u8)?;
538
539        // Store number of slots and `TransactionSlot` for each slot
540        self.push_payload_byte(num_slot_arguments)?;
541        for slot_offset in 0..usize::from(num_slot_arguments) {
542            let slot_type = if let Some(&Some(output_index)) = slot_output_indices.get(slot_offset)
543            {
544                TransactionSlot::new_output_index(output_index).ok_or(
545                    TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
546                )?
547            } else {
548                TransactionSlot::new_address()
549            };
550            self.push_payload_byte(slot_type.into_u8())?;
551        }
552
553        // Store number of inputs and `TransactionInput` for each input
554        self.push_payload_byte(num_input_arguments)?;
555        for (input_offset, type_details) in input_type_details.iter().enumerate() {
556            let input_type =
557                if let Some(&Some(output_index)) = input_output_indices.get(input_offset) {
558                    // Only the output index is written, so the alignment of the input isn't used
559                    TransactionInput::new_output_index(output_index).ok_or(
560                        TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
561                    )?
562                } else {
563                    TransactionInput::new_value(type_details.alignment)
564                };
565            self.push_payload_byte(input_type.into_u8())?;
566        }
567
568        // Store number of outputs and `TransactionOutput` for each output
569        self.push_payload_byte(num_output_arguments)?;
570        for &output_tag in output_tags {
571            self.push_payload_byte(output_tag)?;
572        }
573
574        for slot_offset in 0..usize::from(num_slot_arguments) {
575            // SAFETY: Method description requires the layout to correspond to metadata, where all
576            // slots come before inputs and outputs (see `ArgumentsMetadataDecoder::decode_next()`),
577            // and the pointer to be non-null even when it dangles
578            let address = unsafe { read_external_args::<NonNull<Address>>(external_args) };
579
580            if slot_output_indices
581                .get(slot_offset)
582                .copied()
583                .flatten()
584                .is_none()
585            {
586                // SAFETY: Method description requires the layout to correspond to metadata, and
587                // the slot doesn't refer to a declared output, so it points to an address
588                let address = unsafe { address.as_ref() };
589                self.extend_payload_with_alignment(address.as_bytes(), Alignment::of::<Address>())?;
590            }
591        }
592
593        for (input_offset, type_details) in input_type_details.iter().enumerate() {
594            // SAFETY: Method description requires the layout to correspond to metadata, where the
595            // inputs come right after the slots (see `ArgumentsMetadataDecoder::decode_next()`),
596            // and the pointer to be non-null even when it dangles
597            let FfiDataSizeCapacityRo {
598                data_ptr,
599                size,
600                capacity: _,
601            } = unsafe { read_external_args(external_args) };
602
603            if input_output_indices
604                .get(input_offset)
605                .copied()
606                .flatten()
607                .is_none()
608            {
609                // SAFETY: Method description requires the layout to correspond to metadata, and
610                // the input doesn't refer to a declared output, so it points to `size` bytes
611                let data =
612                    unsafe { slice::from_raw_parts(data_ptr.as_ptr().cast_const(), size as usize) };
613                self.extend_payload_with_alignment(&size.to_le_bytes(), Alignment::of::<u32>())?;
614                self.extend_payload_with_alignment(data, type_details.alignment)?;
615            }
616        }
617
618        if let Some(size) = return_value_size {
619            self.extend_payload_with_alignment(&size.to_le_bytes(), Alignment::of::<u32>())?;
620        }
621
622        // Only after the whole method call was added, so that declared outputs don't change if it
623        // doesn't fit. An `#[output]` needs its recommended capacity, and the recommended capacity
624        // of a return value is its size.
625        for (&output_tag, type_details) in output_tags.iter().zip(output_type_details) {
626            // SAFETY: The tag was made from a `TransactionOutput` above, and every output index was
627            // checked above to refer to a declared output
628            unsafe {
629                let output = TransactionOutput::from_u8(output_tag).unwrap_unchecked();
630                self.write_output(output, type_details.recommended_capacity);
631            }
632        }
633
634        Ok(())
635    }
636
637    /// Returns 16-byte aligned bytes, padded to a multiple of `u128` size.
638    ///
639    /// The contents is a header with the declared outputs, followed by a concatenated sequence of
640    /// method calls with their arguments. All data structures are correctly aligned in the
641    /// returned byte buffer with `0` used as padding when necessary.
642    /// [`OwnedAlignedBuffer::as_u128_slice()`] returns the whole payload as `u128`s. Every payload
643    /// starts with the header, also without declared outputs and method calls.
644    ///
645    /// The capacity of every declared output is the largest that a method call writing it needs:
646    /// the recommended capacity of an `#[output]`, the size of a return value, or `0` for the state
647    /// returned by an `#[init]` method.
648    ///
649    /// Returns [`TransactionPayloadBuilderError::UnusedOutput`] with the first declared output that
650    /// no method call writes, since its memory would never be used, which most likely means that a
651    /// method call refers to the wrong output index. [`TransactionPayloadDecoder`] and
652    /// [`TransactionPayloadCaller`] accept such a payload.
653    ///
654    /// The header:
655    /// * Number of declared outputs: `u8`
656    /// * For each declared output:
657    ///     * Its capacity as little-endian `u32`
658    ///
659    /// Each method is serialized in the following way:
660    /// * Contract to call: [`Address`]
661    /// * Fingerprint of the method to call: [`MethodFingerprint`]
662    /// * Method context: [`TransactionMethodContext`]
663    /// * Number of `#[slot]` arguments: `u8`
664    /// * For each `#[slot]` argument:
665    ///     * [`TransactionSlot`] as `u8`
666    /// * Number of `#[input]` arguments: `u8`
667    /// * For each `#[input]` argument:
668    ///     * [`TransactionInput`] as `u8`
669    /// * Number of `#[output]` arguments, including the return value: `u8`
670    /// * For each `#[output]` argument, followed by the return value if there is one:
671    ///     * [`TransactionOutput`] as `u8`
672    /// * For each [`TransactionSlot`], whose type is [`TransactionSlotType::Address`]:
673    ///     * [`Address`]
674    /// * For each [`TransactionInput`], whose type is [`TransactionInputType::Value`]:
675    ///     * Input size as little-endian `u32` followed by the input itself
676    /// * If the last [`TransactionOutput`] is a return value:
677    ///     * Its size as little-endian `u32`
678    ///
679    /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
680    /// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
681    /// [`TransactionSlotType::Address`]: crate::payload::TransactionSlotType::Address
682    /// [`TransactionInputType::Value`]: crate::payload::TransactionInputType::Value
683    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
684    pub fn into_aligned_bytes(
685        mut self,
686    ) -> Result<OwnedAlignedBuffer, TransactionPayloadBuilderError<'static>> {
687        for (output_index, declared_output) in (0..=u8::MAX).zip(self.declared_outputs()) {
688            if declared_output.state == OutputState::NotWritten {
689                return Err(TransactionPayloadBuilderError::UnusedOutput(output_index));
690            }
691        }
692
693        // SAFETY: Constructor guarantees that capacity is a multiple of `Alignment::MAX` and is
694        // never exceeded
695        unsafe {
696            self.ensure_alignment(Alignment::MAX).unwrap_unchecked();
697        }
698
699        Ok(self.payload)
700    }
701
702    /// Record that a method call writes `output`, which needs `capacity` bytes, and raise the
703    /// capacity of its declared output in the header to that if it is smaller.
704    ///
705    /// # Safety
706    /// The output index of `output` must refer to a declared output.
707    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
708    unsafe fn write_output(&mut self, output: TransactionOutput, capacity: u32) {
709        let output_index = usize::from(output.output_index());
710        // Capacities follow the number of declared outputs and 3 bytes of padding
711        let offset = size_of::<u32>() * (1 + output_index);
712        // SAFETY: The output is declared (function contract), and `Self::new()` declares at most
713        // `MAX_OUTPUTS` outputs. `Self::new()` wrote a capacity for every declared output into the
714        // header, and the payload only grows after that.
715        unsafe {
716            hint::assert_unchecked(
717                output_index < MAX_OUTPUTS
718                    && offset + size_of::<u32>() <= self.payload.len() as usize,
719            );
720        }
721
722        let declared_output = self
723            .declared_outputs
724            .get_mut(output_index)
725            .expect("Below `MAX_OUTPUTS`, as asserted above; qed");
726        declared_output.capacity = declared_output.capacity.max(capacity);
727        let kind = output.kind();
728        let size = match kind {
729            TransactionOutputKind::Output => 0,
730            // The capacity that a return value needs is its size
731            TransactionOutputKind::ReturnValue => capacity,
732        };
733        declared_output.set_written(kind, size);
734
735        let declared_capacity = self.payload.as_mut_slice()[offset..]
736            .first_chunk_mut::<{ size_of::<u32>() }>()
737            .expect("The header has a capacity for every declared output; qed");
738        *declared_capacity = declared_output.capacity.to_le_bytes();
739    }
740
741    /// Outputs that the payload declares
742    #[inline(always)]
743    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
744    fn declared_outputs(&self) -> &[DeclaredOutput] {
745        // SAFETY: `Self::new()` sets `num_outputs` to at most `MAX_OUTPUTS`, the length of
746        // `declared_outputs`, and it never changes after that
747        unsafe {
748            self.declared_outputs
749                .get_unchecked(..usize::from(self.num_outputs))
750        }
751    }
752
753    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
754    fn extend_payload_with_alignment(
755        &mut self,
756        bytes: &[u8],
757        alignment: Alignment,
758    ) -> Result<(), TransactionPayloadBuilderError<'static>> {
759        self.ensure_alignment(alignment)?;
760        self.extend_payload(bytes)
761    }
762
763    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
764    fn ensure_alignment(
765        &mut self,
766        alignment: Alignment,
767    ) -> Result<(), TransactionPayloadBuilderError<'static>> {
768        let alignment = usize::from(alignment.get());
769
770        let unaligned_by = self.payload.len() as usize % alignment;
771        if unaligned_by > 0 {
772            let padding_bytes = alignment - unaligned_by;
773            // Byte by byte rather than as a slice of zeroes, which the compiler can't prove to be
774            // in bounds
775            for _ in 0..padding_bytes {
776                self.push_payload_byte(0)?;
777            }
778        }
779
780        Ok(())
781    }
782
783    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
784    fn push_payload_byte(
785        &mut self,
786        byte: u8,
787    ) -> Result<(), TransactionPayloadBuilderError<'static>> {
788        self.extend_payload(&[byte])
789    }
790
791    /// Append `bytes` to the payload if they fit into its capacity, it is never reallocated
792    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
793    fn extend_payload(
794        &mut self,
795        bytes: &[u8],
796    ) -> Result<(), TransactionPayloadBuilderError<'static>> {
797        // The same checks as in `OwnedAlignedBuffer::append()`, so the compiler can prove that it
798        // never reallocates
799        let fits = u32::try_from(bytes.len())
800            .ok()
801            .and_then(|len| self.payload.len().checked_add(len))
802            .is_some_and(|new_len| new_len <= self.payload.capacity());
803        if !(fits && self.payload.append(bytes)) {
804            return Err(TransactionPayloadBuilderError::PayloadTooLarge);
805        }
806
807        Ok(())
808    }
809}