ab_system_contract_simple_wallet_base/payload/
builder.rs

1//! Transaction payload creation utilities
2
3#[cfg(test)]
4mod tests;
5
6extern crate alloc;
7
8use crate::payload::{
9    FfiDataSizeCapacityRo, TransactionInput, TransactionMethodContext, TransactionSlot,
10};
11use ab_contracts_common::MAX_TOTAL_METHOD_ARGS;
12use ab_contracts_common::metadata::decode::{
13    ArgumentKind, MetadataDecodingError, MethodMetadataDecoder, MethodMetadataItem,
14    MethodsContainerKind,
15};
16use ab_contracts_common::method::{ExternalArgs, MethodFingerprint};
17use ab_core_primitives::address::Address;
18use ab_io_type::MAX_ALIGNMENT;
19use ab_io_type::metadata::IoTypeDetails;
20use ab_io_type::trivial_type::TrivialType;
21use alloc::vec::Vec;
22use core::ffi::c_void;
23use core::mem::MaybeUninit;
24use core::num::NonZeroU8;
25use core::ptr::NonNull;
26use core::{ptr, 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)]
39unsafe fn read_external_args<T>(external_args: &mut NonNull<c_void>) -> T {
40    // SAFETY: guaranteed by this function signature
41    unsafe {
42        let value = external_args.cast::<T>().read();
43        *external_args = external_args.byte_add(size_of::<T>());
44        value
45    }
46}
47
48/// Errors for [`TransactionPayloadBuilder`]
49#[derive(Debug, thiserror::Error)]
50pub enum TransactionPayloadBuilderError<'a> {
51    /// Metadata decoding error
52    #[error("Metadata decoding error: {0}")]
53    MetadataDecodingError(MetadataDecodingError<'a>),
54    /// Too many arguments
55    #[error("Too many arguments")]
56    TooManyArguments(u8),
57    /// Invalid alignment
58    #[error("Invalid alignment: {0}")]
59    InvalidAlignment(NonZeroU8),
60    /// Invalid output index
61    #[error("Invalid output index: {0}")]
62    InvalidOutputIndex(u8),
63}
64
65/// Builder for payload to be used with [`TxHandler`] (primarily for [`SimpleWallet`]).
66///
67/// Decoding can be done with [`TransactionPayloadDecoder`]
68///
69/// [`TxHandler`]: ab_contracts_standards::tx_handler::TxHandler
70/// [`SimpleWallet`]: crate::SimpleWalletBase
71/// [`TransactionPayloadDecoder`]: crate::payload::TransactionPayloadDecoder
72#[derive(Debug, Clone)]
73pub struct TransactionPayloadBuilder {
74    payload: Vec<u8>,
75}
76
77impl Default for TransactionPayloadBuilder {
78    #[inline]
79    fn default() -> Self {
80        Self {
81            payload: Vec::with_capacity(1024),
82        }
83    }
84}
85
86impl TransactionPayloadBuilder {
87    /// Add method call to the payload.
88    ///
89    /// The wallet will call this method in addition order.
90    ///
91    /// `slot_output_index` and `input_output_index` are used for referencing earlier outputs as
92    /// slots or inputs of this method, its values are optional, see [`TransactionInput`] for more
93    /// details.
94    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
95    pub fn with_method_call<Args>(
96        &mut self,
97        contract: &Address,
98        external_args: &Args,
99        method_context: TransactionMethodContext,
100        slot_output_index: &[Option<u8>],
101        input_output_index: &[Option<u8>],
102    ) -> Result<(), TransactionPayloadBuilderError<'static>>
103    where
104        Args: ExternalArgs,
105    {
106        let external_args = NonNull::from_ref(external_args).cast::<*const c_void>();
107
108        // SAFETY: Called with statically valid data
109        unsafe {
110            self.with_method_call_untyped(
111                contract,
112                external_args,
113                Args::METADATA,
114                &Args::FINGERPRINT,
115                method_context,
116                slot_output_index,
117                input_output_index,
118            )
119        }
120    }
121
122    /// Other than unsafe API, this method is identical to [`Self::with_method_call()`].
123    ///
124    /// # Safety
125    /// `external_args` must correspond to `method_metadata` and `method_fingerprint`. Outputs are
126    /// never read from `external_args` and inputs that have corresponding `input_output_index`
127    /// are not read either.
128    #[expect(
129        clippy::too_many_arguments,
130        reason = "Only exceeds the limit due to being untyped, while above typed version is not"
131    )]
132    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
133    pub unsafe fn with_method_call_untyped<'a>(
134        &mut self,
135        contract: &Address,
136        external_args: NonNull<*const c_void>,
137        mut method_metadata: &'a [u8],
138        method_fingerprint: &MethodFingerprint,
139        method_context: TransactionMethodContext,
140        slot_output_index: &[Option<u8>],
141        input_output_index: &[Option<u8>],
142    ) -> Result<(), TransactionPayloadBuilderError<'a>> {
143        let external_args = &mut external_args.cast::<c_void>();
144
145        let (metadata_decoder, method_metadata_item) =
146            MethodMetadataDecoder::new(&mut method_metadata, MethodsContainerKind::Unknown)
147                .decode_next()
148                .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?;
149        let mut metadata_decoder = metadata_decoder.without_auto_drain();
150
151        let MethodMetadataItem {
152            method_kind,
153            num_arguments,
154            ..
155        } = method_metadata_item;
156        let number_of_arguments =
157            num_arguments.saturating_add(if method_kind.has_self() { 1 } else { 0 });
158
159        if number_of_arguments > MAX_TOTAL_METHOD_ARGS {
160            return Err(TransactionPayloadBuilderError::TooManyArguments(
161                number_of_arguments,
162            ));
163        }
164
165        self.extend_payload_with_alignment(contract.as_bytes(), align_of_val(contract));
166        self.extend_payload_with_alignment(
167            method_fingerprint.as_bytes(),
168            align_of_val(method_fingerprint),
169        );
170        self.push_payload_byte(method_context as u8);
171
172        let mut num_slot_arguments = 0u8;
173        let mut num_input_arguments = 0u8;
174        let mut num_output_arguments = 0u8;
175
176        let mut input_output_type_details =
177            [MaybeUninit::<IoTypeDetails>::uninit(); MAX_TOTAL_METHOD_ARGS as usize];
178        // Collect information about all arguments so everything below is able to be purely additive
179        while let Some(item) = metadata_decoder
180            .decode_next()
181            .transpose()
182            .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?
183        {
184            match item.argument_kind {
185                ArgumentKind::EnvRo
186                | ArgumentKind::EnvRw
187                | ArgumentKind::TmpRo
188                | ArgumentKind::TmpRw => {
189                    // Not represented in external args
190                }
191                ArgumentKind::SlotRo | ArgumentKind::SlotRw => {
192                    num_slot_arguments += 1;
193                }
194                ArgumentKind::Input => {
195                    input_output_type_details[usize::from(num_input_arguments)]
196                        .write(item.type_details.unwrap_or(IoTypeDetails::bytes(0)));
197                    num_input_arguments += 1;
198                }
199                ArgumentKind::Output | ArgumentKind::Return => {
200                    input_output_type_details
201                        [usize::from(num_input_arguments + num_output_arguments)]
202                    .write(item.type_details.unwrap_or(IoTypeDetails::bytes(0)));
203                    num_output_arguments += 1;
204                }
205            }
206        }
207        // SAFETY: Just initialized elements above
208        let (input_type_details, output_type_details) = unsafe {
209            let (input_type_details, output_type_details) =
210                input_output_type_details.split_at_unchecked(usize::from(num_input_arguments));
211            let output_type_details =
212                output_type_details.get_unchecked(..usize::from(num_output_arguments));
213
214            (
215                input_type_details.assume_init_ref(),
216                output_type_details.assume_init_ref(),
217            )
218        };
219
220        // Store number of slots and `TransactionSlot` for each slot
221        self.push_payload_byte(num_slot_arguments);
222        for slot_offset in 0..usize::from(num_slot_arguments) {
223            let slot_type = if let Some(&Some(output_index)) = slot_output_index.get(slot_offset) {
224                TransactionSlot::new_output_index(output_index).ok_or(
225                    TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
226                )?
227            } else {
228                TransactionSlot::new_address()
229            };
230            self.push_payload_byte(slot_type.into_u8());
231        }
232
233        // Store number of inputs and `TransactionInput` for each input
234        self.push_payload_byte(num_input_arguments);
235        for (input_offset, type_details) in input_type_details.iter().enumerate() {
236            let input_type = if let Some(&Some(output_index)) = input_output_index.get(input_offset)
237            {
238                TransactionInput::new_output_index(output_index).ok_or(
239                    TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
240                )?
241            } else {
242                TransactionInput::new_value(type_details.alignment).ok_or(
243                    TransactionPayloadBuilderError::InvalidAlignment(type_details.alignment),
244                )?
245            };
246            self.push_payload_byte(input_type.into_u8());
247        }
248
249        // Store number of outputs
250        self.push_payload_byte(num_output_arguments);
251
252        for slot_offset in 0..usize::from(num_slot_arguments) {
253            // SAFETY: Method description requires the layout to correspond to metadata
254            let address = unsafe { read_external_args::<NonNull<Address>>(external_args).as_ref() };
255
256            if slot_output_index
257                .get(slot_offset)
258                .copied()
259                .flatten()
260                .is_none()
261            {
262                self.extend_payload_with_alignment(address.as_bytes(), align_of_val(address));
263            }
264        }
265
266        for (input_offset, type_details) in input_type_details.iter().enumerate() {
267            // SAFETY: Method description requires the layout to correspond to metadata
268            let (size, data) = unsafe {
269                let FfiDataSizeCapacityRo {
270                    data_ptr,
271                    size,
272                    capacity: _,
273                } = read_external_args(external_args);
274
275                let data = slice::from_raw_parts(data_ptr.as_ptr().cast_const(), size as usize);
276
277                (size, data)
278            };
279
280            if input_output_index
281                .get(input_offset)
282                .copied()
283                .flatten()
284                .is_none()
285            {
286                self.extend_payload_with_alignment(&size.to_le_bytes(), align_of_val(&size));
287                self.extend_payload_with_alignment(data, type_details.alignment.get() as usize);
288            }
289        }
290
291        for type_details in output_type_details {
292            self.extend_payload_with_alignment(
293                &type_details.recommended_capacity.to_le_bytes(),
294                align_of_val(&type_details.recommended_capacity),
295            );
296            self.extend_payload_with_alignment(
297                &[type_details.alignment.ilog2() as u8],
298                align_of::<u8>(),
299            );
300        }
301
302        Ok(())
303    }
304
305    /// Returns 16-byte aligned bytes.
306    ///
307    /// The contents is a concatenated sequence of method calls with their arguments. All data
308    /// structures are correctly aligned in the returned byte buffer with `0` used as padding when
309    /// necessary.
310    ///
311    /// Each method is serialized in the following way:
312    /// * Contract to call: [`Address`]
313    /// * Fingerprint of the method to call: [`MethodFingerprint`]
314    /// * Method context: [`TransactionMethodContext`]
315    /// * Number of `#[slot]` arguments: `u8`
316    /// * For each `#[slot]` argument:
317    ///     * [`TransactionSlot`] as `u8`
318    /// * Number of `#[input]` arguments: `u8`
319    /// * For each `#[input]` argument:
320    ///     * [`TransactionInput`] for each `#[input]` argument as `u8`
321    /// * Number of `#[output]` arguments: `u8`
322    /// * For each [`TransactionSlot`], whose type is [`TransactionSlotType::Address`]:
323    ///     * [`Address`]
324    /// * For each [`TransactionInput`], whose type is [`TransactionInputType::Value`]:
325    ///     * Input size as little-endian `u32` followed by the input itself
326    /// * For each `#[output]`:
327    ///     * recommended capacity as little-endian `u32` followed by alignment power as `u8`
328    ///       (`NonZeroU8::ilog2(alignment)`)
329    ///
330    /// [`TransactionSlotType::Address`]: crate::payload::TransactionSlotType::Address
331    /// [`TransactionInputType::Value`]: crate::payload::TransactionInputType::Value
332    // TODO: Figure out how and make it possible to apply `no-panic` here
333    pub fn into_aligned_bytes(mut self) -> Vec<u128> {
334        // Fill bytes to make it multiple of `u128` before creating `u128`-based vector
335        self.ensure_alignment(usize::from(MAX_ALIGNMENT));
336
337        let output_len = self.payload.len() / size_of::<u128>();
338        let mut output = Vec::<u128>::with_capacity(output_len);
339
340        // SAFETY: Pointers are valid for reads/writes, aligned and not overlapping
341        unsafe {
342            ptr::copy_nonoverlapping(
343                self.payload.as_ptr(),
344                output.as_mut_ptr().cast::<u8>(),
345                self.payload.len(),
346            );
347            output.set_len(output_len);
348        }
349
350        debug_assert_eq!(align_of_val(output.as_slice()), usize::from(MAX_ALIGNMENT));
351
352        output
353    }
354
355    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
356    fn extend_payload_with_alignment(&mut self, bytes: &[u8], alignment: usize) {
357        self.ensure_alignment(alignment);
358
359        self.payload.extend_from_slice(bytes);
360    }
361
362    // TODO: Figure out how sand make it possible to apply `no-panic` here
363    fn ensure_alignment(&mut self, alignment: usize) {
364        debug_assert!(alignment <= usize::from(MAX_ALIGNMENT));
365
366        // Optimized version of the following that expects `alignment` to be a power of 2:
367        // let unaligned_by = self.payload.len() % alignment;
368        let unaligned_by = self.payload.len() & (alignment - 1);
369        if unaligned_by > 0 {
370            // SAFETY: Subtracted value is always smaller than alignment
371            let padding_bytes = unsafe { alignment.unchecked_sub(unaligned_by) };
372            self.payload.resize(self.payload.len() + padding_bytes, 0);
373        }
374    }
375
376    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
377    fn push_payload_byte(&mut self, byte: u8) {
378        self.payload.push(byte);
379    }
380}