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}