ab_system_contract_simple_wallet_base/
payload.rs

1//! This module contains generic utilities for serializing and deserializing method calls to/from
2//! payload bytes.
3//!
4//! It can be reused to implement a different wallet implementation as well as read and verify the
5//! contents of the transaction (for example, to display it on the screen of the hardware wallet).
6//!
7//! Builder interface requires heap allocations and can be enabled with `payload-builder` feature,
8//! while the rest works in `no_std` environment without a global allocator.
9
10#[cfg(feature = "payload-builder")]
11pub mod builder;
12
13use ab_contracts_common::Address;
14use ab_contracts_common::env::{MethodContext, PreparedMethod};
15use ab_contracts_common::method::MethodFingerprint;
16use ab_contracts_io_type::MAX_ALIGNMENT;
17use ab_contracts_io_type::trivial_type::TrivialType;
18use core::ffi::c_void;
19use core::marker::PhantomData;
20use core::mem::MaybeUninit;
21use core::num::{NonZeroU8, NonZeroUsize};
22use core::ops::{Deref, DerefMut};
23use core::ptr::NonNull;
24use core::slice;
25
26#[derive(Debug, Copy, Clone, Eq, PartialEq, TrivialType)]
27#[repr(u8)]
28pub enum TransactionMethodContext {
29    /// Call contract under [`Address::NULL`] context (corresponds to [`MethodContext::Reset`])
30    Null,
31    /// Call contract under context of the wallet (corresponds to [`MethodContext::Replace`])
32    Wallet,
33}
34
35impl TransactionMethodContext {
36    // TODO: Implement `TryFrom` once it is available in const environment
37    /// Try to create an instance from its `u8` representation
38    pub const fn try_from_u8(n: u8) -> Option<Self> {
39        Some(match n {
40            0 => Self::Null,
41            1 => Self::Wallet,
42            _ => {
43                return None;
44            }
45        })
46    }
47}
48
49#[derive(Debug, Copy, Eq, PartialEq, Clone)]
50pub enum TransactionInputType {
51    Value { alignment_power: u8 },
52    OutputIndex { output_index: u8 },
53}
54
55/// The type of transaction input could be either explicit value or output index.
56///
57/// Specifically, if the previous method has `#[output]` or return value, those values are collected
58/// and pushed into a virtual "stack". Then, if [`Self::input_type()`] returns
59/// [`TransactionInputType::OutputIndex`], then the corresponding input will use the value at
60/// `output_index` of this stack instead of what was specified in `external_args`. This allows
61/// composing calls to multiple methods into more sophisticated workflows without writing special
62/// contracts for this.
63#[derive(Debug, Copy, Clone)]
64pub struct TransactionInput(TransactionInputType);
65
66impl TransactionInput {
67    /// Regular input value with specified alignment.
68    ///
69    /// Valid alignment values are: 1, 2, 4, 8, 16.
70    pub const fn new_value(alignment: NonZeroU8) -> Option<Self> {
71        match alignment.get() {
72            1 | 2 | 4 | 8 | 16 => Some(Self(TransactionInputType::Value {
73                alignment_power: alignment.ilog2() as u8,
74            })),
75            _ => None,
76        }
77    }
78
79    /// Output index value.
80    ///
81    /// Valid index values are 0..=127.
82    pub const fn new_output_index(output_index: u8) -> Option<Self> {
83        if output_index > 0b0111_1111 {
84            return None;
85        }
86
87        Some(Self(TransactionInputType::OutputIndex { output_index }))
88    }
89
90    /// Create an instance from `u8`
91    pub const fn from_u8(n: u8) -> Self {
92        // The first bit is set to 1 for value and 0 for output index
93        if n & 0b1000_0000 == 0 {
94            Self(TransactionInputType::OutputIndex { output_index: n })
95        } else {
96            Self(TransactionInputType::Value {
97                alignment_power: n & 0b0111_1111,
98            })
99        }
100    }
101
102    /// Convert instance into `u8`
103    pub const fn into_u8(self) -> u8 {
104        // The first bit is set to 1 for value and 0 for output index
105        match self.0 {
106            TransactionInputType::Value { alignment_power } => 0b1000_0000 | alignment_power,
107            TransactionInputType::OutputIndex { output_index } => output_index,
108        }
109    }
110
111    /// Returns `Some(output_index)` or `None` if regular input value
112    pub const fn input_type(self) -> TransactionInputType {
113        self.0
114    }
115}
116
117/// Errors for [`TransactionPayloadDecoder`]
118#[derive(Debug, thiserror::Error)]
119pub enum TransactionPayloadDecoderError {
120    /// Payload too small
121    #[error("Payload too small")]
122    PayloadTooSmall,
123    /// `ExternalArgs` buffer too small
124    #[error("`ExternalArgs` buffer too small")]
125    ExternalArgsBufferTooSmall,
126    /// Output index not found
127    #[error("Output index not found: {0}")]
128    OutputIndexNotFound(u8),
129    /// Alignment power is too large
130    #[error("Alignment power is too large: {0}")]
131    AlignmentPowerTooLarge(u8),
132    /// Output buffer too small
133    #[error("Output buffer too small")]
134    OutputBufferTooSmall,
135    /// Output buffer offsets too small
136    #[error("Output buffer offsets too small")]
137    OutputBufferOffsetsTooSmall,
138}
139
140/// Decoder for transaction payload created using `TransactionPayloadBuilder`.
141#[derive(Debug)]
142pub struct TransactionPayloadDecoder<'a> {
143    payload: &'a [u8],
144    external_args_buffer: &'a mut [*mut c_void],
145    output_buffer: &'a mut [MaybeUninit<u128>],
146    output_buffer_cursor: usize,
147    output_buffer_offsets: &'a mut [MaybeUninit<(u32, u32)>],
148    output_buffer_offsets_cursor: usize,
149    map_context: fn(TransactionMethodContext) -> MethodContext,
150}
151
152impl<'a> TransactionPayloadDecoder<'a> {
153    /// Create new instance.
154    ///
155    /// The size of `external_args_buffer` defines max number of bytes allocated for `ExternalArgs`,
156    /// which impacts the number of arguments that can be represented by `ExternalArgs`. The size is
157    /// specified in pointers with `#[slot]` argument using one pointer, `#[input]` two pointers and
158    /// `#[output]` three pointers each.
159    ///
160    /// The size of `output_buffer` defines how big the total size of `#[output]` and return values
161    /// could be in all methods of the payload together.
162    ///
163    /// The size of `output_buffer_offsets` defines how many `#[output]` arguments and return values
164    /// could exist in all methods of the payload together.
165    #[inline]
166    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
167    pub fn new(
168        payload: &'a [u128],
169        external_args_buffer: &'a mut [*mut c_void],
170        output_buffer: &'a mut [MaybeUninit<u128>],
171        output_buffer_offsets: &'a mut [MaybeUninit<(u32, u32)>],
172        map_context: fn(TransactionMethodContext) -> MethodContext,
173    ) -> Self {
174        debug_assert_eq!(align_of_val(payload), usize::from(MAX_ALIGNMENT));
175        debug_assert_eq!(align_of_val(output_buffer), usize::from(MAX_ALIGNMENT));
176
177        // SAFETY: Memory is valid and bound by argument's lifetime
178        let payload =
179            unsafe { slice::from_raw_parts(payload.as_ptr().cast::<u8>(), size_of_val(payload)) };
180
181        Self {
182            payload,
183            external_args_buffer,
184            output_buffer,
185            output_buffer_cursor: 0,
186            output_buffer_offsets,
187            output_buffer_offsets_cursor: 0,
188            map_context,
189        }
190    }
191}
192
193impl<'a> TransactionPayloadDecoder<'a> {
194    /// Decode the next method (if any) in the payload
195    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
196    pub fn decode_next_method(
197        &mut self,
198    ) -> Result<Option<PreparedMethod<'_>>, TransactionPayloadDecoderError> {
199        TransactionPayloadDecoderInternal::<true>(self).decode_next_method()
200    }
201
202    /// Decode the next method (if any) in the payload without checking size.
203    ///
204    /// # Safety
205    /// Must be used with trusted input created using `TransactionPayloadBuilder` or pre-verified
206    /// using [`Self::decode_next_method()`] earlier.
207    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
208    pub unsafe fn decode_next_method_unchecked(&mut self) -> Option<PreparedMethod<'_>> {
209        TransactionPayloadDecoderInternal::<false>(self)
210            .decode_next_method()
211            .expect("No decoding errors are possible with trusted input; qed")
212    }
213}
214
215/// # Safety
216/// When `VERIFY == false` input must be trusted and created using `TransactionPayloadBuilder` or
217/// pre-verified using `VERIFY == true` earlier.
218struct TransactionPayloadDecoderInternal<'tmp, 'decoder, const VERIFY: bool>(
219    &'tmp mut TransactionPayloadDecoder<'decoder>,
220);
221
222impl<'tmp, 'decoder, const VERIFY: bool> Deref
223    for TransactionPayloadDecoderInternal<'tmp, 'decoder, VERIFY>
224{
225    type Target = TransactionPayloadDecoder<'decoder>;
226
227    #[inline(always)]
228    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
229    fn deref(&self) -> &Self::Target {
230        self.0
231    }
232}
233
234impl<'tmp, 'decoder, const VERIFY: bool> DerefMut
235    for TransactionPayloadDecoderInternal<'tmp, 'decoder, VERIFY>
236{
237    #[inline(always)]
238    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
239    fn deref_mut(&mut self) -> &mut Self::Target {
240        self.0
241    }
242}
243
244impl<'tmp, 'decoder, const VERIFY: bool> TransactionPayloadDecoderInternal<'tmp, 'decoder, VERIFY> {
245    #[inline(always)]
246    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
247    fn decode_next_method(
248        mut self,
249    ) -> Result<Option<PreparedMethod<'decoder>>, TransactionPayloadDecoderError> {
250        if self.payload.len() <= usize::from(MAX_ALIGNMENT) {
251            return Ok(None);
252        }
253
254        let contract = self.get_trivial_type::<Address>()?;
255        let method_fingerprint = self.get_trivial_type::<MethodFingerprint>()?;
256        let method_context =
257            (self.map_context)(*self.get_trivial_type::<TransactionMethodContext>()?);
258        let num_slot_arguments = self.read_u8()?;
259        let num_input_arguments = self.read_u8()?;
260        let num_output_arguments = self.read_u8()?;
261
262        // Slot needs one address pointer, input needs pointers to data and size, output needs
263        // pointers to data, size and capacity
264        let expected_external_args_buffer_size = usize::from(num_slot_arguments)
265            + usize::from(num_input_arguments) * 2
266            + usize::from(num_output_arguments) * 3;
267        if expected_external_args_buffer_size > self.external_args_buffer.len() {
268            return Err(TransactionPayloadDecoderError::ExternalArgsBufferTooSmall);
269        }
270
271        let external_args =
272            NonNull::new(self.external_args_buffer.as_mut_ptr()).expect("Not null; qed");
273        {
274            let mut external_args_cursor = external_args;
275
276            for _ in 0..num_slot_arguments {
277                let slot = self.get_trivial_type::<Address>()?;
278                // SAFETY: Size of `self.external_args_buffer` checked above, buffer is correctly
279                // aligned
280                unsafe {
281                    external_args_cursor.cast::<*const Address>().write(slot);
282                    external_args_cursor = external_args_cursor.offset(1);
283                }
284            }
285
286            for _ in 0..num_input_arguments {
287                let (bytes, size) = match TransactionInput::from_u8(self.read_u8()?).input_type() {
288                    TransactionInputType::Value { alignment_power } => {
289                        // Optimized version of the following:
290                        // let alignment = 2usize.pow(u32::from(alignment_power));
291                        let alignment = if VERIFY {
292                            1_usize.checked_shl(u32::from(alignment_power)).ok_or(
293                                TransactionPayloadDecoderError::AlignmentPowerTooLarge(
294                                    alignment_power,
295                                ),
296                            )?
297                        } else {
298                            // SAFETY: The unverified version, see struct description
299                            unsafe { 1_usize.unchecked_shl(u32::from(alignment_power)) }
300                        };
301
302                        let size = self.get_trivial_type::<u32>()?;
303                        let bytes = self.get_bytes(
304                            *size,
305                            NonZeroUsize::new(alignment).expect("Not zero; qed"),
306                        )?;
307
308                        (bytes, size)
309                    }
310                    TransactionInputType::OutputIndex { output_index } => {
311                        let (size_offset, output_offset) = if VERIFY {
312                            if usize::from(output_index) < self.output_buffer_offsets_cursor {
313                                // SAFETY: Checked that index is initialized
314                                unsafe {
315                                    self.output_buffer_offsets
316                                        .get_unchecked(usize::from(output_index))
317                                        .assume_init()
318                                }
319                            } else {
320                                return Err(TransactionPayloadDecoderError::OutputIndexNotFound(
321                                    output_index,
322                                ));
323                            }
324                        } else {
325                            // SAFETY: The unverified version, see struct description
326                            unsafe {
327                                self.output_buffer_offsets
328                                    .get_unchecked(usize::from(output_index))
329                                    .assume_init()
330                            }
331                        };
332
333                        // SAFETY: Offset was created as the result of writing value at the correct
334                        // offset into `output_buffer_offsets` earlier
335                        let size = unsafe {
336                            self.output_buffer
337                                .as_ptr()
338                                .byte_add(size_offset as usize)
339                                .cast::<u32>()
340                                .as_ref_unchecked()
341                        };
342                        // SAFETY: Offset was created as the result of writing value at the correct
343                        // offset into `output_buffer_offsets` earlier
344                        let bytes = unsafe {
345                            let bytes_ptr = self
346                                .output_buffer
347                                .as_ptr()
348                                .cast::<u8>()
349                                .add(output_offset as usize);
350
351                            slice::from_raw_parts(bytes_ptr, *size as usize)
352                        };
353
354                        (bytes, size)
355                    }
356                };
357
358                // SAFETY: Size of `self.external_args_buffer` checked above, buffer is correctly
359                // aligned
360                unsafe {
361                    external_args_cursor
362                        .cast::<*const u8>()
363                        .write(bytes.as_ptr());
364                    external_args_cursor = external_args_cursor.offset(1);
365
366                    external_args_cursor.cast::<*const u32>().write(size);
367                    external_args_cursor = external_args_cursor.offset(1);
368                }
369            }
370
371            for _ in 0..num_output_arguments {
372                let recommended_capacity = self.get_trivial_type::<u32>()?;
373                let alignment_power = *self.get_trivial_type::<u8>()?;
374                // Optimized version of the following:
375                // let alignment = 2usize.pow(u32::from(alignment_power));
376                let alignment = if VERIFY {
377                    1_usize.checked_shl(u32::from(alignment_power)).ok_or(
378                        TransactionPayloadDecoderError::AlignmentPowerTooLarge(alignment_power),
379                    )?
380                } else {
381                    // SAFETY: The unverified version, see struct description
382                    unsafe { 1_usize.unchecked_shl(u32::from(alignment_power)) }
383                };
384
385                let (size, data) = self.allocate_output_buffer(
386                    *recommended_capacity,
387                    NonZeroUsize::new(alignment).expect("Not zero; qed"),
388                )?;
389
390                // SAFETY: Size of `self.external_args_buffer` checked above, buffer is correctly
391                // aligned
392                unsafe {
393                    external_args_cursor.cast::<*mut u8>().write(data.as_ptr());
394                    external_args_cursor = external_args_cursor.offset(1);
395
396                    external_args_cursor.cast::<*mut u32>().write(size.as_ptr());
397                    external_args_cursor = external_args_cursor.offset(1);
398
399                    external_args_cursor
400                        .cast::<*const u32>()
401                        .write(recommended_capacity);
402                    external_args_cursor = external_args_cursor.offset(1);
403                }
404            }
405        }
406
407        Ok(Some(PreparedMethod {
408            contract: *contract,
409            fingerprint: *method_fingerprint,
410            external_args: external_args.cast::<NonNull<c_void>>(),
411            method_context,
412            phantom: PhantomData,
413        }))
414    }
415
416    #[inline(always)]
417    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
418    fn get_trivial_type<T>(&mut self) -> Result<&'decoder T, TransactionPayloadDecoderError>
419    where
420        T: TrivialType,
421    {
422        self.ensure_alignment(NonZeroUsize::new(align_of::<T>()).expect("Not zero; qed"));
423
424        let bytes;
425        if VERIFY {
426            (bytes, self.payload) = self
427                .payload
428                .split_at_checked(size_of::<T>())
429                .ok_or(TransactionPayloadDecoderError::PayloadTooSmall)?;
430        } else {
431            // SAFETY: The unverified version, see struct description
432            (bytes, self.payload) = unsafe { self.payload.split_at_unchecked(size_of::<T>()) };
433        }
434
435        // SAFETY: Correctly aligned bytes of correct size
436        let value_ref = unsafe { bytes.as_ptr().cast::<T>().as_ref().expect("Not null; qed") };
437
438        Ok(value_ref)
439    }
440
441    #[inline(always)]
442    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
443    fn get_bytes(
444        &mut self,
445        size: u32,
446        alignment: NonZeroUsize,
447    ) -> Result<&'decoder [u8], TransactionPayloadDecoderError> {
448        self.ensure_alignment(alignment);
449
450        let bytes;
451        if VERIFY {
452            (bytes, self.payload) = self
453                .payload
454                .split_at_checked(size as usize)
455                .ok_or(TransactionPayloadDecoderError::PayloadTooSmall)?;
456        } else {
457            // SAFETY: The unverified version, see struct description
458            (bytes, self.payload) = unsafe { self.payload.split_at_unchecked(size as usize) };
459        }
460
461        Ok(bytes)
462    }
463
464    #[inline(always)]
465    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
466    fn read_u8(&mut self) -> Result<u8, TransactionPayloadDecoderError> {
467        let value;
468        if VERIFY {
469            (value, self.payload) = self
470                .payload
471                .split_at_checked(1)
472                .ok_or(TransactionPayloadDecoderError::PayloadTooSmall)?;
473        } else {
474            // SAFETY: The unverified version, see struct description
475            (value, self.payload) = unsafe { self.payload.split_at_unchecked(1) };
476        }
477
478        Ok(value[0])
479    }
480
481    #[inline(always)]
482    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
483    fn ensure_alignment(&mut self, alignment: NonZeroUsize) {
484        debug_assert!(alignment.get() <= usize::from(MAX_ALIGNMENT));
485
486        // Optimized version of the following that expects `alignment` to be a power of 2:
487        // let unaligned_by = self.payload.len() % alignment;
488        let unaligned_by = {
489            let mask = alignment
490                .get()
491                .checked_sub(1)
492                .expect("Left side is not zero; qed");
493            self.payload.len() & mask
494        };
495        self.payload = &self.payload[unaligned_by..];
496    }
497
498    #[inline(always)]
499    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
500    fn allocate_output_buffer(
501        &mut self,
502        capacity: u32,
503        output_alignment: NonZeroUsize,
504    ) -> Result<(NonNull<u32>, NonNull<u8>), TransactionPayloadDecoderError> {
505        if VERIFY && self.output_buffer_offsets.len() == self.output_buffer_offsets_cursor {
506            return Err(TransactionPayloadDecoderError::OutputBufferOffsetsTooSmall);
507        }
508
509        let (size_offset, size_ptr) = self
510            .allocate_output_buffer_ptr(
511                NonZeroUsize::new(align_of::<u32>()).expect("Not zero; qed"),
512                size_of::<u32>(),
513            )
514            .ok_or(TransactionPayloadDecoderError::OutputBufferTooSmall)?;
515        let (output_offset, output_ptr) = self
516            .allocate_output_buffer_ptr(output_alignment, capacity as usize)
517            .ok_or(TransactionPayloadDecoderError::OutputBufferTooSmall)?;
518
519        // SAFETY: Checked above that output buffer offsets is not full
520        let output_buffer_offsets = unsafe {
521            // Borrow-checker doesn't understand that these variables are disjoint
522            let output_buffer_offsets_cursor = self.output_buffer_offsets_cursor;
523            self.output_buffer_offsets
524                .get_unchecked_mut(output_buffer_offsets_cursor)
525        };
526        output_buffer_offsets.write((size_offset as u32, output_offset as u32));
527        self.output_buffer_offsets_cursor += 1;
528
529        Ok((size_ptr, output_ptr))
530    }
531
532    /// Returns `None` if output buffer is not large enough
533    #[inline(always)]
534    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
535    fn allocate_output_buffer_ptr<T>(
536        &mut self,
537        alignment: NonZeroUsize,
538        size: usize,
539    ) -> Option<(usize, NonNull<T>)> {
540        debug_assert!(alignment.get() <= usize::from(MAX_ALIGNMENT));
541
542        // Optimized version of the following that expects `alignment` to be a power of 2:
543        // let unaligned_by = self.output_buffer_cursor % alignment;
544        let unaligned_by = {
545            let mask = alignment
546                .get()
547                .checked_sub(1)
548                .expect("Left side is not zero; qed");
549            self.output_buffer_cursor & mask
550        };
551
552        let new_output_buffer_cursor = if VERIFY {
553            let new_output_buffer_cursor = self
554                .output_buffer_cursor
555                .checked_add(unaligned_by)?
556                .checked_add(size)?;
557
558            if new_output_buffer_cursor > size_of_val(self.output_buffer) {
559                return None;
560            }
561
562            new_output_buffer_cursor
563        } else {
564            // SAFETY: The unverified version, see struct description
565            unsafe {
566                self.output_buffer_cursor
567                    .unchecked_add(unaligned_by)
568                    .unchecked_add(size)
569            }
570        };
571
572        // SAFETY: Bounds and alignment checks are done above
573        let (offset, buffer_ptr) = unsafe {
574            let offset = self.output_buffer_cursor.unchecked_add(unaligned_by);
575            let buffer_ptr = NonNull::new_unchecked(
576                self.output_buffer.as_mut_ptr().byte_add(offset).cast::<T>(),
577            );
578
579            (offset, buffer_ptr)
580        };
581        self.output_buffer_cursor = new_output_buffer_cursor;
582
583        Some((offset, buffer_ptr))
584    }
585}