Skip to main content

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. [`TransactionPayloadCaller`]
5//! calls the methods of a payload one by one. [`TransactionPayloadDecoder`] in [`decoder`] checks a
6//! payload and describes its methods without calling them, for example to show a transaction on the
7//! screen of a hardware wallet before it is signed. Both use the same decoding code, so they apply
8//! the same rules.
9//!
10//! Methods can pass values to later methods of the same payload through outputs. A payload declares
11//! all its outputs upfront, each with a capacity, and every `#[output]` and return value of a
12//! method writes one of them. Later methods can refer to what was written last into a declared
13//! output as a slot or an input, see [`TransactionOutput`].
14//!
15//! Builder interface requires heap allocations and can be enabled with `payload-builder` feature,
16//! while the rest works in `no_std` environment without a global allocator.
17//!
18//! [`TransactionPayloadDecoder`]: decoder::TransactionPayloadDecoder
19
20#[cfg(feature = "payload-builder")]
21pub mod builder;
22mod caller;
23pub mod decoder;
24mod decoder_core;
25#[cfg(test)]
26mod test_utils;
27#[cfg(test)]
28mod tests;
29
30use crate::MAX_OUTPUTS;
31use ab_contracts_common::ContractError;
32use ab_io_type::alignment::Alignment;
33use ab_io_type::trivial_type::TrivialType;
34pub use caller::TransactionPayloadCaller;
35use core::ptr::NonNull;
36
37#[derive(Copy, Clone)]
38#[repr(C)]
39struct FfiDataSizeCapacityRo {
40    data_ptr: NonNull<u8>,
41    size: u32,
42    capacity: u32,
43}
44
45#[derive(Copy, Clone)]
46#[repr(C)]
47struct FfiDataSizeCapacityRw {
48    data_ptr: *mut u8,
49    size: u32,
50    capacity: u32,
51}
52
53#[derive(Debug, Copy, Clone, Eq, PartialEq, TrivialType)]
54#[repr(u8)]
55pub enum TransactionMethodContext {
56    /// Call contract under [`Address::NULL`] context (corresponds to [`MethodContext::Reset`])
57    ///
58    /// [`Address::NULL`]: ab_core_primitives::address::Address::NULL
59    /// [`MethodContext::Reset`]: ab_contracts_common::env::MethodContext::Reset
60    Null,
61    /// Call contract under context of the wallet (corresponds to [`MethodContext::Keep`])
62    ///
63    /// [`MethodContext::Keep`]: ab_contracts_common::env::MethodContext::Keep
64    Wallet,
65}
66
67const impl TryFrom<u8> for TransactionMethodContext {
68    type Error = ();
69
70    #[inline(always)]
71    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic(const))]
72    fn try_from(value: u8) -> Result<Self, Self::Error> {
73        Ok(match value {
74            0 => Self::Null,
75            1 => Self::Wallet,
76            _ => return Err(()),
77        })
78    }
79}
80
81#[derive(Debug, Copy, Eq, PartialEq, Clone)]
82pub enum TransactionInputType {
83    Value { alignment: Alignment },
84    OutputIndex { output_index: u8 },
85}
86
87#[derive(Debug, Copy, Eq, PartialEq, Clone)]
88pub enum TransactionSlotType {
89    Address,
90    OutputIndex { output_index: u8 },
91}
92
93/// The type of transaction slot could be either explicit slot address or output index.
94///
95/// A payload declares its outputs upfront, and every `#[output]` and return value of a method
96/// writes one of them, see [`TransactionOutput`]. If [`Self::slot_type()`] returns
97/// [`TransactionSlotType::OutputIndex`], then the slot is the value that an earlier method wrote
98/// last into the declared output with `output_index`, instead of what was specified in
99/// `external_args`. This allows composing calls to multiple methods into more sophisticated
100/// workflows without writing special contracts for this.
101#[derive(Debug, Copy, Clone)]
102pub struct TransactionSlot(TransactionSlotType);
103
104impl TransactionSlot {
105    /// Explicit slot address
106    #[inline(always)]
107    pub const fn new_address() -> Self {
108        Self(TransactionSlotType::Address)
109    }
110
111    /// Output index value.
112    ///
113    /// Valid index values are below [`MAX_OUTPUTS`].
114    #[inline(always)]
115    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
116    pub const fn new_output_index(output_index: u8) -> Option<Self> {
117        if usize::from(output_index) >= MAX_OUTPUTS {
118            return None;
119        }
120
121        Some(Self(TransactionSlotType::OutputIndex { output_index }))
122    }
123
124    /// Create an instance from `u8`.
125    ///
126    /// Returns `None` for bytes that [`Self::into_u8()`] never returns, so each slot has one
127    /// encoding.
128    #[inline(always)]
129    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
130    pub const fn from_u8(n: u8) -> Option<Self> {
131        // `0b1000_0000` is an explicit slot address, while `0b0xxx_xxxx` is an output index, which
132        // must be below `MAX_OUTPUTS`
133        if n & 0b1000_0000 == 0 {
134            Self::new_output_index(n)
135        } else if n == 0b1000_0000 {
136            Some(Self(TransactionSlotType::Address))
137        } else {
138            None
139        }
140    }
141
142    /// Convert instance into `u8`
143    #[inline(always)]
144    pub const fn into_u8(self) -> u8 {
145        // The highest bit (`0b1000_0000`) is set to 1 for explicit slot address and 0 for output
146        // index
147        match self.0 {
148            TransactionSlotType::Address => 0b1000_0000,
149            TransactionSlotType::OutputIndex { output_index } => output_index,
150        }
151    }
152
153    /// Returns `Some(output_index)` or `None` if explicit slot address
154    #[inline(always)]
155    pub const fn slot_type(self) -> TransactionSlotType {
156        self.0
157    }
158}
159
160/// The type of transaction input could be either explicit value or output index.
161///
162/// A payload declares its outputs upfront, and every `#[output]` and return value of a method
163/// writes one of them, see [`TransactionOutput`]. If [`Self::input_type()`] returns
164/// [`TransactionInputType::OutputIndex`], then the input is the value that an earlier method wrote
165/// last into the declared output with `output_index`, instead of what was specified in
166/// `external_args`. This allows composing calls to multiple methods into more sophisticated
167/// workflows without writing special contracts for this.
168#[derive(Debug, Copy, Clone)]
169pub struct TransactionInput(TransactionInputType);
170
171impl TransactionInput {
172    /// Regular input value with `alignment`
173    #[inline(always)]
174    pub const fn new_value(alignment: Alignment) -> Self {
175        Self(TransactionInputType::Value { alignment })
176    }
177
178    /// Output index value.
179    ///
180    /// Valid index values are below [`MAX_OUTPUTS`].
181    #[inline(always)]
182    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
183    pub const fn new_output_index(output_index: u8) -> Option<Self> {
184        if usize::from(output_index) >= MAX_OUTPUTS {
185            return None;
186        }
187
188        Some(Self(TransactionInputType::OutputIndex { output_index }))
189    }
190
191    /// Create an instance from `u8`.
192    ///
193    /// Returns `None` for bytes that [`Self::into_u8()`] never returns, so each input has one
194    /// encoding.
195    #[inline(always)]
196    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
197    pub const fn from_u8(n: u8) -> Option<Self> {
198        // The highest bit (`0b1000_0000`) is set to 1 for value and 0 for output index, the
199        // remaining bits of a value hold the power of two of its alignment
200        if n & 0b1000_0000 == 0 {
201            return Self::new_output_index(n);
202        }
203
204        let Some(alignment) = Alignment::from_power(n & 0b0111_1111) else {
205            return None;
206        };
207
208        Some(Self(TransactionInputType::Value { alignment }))
209    }
210
211    /// Convert instance into `u8`
212    #[inline(always)]
213    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
214    pub const fn into_u8(self) -> u8 {
215        // The highest bit (`0b1000_0000`) is set to 1 for value and 0 for output index
216        match self.0 {
217            TransactionInputType::Value { alignment } => 0b1000_0000 | alignment.power,
218            TransactionInputType::OutputIndex { output_index } => output_index,
219        }
220    }
221
222    /// Returns `Some(output_index)` or `None` if regular input value
223    #[inline(always)]
224    pub const fn input_type(self) -> TransactionInputType {
225        self.0
226    }
227}
228
229#[derive(Debug, Copy, Eq, PartialEq, Clone)]
230pub enum TransactionOutputType {
231    Output { output_index: u8 },
232    ReturnValue { output_index: u8 },
233}
234
235/// The type of transaction output could be either `#[output]` or return value, with the index of
236/// the declared output that it writes.
237///
238/// A payload declares its outputs upfront, each with a capacity, and every `#[output]` and return
239/// value of a method writes one of them, which later methods can refer to, see
240/// [`TransactionInput`]. Several methods can write the same declared output one after another, and
241/// each of them replaces the value. A method writes a declared output at most once, and its slots
242/// and inputs don't refer to a declared output that it writes. The memory of every declared output
243/// is aligned to [`Alignment::MAX`], the largest alignment of any type that methods accept, so it
244/// is aligned for every output type.
245///
246/// `#[output]` is given to the method empty, whatever the memory held before, and has the size the
247/// method wrote into it. The return value doesn't have a size in `ExternalArgs`, executors only
248/// write the value, so the payload declares its size, which is the size of its type and must not
249/// exceed the capacity of the declared output.
250#[derive(Debug, Copy, Clone)]
251pub struct TransactionOutput(TransactionOutputType);
252
253impl TransactionOutput {
254    /// `#[output]` that writes the declared output with `output_index`.
255    ///
256    /// Valid index values are below [`MAX_OUTPUTS`].
257    #[inline(always)]
258    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
259    pub const fn new_output(output_index: u8) -> Option<Self> {
260        if usize::from(output_index) >= MAX_OUTPUTS {
261            return None;
262        }
263
264        Some(Self(TransactionOutputType::Output { output_index }))
265    }
266
267    /// Return value that is written into the declared output with `output_index`.
268    ///
269    /// Valid index values are below [`MAX_OUTPUTS`].
270    #[inline(always)]
271    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
272    pub const fn new_return_value(output_index: u8) -> Option<Self> {
273        if usize::from(output_index) >= MAX_OUTPUTS {
274            return None;
275        }
276
277        Some(Self(TransactionOutputType::ReturnValue { output_index }))
278    }
279
280    /// Create an instance from `u8`.
281    ///
282    /// Returns `None` for bytes that [`Self::into_u8()`] never returns, so each output has one
283    /// encoding.
284    #[inline(always)]
285    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
286    pub const fn from_u8(n: u8) -> Option<Self> {
287        let output_index = Self::tag_output_index(n);
288        // The highest bit (`0b1000_0000`) is set to 1 for output and 0 for return value
289        if n & 0b1000_0000 == 0 {
290            Self::new_return_value(output_index)
291        } else {
292            Self::new_output(output_index)
293        }
294    }
295
296    /// Index of the declared output that the output tag `n` holds, which is [`MAX_OUTPUTS`] or
297    /// more for a tag that [`Self::from_u8()`] rejects
298    #[inline(always)]
299    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
300    pub(crate) const fn tag_output_index(n: u8) -> u8 {
301        // The highest bit (`0b1000_0000`) holds the kind, the remaining bits hold the index
302        n & 0b0111_1111
303    }
304
305    /// Convert instance into `u8`
306    #[inline(always)]
307    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
308    pub const fn into_u8(self) -> u8 {
309        // The highest bit (`0b1000_0000`) is set to 1 for output and 0 for return value
310        match self.0 {
311            TransactionOutputType::Output { output_index } => 0b1000_0000 | output_index,
312            TransactionOutputType::ReturnValue { output_index } => output_index,
313        }
314    }
315
316    /// Returns whether this is `#[output]` or return value, with the index of its declared output
317    #[inline(always)]
318    pub const fn output_type(self) -> TransactionOutputType {
319        self.0
320    }
321
322    /// Whether this is `#[output]` or return value
323    #[inline(always)]
324    pub(crate) const fn kind(self) -> TransactionOutputKind {
325        match self.0 {
326            TransactionOutputType::Output { output_index: _ } => TransactionOutputKind::Output,
327            TransactionOutputType::ReturnValue { output_index: _ } => {
328                TransactionOutputKind::ReturnValue
329            }
330        }
331    }
332
333    /// Index of the declared output
334    #[inline(always)]
335    pub(crate) const fn output_index(self) -> u8 {
336        match self.0 {
337            TransactionOutputType::Output { output_index }
338            | TransactionOutputType::ReturnValue { output_index } => output_index,
339        }
340    }
341}
342
343/// Whether an output is `#[output]` or the return value
344#[derive(Debug, Copy, Clone, Eq, PartialEq)]
345pub enum TransactionOutputKind {
346    /// `#[output]`, which is given to the method empty
347    Output,
348    /// Return value, which executors write whole
349    ReturnValue,
350}
351
352/// Errors of decoding a payload with [`TransactionPayloadDecoder`] or
353/// [`TransactionPayloadCaller`], whose constructors already read the declared outputs
354///
355/// [`TransactionPayloadDecoder`]: decoder::TransactionPayloadDecoder
356#[derive(Debug, Copy, Clone, Eq, PartialEq, thiserror::Error)]
357pub enum TransactionPayloadDecoderError {
358    /// Payload too small
359    #[error("Payload too small")]
360    PayloadTooSmall,
361    /// Too many arguments
362    #[error("Too many arguments")]
363    TooManyArguments(u8),
364    /// A slot tag that [`TransactionSlot::from_u8()`] rejects
365    #[error("Invalid slot tag: {0:#010b}")]
366    InvalidSlotTag(u8),
367    /// An input tag that [`TransactionInput::from_u8()`] rejects
368    #[error("Invalid input tag: {0:#010b}")]
369    InvalidInputTag(u8),
370    /// An output tag that [`TransactionOutput::from_u8()`] rejects
371    #[error("Invalid output tag: {0:#010b}")]
372    InvalidOutputTag(u8),
373    /// A method writes an output that the payload doesn't declare, or a slot or an input refers to
374    /// an output that the payload doesn't declare or that no earlier method wrote
375    #[error("Output index not found: {0}")]
376    OutputIndexNotFound(u8),
377    /// A method writes the output with this index more than once, as `#[output]` or return value
378    #[error("Output index {0} is written more than once by the same method")]
379    OutputUsedTwice(u8),
380    /// A slot or an input refers to the output with this index, which the same method writes as
381    /// `#[output]` or return value
382    #[error("Output index {0} is referred to by the method that writes it")]
383    OutputReferencedBySameMethod(u8),
384    /// The return value with this output index is followed by another output of its method,
385    /// while a method has at most one return value, which is its last output
386    #[error("Return value with output index {0} is not the last output of its method")]
387    ReturnValueNotLast(u8),
388    /// The size that the payload declares for a return value exceeds the capacity of its output.
389    ///
390    /// The capacity is not included, so that errors fit into 8 bytes, which keeps decoding fast.
391    #[error("Return value with output index {output_index} has size {size}, more than capacity")]
392    ReturnValueTooLarge { output_index: u8, size: u32 },
393    /// An output used as a slot doesn't have the size of an address: the payload declares a
394    /// different size for a return value, or a method wrote a different size into its `#[output]`
395    #[error("Invalid output index {output_index} size for slot: {size}")]
396    InvalidSlotOutputIndexSize { output_index: u8, size: u32 },
397    /// Invalid method context
398    #[error("Invalid method context: {0}")]
399    InvalidMethodContext(u8),
400    /// The declared outputs don't fit into the output buffer of [`TransactionPayloadCaller`]
401    #[error("Output buffer too small")]
402    OutputBufferTooSmall,
403    /// The payload declares more than [`MAX_OUTPUTS`] outputs
404    #[error("Too many declared outputs: {0}")]
405    TooManyDeclaredOutputs(u8),
406    /// Decoding failed before, so the decoder can't continue
407    #[error("Decoding failed before")]
408    FailedEarlier,
409}
410
411/// Errors for [`TransactionPayloadCaller::call_next_method()`]
412#[derive(Debug, thiserror::Error)]
413pub enum TransactionPayloadCallError {
414    /// Decoder error
415    #[error("Decoder error: {0}")]
416    Decoder(TransactionPayloadDecoderError),
417    /// Method call error
418    #[error("Method call error: {0}")]
419    Method(ContractError),
420}
421
422/// Output that a payload declares, with what is known about the value that was written into it
423/// last, see [`TransactionPayloadDecoder::new()`] and [`TransactionPayloadCaller::new()`]
424///
425/// [`TransactionPayloadDecoder::new()`]: decoder::TransactionPayloadDecoder::new
426#[derive(Debug, Copy, Clone)]
427#[expect(
428    clippy::partial_pub_fields,
429    reason = "Offset, size and state are details of decoding and calling methods"
430)]
431pub struct DeclaredOutput {
432    /// Offset of the output's memory in the output buffer of [`TransactionPayloadCaller`], `0`
433    /// without an output buffer
434    offset: u32,
435    /// Capacity of the output's memory in bytes, as the payload declares it
436    pub mut(crate) capacity: u32,
437    /// Size of the value that was written last: the size that the payload declares for a return
438    /// value, which executors write whole, while `#[output]` has size `0` until its method
439    /// returned `Ok(())`, and the size it wrote after that
440    size: u32,
441    /// What was written into the output last
442    state: OutputState,
443}
444
445impl DeclaredOutput {
446    /// Record that a method writes the output as `kind`, after which it has `size`: `0` for
447    /// `#[output]`, which is given to the method empty, and the declared size for a return value
448    #[inline(always)]
449    fn set_written(&mut self, kind: TransactionOutputKind, size: u32) {
450        self.size = size;
451        self.state = match kind {
452            TransactionOutputKind::Output => OutputState::Output,
453            TransactionOutputKind::ReturnValue => OutputState::ReturnValue,
454        };
455    }
456}
457
458/// What was written into a declared output last
459#[derive(Debug, Copy, Clone, Eq, PartialEq)]
460#[repr(u8)]
461enum OutputState {
462    /// No method wrote it yet. It is `0`, which a reference checks with a single instruction.
463    NotWritten = 0,
464    /// An `#[output]` of an earlier method wrote it
465    Output = 1,
466    /// A return value of an earlier method was written into it
467    ReturnValue = 2,
468}