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