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}