Skip to main content

ab_system_contract_simple_wallet_base/payload/
decoder.rs

1//! Checking a payload and describing its methods without calling them
2
3#[cfg(test)]
4mod tests;
5
6use crate::MAX_OUTPUTS;
7use crate::payload::decoder_core::{MethodHeader, MethodSink, OutputAllocator, PayloadDecoderCore};
8use crate::payload::{
9    DeclaredOutput, TransactionMethodContext, TransactionOutputKind, TransactionPayloadDecoderError,
10};
11use ab_contracts_common::MAX_TOTAL_METHOD_ARGS;
12use ab_contracts_common::method::MethodFingerprint;
13use ab_core_primitives::address::Address;
14use ab_io_type::alignment::Alignment;
15use core::hint;
16use core::mem::MaybeUninit;
17
18/// Reference from a slot or an input to the value that an earlier method wrote last into a declared
19/// output
20#[derive(Debug, Copy, Clone, Eq, PartialEq)]
21#[expect(
22    clippy::partial_pub_fields,
23    reason = "Offset and size are details of calling methods, see `Self::return_value_size()`"
24)]
25pub struct OutputRef {
26    /// Index of the declared output
27    pub mut(crate) output_index: u8,
28    /// Whether the value was written as `#[output]` or as a return value
29    pub mut(crate) kind: TransactionOutputKind,
30    /// Offset of the output's memory, see [`DeclaredOutput`]
31    pub(crate) offset: u32,
32    /// Size of the value, see [`DeclaredOutput`]
33    pub(crate) size: u32,
34}
35
36impl OutputRef {
37    /// Size of a return value, which the payload declares.
38    ///
39    /// `None` for `#[output]`, whose size is only known after its method was called.
40    #[inline(always)]
41    pub const fn return_value_size(&self) -> Option<u32> {
42        match self.kind {
43            TransactionOutputKind::Output => None,
44            TransactionOutputKind::ReturnValue => Some(self.size),
45        }
46    }
47}
48
49/// `#[slot]` argument
50#[derive(Debug, Copy, Clone, Eq, PartialEq)]
51pub enum DecodedSlot<'payload> {
52    /// Explicit address in the payload
53    Address(&'payload Address),
54    /// Value that an earlier method wrote into a declared output. A return value is declared with
55    /// the size of an address.
56    Output(OutputRef),
57}
58
59/// `#[input]` argument
60#[derive(Debug, Copy, Clone, Eq, PartialEq)]
61pub enum DecodedInput<'payload> {
62    /// Explicit value in the payload, whose bytes are aligned to `alignment`
63    Value {
64        bytes: &'payload [u8],
65        alignment: Alignment,
66    },
67    /// Value that an earlier method wrote into a declared output
68    Output(OutputRef),
69}
70
71/// `#[output]` or return value, which writes a declared output
72#[derive(Debug, Copy, Clone, Eq, PartialEq)]
73#[expect(
74    clippy::partial_pub_fields,
75    reason = "The size is only meaningful for a return value, see `Self::return_value_size()`"
76)]
77pub struct DecodedOutput {
78    /// Index of the declared output that this `#[output]` or return value writes, which later
79    /// methods refer to
80    pub mut(crate) output_index: u8,
81    /// Whether the output is `#[output]` or a return value
82    pub mut(crate) kind: TransactionOutputKind,
83    /// Capacity of the declared output's memory, whose alignment is [`Alignment::MAX`]
84    pub mut(crate) capacity: u32,
85    /// Size that the method gets: `0` for `#[output]`, the size that the payload declares for the
86    /// return value
87    pub(crate) size: u32,
88}
89
90impl DecodedOutput {
91    /// Size of a return value as the payload declares it, the size of its type, since executors
92    /// write the whole value.
93    ///
94    /// `None` for `#[output]`, which the method gets empty.
95    #[inline(always)]
96    pub const fn return_value_size(&self) -> Option<u32> {
97        match self.kind {
98            TransactionOutputKind::Output => None,
99            TransactionOutputKind::ReturnValue => Some(self.size),
100        }
101    }
102}
103
104/// Placeholder for unused entries of [`DecodedArguments::slots`]
105const UNUSED_SLOT: DecodedSlot<'static> = DecodedSlot::Address(&Address::NULL);
106/// Placeholder for unused entries of [`DecodedArguments::inputs`]
107const UNUSED_INPUT: DecodedInput<'static> = DecodedInput::Value {
108    bytes: &[],
109    alignment: Alignment::of::<u8>(),
110};
111/// Placeholder for unused entries of [`DecodedArguments::outputs`]
112const UNUSED_OUTPUT: DecodedOutput = DecodedOutput {
113    output_index: 0,
114    kind: TransactionOutputKind::Output,
115    capacity: 0,
116    size: 0,
117};
118
119/// Arguments of a method, which are collected as they are decoded
120#[derive(Debug)]
121struct DecodedArguments<'payload> {
122    /// Entries before `num_slots` are arguments of the method
123    slots: [DecodedSlot<'payload>; MAX_TOTAL_METHOD_ARGS as usize],
124    num_slots: u8,
125    /// Entries before `num_inputs` are arguments of the method
126    inputs: [DecodedInput<'payload>; MAX_TOTAL_METHOD_ARGS as usize],
127    num_inputs: u8,
128    /// Entries before `num_outputs` are arguments of the method
129    outputs: [DecodedOutput; MAX_TOTAL_METHOD_ARGS as usize],
130    num_outputs: u8,
131}
132
133// The core gives at most `MAX_TOTAL_METHOD_ARGS` arguments per method to the sink, so there is
134// always a free entry
135impl<'payload> MethodSink<'payload> for DecodedArguments<'payload> {
136    #[inline(always)]
137    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
138    fn slot(&mut self, slot: DecodedSlot<'payload>) -> Result<(), TransactionPayloadDecoderError> {
139        if let Some(entry) = self.slots.get_mut(usize::from(self.num_slots)) {
140            *entry = slot;
141            self.num_slots += 1;
142        }
143
144        Ok(())
145    }
146
147    #[inline(always)]
148    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
149    fn input(&mut self, input: DecodedInput<'payload>) {
150        if let Some(entry) = self.inputs.get_mut(usize::from(self.num_inputs)) {
151            *entry = input;
152            self.num_inputs += 1;
153        }
154    }
155
156    #[inline(always)]
157    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
158    fn output(&mut self, output: DecodedOutput, _offset: u32) {
159        if let Some(entry) = self.outputs.get_mut(usize::from(self.num_outputs)) {
160            *entry = output;
161            self.num_outputs += 1;
162        }
163    }
164}
165
166/// Allocates declared outputs for [`TransactionPayloadDecoder`], which has no output buffer, so
167/// every output has offset `0`
168struct NoOutputBuffer;
169
170impl OutputAllocator for NoOutputBuffer {
171    #[inline(always)]
172    fn alloc(&mut self, _capacity: u32) -> Result<u32, TransactionPayloadDecoderError> {
173        Ok(0)
174    }
175}
176
177impl<'payload> DecodedArguments<'payload> {
178    #[inline(always)]
179    fn new() -> Self {
180        Self {
181            slots: [UNUSED_SLOT; MAX_TOTAL_METHOD_ARGS as usize],
182            num_slots: 0,
183            inputs: [UNUSED_INPUT; MAX_TOTAL_METHOD_ARGS as usize],
184            num_inputs: 0,
185            outputs: [UNUSED_OUTPUT; MAX_TOTAL_METHOD_ARGS as usize],
186            num_outputs: 0,
187        }
188    }
189
190    /// Forget the arguments of the previous method
191    #[inline(always)]
192    fn clear(&mut self) {
193        self.num_slots = 0;
194        self.num_inputs = 0;
195        self.num_outputs = 0;
196    }
197
198    /// Slots of the method
199    #[inline(always)]
200    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
201    fn slots(&self) -> &[DecodedSlot<'payload>] {
202        // SAFETY: The fields are private to this module, where `num_slots` is only set to `0` and
203        // incremented by `Self::slot()` after it wrote an entry before the end of `slots`, so it
204        // never exceeds the length of `slots`
205        unsafe {
206            hint::assert_unchecked(usize::from(self.num_slots) <= self.slots.len());
207        }
208
209        self.slots
210            .get(..usize::from(self.num_slots))
211            .expect("Within the length of `slots`, as asserted above; qed")
212    }
213
214    /// Inputs of the method
215    #[inline(always)]
216    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
217    fn inputs(&self) -> &[DecodedInput<'payload>] {
218        // SAFETY: The fields are private to this module, where `num_inputs` is only set to `0` and
219        // incremented by `Self::input()` after it wrote an entry before the end of `inputs`, so it
220        // never exceeds the length of `inputs`
221        unsafe {
222            hint::assert_unchecked(usize::from(self.num_inputs) <= self.inputs.len());
223        }
224
225        self.inputs
226            .get(..usize::from(self.num_inputs))
227            .expect("Within the length of `inputs`, as asserted above; qed")
228    }
229
230    /// Outputs of the method
231    #[inline(always)]
232    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
233    fn outputs(&self) -> &[DecodedOutput] {
234        // SAFETY: The fields are private to this module, where `num_outputs` is only set to `0`
235        // and incremented by `Self::output()` after it wrote an entry before the end of `outputs`,
236        // so it never exceeds the length of `outputs`
237        unsafe {
238            hint::assert_unchecked(usize::from(self.num_outputs) <= self.outputs.len());
239        }
240
241        self.outputs
242            .get(..usize::from(self.num_outputs))
243            .expect("Within the length of `outputs`, as asserted above; qed")
244    }
245}
246
247/// One method of the payload, see [`TransactionPayloadDecoder::next_method()`].
248///
249/// Arguments of each kind are in payload order, which is the order in which the method declares
250/// them. So the `n`-th slot is the `n`-th `#[slot]` in the method's metadata, and the same holds
251/// for inputs and outputs. A method has at most one return value, which is its last output, as
252/// the decoder checks. The state that `#[init]` returns is an `#[output]` (often with capacity
253/// `0`), not a return value, since executors store it instead of writing it.
254#[derive(Debug, Copy, Clone)]
255pub struct DecodedMethod<'decoder, 'payload> {
256    header: MethodHeader<'payload>,
257    arguments: &'decoder DecodedArguments<'payload>,
258}
259
260impl<'decoder, 'payload> DecodedMethod<'decoder, 'payload> {
261    /// Contract to call
262    #[inline(always)]
263    pub fn contract(&self) -> &'payload Address {
264        self.header.contract
265    }
266
267    /// Fingerprint of the method to call
268    #[inline(always)]
269    pub fn fingerprint(&self) -> &'payload MethodFingerprint {
270        self.header.fingerprint
271    }
272
273    /// Context to call the method with
274    #[inline(always)]
275    pub fn context(&self) -> TransactionMethodContext {
276        self.header.context
277    }
278
279    /// `#[slot]` arguments
280    #[inline(always)]
281    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
282    pub fn slots(&self) -> &'decoder [DecodedSlot<'payload>] {
283        self.arguments.slots()
284    }
285
286    /// `#[input]` arguments
287    #[inline(always)]
288    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
289    pub fn inputs(&self) -> &'decoder [DecodedInput<'payload>] {
290        self.arguments.inputs()
291    }
292
293    /// `#[output]` arguments and the return value
294    #[inline(always)]
295    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
296    pub fn outputs(&self) -> &'decoder [DecodedOutput] {
297        self.arguments.outputs()
298    }
299
300    /// The return value, which is the last output if there is one
301    #[inline(always)]
302    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
303    pub fn return_value(&self) -> Option<&'decoder DecodedOutput> {
304        self.outputs()
305            .last()
306            .filter(|output| output.kind == TransactionOutputKind::ReturnValue)
307    }
308}
309
310/// Decoder that checks a payload created using `TransactionPayloadBuilder` and describes its
311/// methods without calling them, for example to show a transaction to the user before it is
312/// signed.
313///
314/// It runs the same decoding code as [`TransactionPayloadCaller`], so it applies every rule that
315/// depends only on the payload. A payload that it accepts can still be rejected by the caller in
316/// two cases, which depend on execution:
317/// * the declared outputs don't fit into the output buffer of the caller, which allocates each of
318///   them at a multiple of 16 bytes ([`Alignment::MAX`]) after the previous one: they fit if the
319///   capacities of [`Self::declared_outputs()`], each except the last rounded up to a multiple of
320///   16, add up to at most the size of the output buffer (32 KiB in
321///   [`SimpleWalletBase::execute()`], see [`OUTPUT_BUFFER_SIZE`]); the caller checks this when it
322///   is created, before it calls any method
323/// * an `#[output]` that a later method uses as a slot doesn't hold an address after its method was
324///   called; the caller checks this when it reaches that slot, so when the caller rejects a payload
325///   for it, the decoder either accepts the payload or rejects it with an error from later in the
326///   payload
327///
328/// A payload can declare outputs that no method writes. The decoder and the caller accept it, the
329/// memory of such an output is allocated but never used, while `TransactionPayloadBuilder` never
330/// creates such a payload.
331///
332/// Descriptions contain no pointers into memory other than the payload, so any payload can be
333/// decoded safely.
334///
335/// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
336/// [`SimpleWalletBase::execute()`]: crate::SimpleWalletBase::execute
337/// [`OUTPUT_BUFFER_SIZE`]: crate::OUTPUT_BUFFER_SIZE
338#[derive(Debug)]
339pub struct TransactionPayloadDecoder<'payload, 'outputs> {
340    core: PayloadDecoderCore<'payload, 'outputs>,
341    /// Arguments of the method that was decoded last
342    arguments: DecodedArguments<'payload>,
343    /// The core must not be used anymore after an error
344    failed: bool,
345}
346
347impl<'payload, 'outputs> TransactionPayloadDecoder<'payload, 'outputs> {
348    /// Create a new instance and read the outputs that the payload declares, at most
349    /// [`MAX_OUTPUTS`], into `declared_outputs`.
350    ///
351    /// Returns an error if the outputs that the payload declares can't be read. Every payload
352    /// starts with them, so an empty payload is too small.
353    #[inline]
354    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
355    pub fn new(
356        payload: &'payload [u128],
357        declared_outputs: &'outputs mut [MaybeUninit<DeclaredOutput>; MAX_OUTPUTS],
358    ) -> Result<Self, TransactionPayloadDecoderError> {
359        Ok(Self {
360            core: PayloadDecoderCore::new(payload, declared_outputs, &mut NoOutputBuffer)?,
361            arguments: DecodedArguments::new(),
362            failed: false,
363        })
364    }
365
366    /// Outputs that the payload declares, by output index
367    #[inline]
368    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
369    pub fn declared_outputs(&self) -> &[DeclaredOutput] {
370        self.core.declared_outputs()
371    }
372
373    /// Decode and check the next method, `None` if there are no more methods in the payload.
374    ///
375    /// The method is returned only after all of it was checked. Its arguments are stored in the
376    /// decoder and replaced by the next call. After an error, every call returns
377    /// [`TransactionPayloadDecoderError::FailedEarlier`].
378    #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
379    pub fn next_method(
380        &mut self,
381    ) -> Result<Option<DecodedMethod<'_, 'payload>>, TransactionPayloadDecoderError> {
382        if self.failed {
383            return Err(TransactionPayloadDecoderError::FailedEarlier);
384        }
385
386        self.arguments.clear();
387        match self.core.decode_next_method(&mut self.arguments) {
388            Ok(Some(header)) => Ok(Some(DecodedMethod {
389                header,
390                arguments: &self.arguments,
391            })),
392            Ok(None) => Ok(None),
393            Err(error) => {
394                self.failed = true;
395                Err(error)
396            }
397        }
398    }
399}