ab_system_contract_simple_wallet_base/payload/builder.rs
1//! Transaction payload creation utilities
2
3#[cfg(test)]
4mod tests;
5
6use crate::MAX_OUTPUTS;
7use crate::payload::decoder_core::{output_ref, slot_output_ref, writes_output};
8use crate::payload::{
9 DeclaredOutput, FfiDataSizeCapacityRo, OutputState, TransactionInput, TransactionMethodContext,
10 TransactionOutput, TransactionOutputKind, TransactionPayloadDecoderError, TransactionSlot,
11};
12use ab_aligned_buffer::OwnedAlignedBuffer;
13use ab_contracts_common::MAX_TOTAL_METHOD_ARGS;
14use ab_contracts_common::metadata::decode::{
15 ArgumentKind, MetadataDecodingError, MethodKind, MethodMetadataDecoder, MethodMetadataItem,
16 MethodsContainerKind,
17};
18use ab_contracts_common::method::{ExternalArgs, MethodFingerprint};
19use ab_core_primitives::address::Address;
20use ab_io_type::alignment::Alignment;
21use ab_io_type::metadata::IoTypeDetails;
22use ab_io_type::trivial_type::TrivialType;
23use core::ffi::c_void;
24use core::mem::MaybeUninit;
25use core::ptr::NonNull;
26use core::{hint, slice};
27
28const {
29 // Make sure bit flags for all arguments will fit into a single u8 below
30 assert!(MAX_TOTAL_METHOD_ARGS as u32 == u8::BITS);
31}
32
33/// Read from an external arguments pointer and move it forward.
34///
35/// # Safety
36/// `external_args` must have enough capacity for the read value, and the current offset must
37/// have the correct alignment for the type being read.
38#[inline(always)]
39#[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
40unsafe fn read_external_args<T>(external_args: &mut NonNull<c_void>) -> T {
41 // SAFETY: guaranteed by this function signature
42 unsafe {
43 let value = external_args.cast::<T>().read();
44 *external_args = external_args.byte_add(size_of::<T>());
45 value
46 }
47}
48
49/// Errors for [`TransactionPayloadBuilder`]
50#[derive(Debug, thiserror::Error)]
51pub enum TransactionPayloadBuilderError<'a> {
52 /// Metadata decoding error
53 #[error("Metadata decoding error: {0}")]
54 MetadataDecodingError(MetadataDecodingError<'a>),
55 /// Too many arguments
56 #[error("Too many arguments")]
57 TooManyArguments(u8),
58 /// An output index of [`MAX_OUTPUTS`] or more, which tags can't hold
59 #[error("Invalid output index: {0}")]
60 InvalidOutputIndex(u8),
61 /// The payload doesn't fit into the maximum size given to [`TransactionPayloadBuilder::new()`]
62 #[error("The payload is too large")]
63 PayloadTooLarge,
64 /// More than [`MAX_OUTPUTS`] outputs declared
65 #[error("Too many outputs")]
66 TooManyOutputs,
67 /// [`MethodCallReferences::outputs`] doesn't have exactly one entry for each output of the
68 /// method
69 #[error("The method has {expected} outputs, but {actual} output indices are given")]
70 NumberOfOutputsMismatch { expected: u8, actual: usize },
71 /// [`MethodCallReferences::slots`] has more entries than the method has slots
72 #[error("The method has {num_slots} slots, but {num_references} slot references are given")]
73 TooManySlotReferences {
74 num_slots: u8,
75 num_references: usize,
76 },
77 /// [`MethodCallReferences::inputs`] has more entries than the method has inputs
78 #[error("The method has {num_inputs} inputs, but {num_references} input references are given")]
79 TooManyInputReferences {
80 num_inputs: u8,
81 num_references: usize,
82 },
83 /// No method call writes the declared output with this index, see
84 /// [`TransactionPayloadBuilder::into_aligned_bytes()`]
85 #[error("No method call writes the output with output index {0}")]
86 UnusedOutput(u8),
87 /// The method call breaks a rule of the payload format, with the error that
88 /// [`TransactionPayloadDecoder`] returns for such a payload, see
89 /// [`TransactionPayloadBuilder::with_method_call()`]
90 ///
91 /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
92 #[error("The method call breaks a rule of the payload format: {0}")]
93 PayloadRule(TransactionPayloadDecoderError),
94}
95
96/// Declared output that no method call wrote yet, with capacity `0`
97const UNWRITTEN_OUTPUT: DeclaredOutput = DeclaredOutput {
98 offset: 0,
99 capacity: 0,
100 size: 0,
101 state: OutputState::NotWritten,
102};
103
104/// References of a method call to declared outputs, by position among the method's arguments of
105/// each kind, see [`TransactionPayloadBuilder::with_method_call()`]
106#[derive(Debug, Default, Copy, Clone)]
107pub struct MethodCallReferences<'a> {
108 /// Output index for each `#[slot]`, whose value the slot gets instead of an explicit address.
109 /// `None` or no entry for an explicit address.
110 pub slots: &'a [Option<u8>],
111 /// Output index for each `#[input]`, whose value the input gets instead of a value in the
112 /// payload. `None` or no entry for a value in the payload.
113 pub inputs: &'a [Option<u8>],
114 /// Output index for each `#[output]` and the return value (last), which they write. Exactly
115 /// one entry for each output of the method.
116 pub outputs: &'a [u8],
117}
118
119/// Builder for payload to be used with [`TxHandler`] (primarily for [`SimpleWallet`]).
120///
121/// The number of outputs that the payload declares is given to [`Self::new()`]. Method calls,
122/// added with [`Self::with_method_call()`], write these outputs and refer to them by output index,
123/// and the builder computes the capacity of every declared output from the method calls that write
124/// it.
125///
126/// Methods in the payload are decoded and called with [`TransactionPayloadCaller`], and can be
127/// described without calling them with [`TransactionPayloadDecoder`].
128///
129/// [`TxHandler`]: ab_contracts_standards::tx_handler::TxHandler
130/// [`SimpleWallet`]: crate::SimpleWalletBase
131/// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
132/// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
133#[derive(Debug, Clone)]
134pub struct TransactionPayloadBuilder {
135 /// Starts with the header, which declares the outputs, with a capacity for each of them that
136 /// method calls raise, see [`Self::into_aligned_bytes()`]
137 payload: OwnedAlignedBuffer,
138 /// Entries before `num_outputs` are the outputs that the payload declares, with their
139 /// capacities and what the method calls added so far wrote into them last, as decoders see
140 /// them after those method calls
141 declared_outputs: [DeclaredOutput; MAX_OUTPUTS],
142 /// Number of outputs that the payload declares, at most [`MAX_OUTPUTS`]
143 num_outputs: u8,
144}
145
146impl TransactionPayloadBuilder {
147 /// Create a new instance for a payload of up to `max_size` bytes that declares `num_outputs`
148 /// outputs.
149 ///
150 /// Memory for the whole payload is allocated here and never reallocated, a method call that
151 /// doesn't fit returns [`TransactionPayloadBuilderError::PayloadTooLarge`]. The payload is
152 /// padded to a multiple of [`Alignment::MAX`], so `max_size` is rounded down to a multiple of
153 /// it.
154 ///
155 /// `#[output]` arguments and return values of method calls write the declared outputs, and
156 /// slots and inputs of later method calls can refer to what was written last, by output index
157 /// from `0` to `num_outputs - 1`, see [`MethodCallReferences`]. The header of the payload has
158 /// room for the capacity of every declared output, which starts at `0`, and every method call
159 /// that writes an output raises its capacity to what the method needs, so capacities don't
160 /// have to be known upfront. The memory of every declared output is aligned for every output
161 /// type. [`SimpleWalletBase::execute()`] only calls the methods of a payload whose declared
162 /// outputs fit into [`OUTPUT_BUFFER_SIZE`] together, see [`TransactionPayloadDecoder`].
163 ///
164 /// The header is written right away, so a payload without method calls is the header.
165 ///
166 /// Returns an error if `num_outputs` is larger than [`MAX_OUTPUTS`] or the header doesn't fit
167 /// into `max_size`.
168 ///
169 /// [`SimpleWalletBase::execute()`]: crate::SimpleWalletBase::execute
170 /// [`OUTPUT_BUFFER_SIZE`]: crate::OUTPUT_BUFFER_SIZE
171 /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
172 // TODO: Not annotated with `no_panic`, since allocating the payload can panic
173 pub fn new(
174 max_size: u32,
175 num_outputs: u8,
176 ) -> Result<Self, TransactionPayloadBuilderError<'static>> {
177 if usize::from(num_outputs) > MAX_OUTPUTS {
178 return Err(TransactionPayloadBuilderError::TooManyOutputs);
179 }
180
181 let mut builder = Self {
182 payload: OwnedAlignedBuffer::with_capacity(
183 max_size / u32::from(Alignment::MAX.get()) * u32::from(Alignment::MAX.get()),
184 ),
185 declared_outputs: [UNWRITTEN_OUTPUT; MAX_OUTPUTS],
186 num_outputs,
187 };
188 builder.push_payload_byte(num_outputs)?;
189 // Method calls that write an output raise its capacity
190 for _ in 0..num_outputs {
191 builder.extend_payload_with_alignment(&0u32.to_le_bytes(), Alignment::of::<u32>())?;
192 }
193
194 Ok(builder)
195 }
196
197 /// Add method call to the payload.
198 ///
199 /// The wallet will call this method in addition order.
200 ///
201 /// `references` refer to outputs that the payload declares (see [`Self::new()`]): the output
202 /// index for each `#[slot]` and `#[input]` that gets the value that an earlier method call
203 /// wrote last into that output instead of the value in `external_args`, and the output index
204 /// that each `#[output]` and the return value write. See [`TransactionInput`] for more details.
205 /// The capacity of every output that the method call writes is raised to the recommended
206 /// capacity of the `#[output]` or the size of the return value if it is smaller.
207 ///
208 /// Every output index must be below [`MAX_OUTPUTS`]
209 /// ([`TransactionPayloadBuilderError::InvalidOutputIndex`]). The method call is also checked
210 /// against the rules of the payload format about outputs, mostly with the code that decoders
211 /// use, so that [`TransactionPayloadDecoder`] accepts the payload, and a broken rule is
212 /// returned as [`TransactionPayloadBuilderError::PayloadRule`] with the error that the
213 /// decoder returns for it, in the order in which the decoder checks the rules:
214 /// * a slot or an input only refers to a declared output that an earlier method call wrote
215 /// ([`TransactionPayloadDecoderError::OutputIndexNotFound`]) and that this method call
216 /// doesn't write ([`TransactionPayloadDecoderError::OutputReferencedBySameMethod`])
217 /// * a slot only refers to a return value with the size of an address
218 /// ([`TransactionPayloadDecoderError::InvalidSlotOutputIndexSize`]); an `#[output]` used as a
219 /// slot gets its size when its method is called, so [`TransactionPayloadCaller`] checks it
220 /// * the method call only writes declared outputs
221 /// ([`TransactionPayloadDecoderError::OutputIndexNotFound`]), each at most once
222 /// ([`TransactionPayloadDecoderError::OutputUsedTwice`])
223 ///
224 /// The other rules about outputs hold by construction: the return value is the last output,
225 /// and every output gets a capacity that is enough for every method call that writes it.
226 ///
227 /// Outputs are given to the method empty, with size `0`, whatever an earlier method call wrote
228 /// into them, so methods called from transactions must accept empty outputs, like
229 /// [`MaybeData`] and [`VariableElements`]. A method with an `#[output]` of a [`TrivialType`]
230 /// can't be called directly from a transaction, since its value has to be initialized first,
231 /// for example by a separate method. Initializing values is domain-specific, so the payload
232 /// format doesn't do it. Executors that check arguments against the method's metadata reject
233 /// such calls, while the native executor trusts transactions, so with it such a call violates
234 /// the requirements of the called method.
235 ///
236 /// The return value is given the size of its type, since executors write the whole value, so
237 /// later method calls can refer to it as an input or a slot. The state returned by an `#[init]`
238 /// method is the exception: executors store it as the state of the contract instead of writing
239 /// it to the caller, so it is encoded as an `#[output]` that needs no capacity, like the state
240 /// in the last `#[output]` of `#[init]` without a return value, and later method calls
241 /// referring to it get it empty.
242 ///
243 /// On error, the builder is left unchanged, so other method calls can still be added.
244 ///
245 /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
246 /// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
247 /// [`MaybeData`]: ab_io_type::maybe_data::MaybeData
248 /// [`VariableElements`]: ab_io_type::variable_elements::VariableElements
249 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
250 pub fn with_method_call<Args>(
251 &mut self,
252 contract: &Address,
253 external_args: &Args,
254 method_context: TransactionMethodContext,
255 references: MethodCallReferences<'_>,
256 ) -> Result<(), TransactionPayloadBuilderError<'static>>
257 where
258 Args: ExternalArgs,
259 {
260 let external_args = NonNull::from_ref(external_args).cast::<*const c_void>();
261
262 // SAFETY: Called with statically valid data
263 unsafe {
264 self.with_method_call_untyped(
265 contract,
266 external_args,
267 Args::METADATA,
268 &Args::FINGERPRINT,
269 method_context,
270 references,
271 )
272 }
273 }
274
275 /// Other than unsafe API, this method is identical to [`Self::with_method_call()`].
276 ///
277 /// # Safety
278 /// `external_args` must correspond to `method_metadata` and `method_fingerprint`. Outputs are
279 /// never read from `external_args`. The pointers of slots and inputs that refer to declared
280 /// outputs in `references` are never dereferenced, so they may dangle, but they must not be
281 /// null, since external arguments hold every pointer as [`NonNull`].
282 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
283 pub unsafe fn with_method_call_untyped<'a>(
284 &mut self,
285 contract: &Address,
286 external_args: NonNull<*const c_void>,
287 method_metadata: &'a [u8],
288 method_fingerprint: &MethodFingerprint,
289 method_context: TransactionMethodContext,
290 references: MethodCallReferences<'_>,
291 ) -> Result<(), TransactionPayloadBuilderError<'a>> {
292 let payload_len = self.payload.len();
293 // SAFETY: Guaranteed by this function's contract
294 let result = unsafe {
295 self.add_method_call_untyped(
296 contract,
297 external_args,
298 method_metadata,
299 method_fingerprint,
300 method_context,
301 references,
302 )
303 };
304 if result.is_err() {
305 // Remove the part of a method call that didn't fit, declared outputs weren't changed
306 // SAFETY: The payload only grows, so its first `payload_len` bytes are still
307 // initialized
308 unsafe {
309 self.payload.set_len(payload_len);
310 }
311 }
312
313 result
314 }
315
316 /// Add the method call to the payload, see [`Self::with_method_call_untyped()`].
317 ///
318 /// Errors other than [`TransactionPayloadBuilderError::PayloadTooLarge`] are returned before
319 /// anything is changed. After that error, part of the method call might have been added to
320 /// the payload, while declared outputs are only changed after the whole method call was added.
321 ///
322 /// # Safety
323 /// The same as for [`Self::with_method_call_untyped()`].
324 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
325 unsafe fn add_method_call_untyped<'a>(
326 &mut self,
327 contract: &Address,
328 external_args: NonNull<*const c_void>,
329 mut method_metadata: &'a [u8],
330 method_fingerprint: &MethodFingerprint,
331 method_context: TransactionMethodContext,
332 references: MethodCallReferences<'_>,
333 ) -> Result<(), TransactionPayloadBuilderError<'a>> {
334 let external_args = &mut external_args.cast::<c_void>();
335 let MethodCallReferences {
336 slots: slot_output_indices,
337 inputs: input_output_indices,
338 outputs: output_indices,
339 } = references;
340
341 let (metadata_decoder, method_metadata_item) =
342 MethodMetadataDecoder::new(&mut method_metadata, MethodsContainerKind::Unknown)
343 .decode_next()
344 .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?;
345 let mut metadata_decoder = metadata_decoder.without_auto_drain();
346
347 #[expect(
348 clippy::rest_pattern_accessible_field,
349 reason = "Do not need other fields"
350 )]
351 let MethodMetadataItem {
352 method_kind,
353 num_arguments,
354 ..
355 } = method_metadata_item;
356 let number_of_arguments = num_arguments.saturating_add(u8::from(method_kind.has_self()));
357
358 if number_of_arguments > MAX_TOTAL_METHOD_ARGS {
359 return Err(TransactionPayloadBuilderError::TooManyArguments(
360 number_of_arguments,
361 ));
362 }
363
364 let mut num_slot_arguments = 0u8;
365 let mut num_input_arguments = 0u8;
366 let mut num_output_arguments = 0u8;
367 // Offset of the return value among outputs and its size if executors write it to the
368 // caller
369 let mut return_value_offset = None;
370 let mut return_value_size = None;
371
372 let mut input_output_type_details =
373 [MaybeUninit::<IoTypeDetails>::uninit(); MAX_TOTAL_METHOD_ARGS as usize];
374 // Collect information about all arguments, so that everything except whether the method
375 // call fits is checked before the payload is changed
376 while let Some(item) = metadata_decoder
377 .decode_next()
378 .transpose()
379 .map_err(TransactionPayloadBuilderError::MetadataDecodingError)?
380 {
381 match item.argument_kind {
382 ArgumentKind::EnvRo
383 | ArgumentKind::EnvRw
384 | ArgumentKind::TmpRo
385 | ArgumentKind::TmpRw => {
386 // Not represented in external args
387 }
388 ArgumentKind::SlotRo | ArgumentKind::SlotRw => {
389 num_slot_arguments += 1;
390 }
391 ArgumentKind::Input => {
392 // SAFETY: The counters only count arguments decoded before this one, so they
393 // and their sum are below `num_arguments`, the number of items that the
394 // metadata decoder returns (see `MethodMetadataItem::num_arguments`). The check
395 // of the number of arguments above limits `num_arguments` to
396 // `MAX_TOTAL_METHOD_ARGS`.
397 unsafe {
398 hint::assert_unchecked(num_input_arguments < MAX_TOTAL_METHOD_ARGS);
399 }
400 input_output_type_details[usize::from(num_input_arguments)]
401 .write(item.type_details.unwrap_or(IoTypeDetails::bytes(0)));
402 num_input_arguments += 1;
403 }
404 ArgumentKind::Output | ArgumentKind::Return => {
405 // The state of `#[init]` has no type details, so it needs no capacity
406 let type_details = item.type_details.unwrap_or(IoTypeDetails::bytes(0));
407 // The return value is the last argument (see
408 // `ArgumentsMetadataDecoder::decode_next()`), so it is also the last output. In
409 // `#[init]` it is the state, which executors store instead of writing it to the
410 // caller, so it is encoded as an output then, like the state in the last
411 // `#[output]` of `#[init]` without a return value.
412 if matches!(item.argument_kind, ArgumentKind::Return)
413 && !matches!(method_kind, MethodKind::Init)
414 {
415 return_value_offset = Some(usize::from(num_output_arguments));
416 // The size of the return value is the size of its type, which is its
417 // recommended capacity
418 return_value_size = Some(type_details.recommended_capacity);
419 }
420 // SAFETY: Same as for inputs above
421 unsafe {
422 hint::assert_unchecked(
423 num_input_arguments + num_output_arguments < MAX_TOTAL_METHOD_ARGS,
424 );
425 }
426 // After all inputs, which come before outputs
427 input_output_type_details
428 [usize::from(num_input_arguments + num_output_arguments)]
429 .write(type_details);
430 num_output_arguments += 1;
431 }
432 }
433 }
434 // SAFETY: As for the writes above, the counters and their sum are at most `num_arguments`,
435 // so they stay within the `MAX_TOTAL_METHOD_ARGS` elements of the array. The metadata
436 // decoder returns all inputs before outputs (see
437 // `ArgumentsMetadataDecoder::decode_next()`), so the first `num_input_arguments` elements
438 // were initialized above with inputs and the next `num_output_arguments` ones with
439 // outputs.
440 let (input_type_details, output_type_details) = unsafe {
441 let (input_type_details, output_type_details) =
442 input_output_type_details.split_at_unchecked(usize::from(num_input_arguments));
443 let output_type_details =
444 output_type_details.get_unchecked(..usize::from(num_output_arguments));
445
446 (
447 input_type_details.assume_init_ref(),
448 output_type_details.assume_init_ref(),
449 )
450 };
451
452 if slot_output_indices.len() > usize::from(num_slot_arguments) {
453 return Err(TransactionPayloadBuilderError::TooManySlotReferences {
454 num_slots: num_slot_arguments,
455 num_references: slot_output_indices.len(),
456 });
457 }
458 if input_output_indices.len() > usize::from(num_input_arguments) {
459 return Err(TransactionPayloadBuilderError::TooManyInputReferences {
460 num_inputs: num_input_arguments,
461 num_references: input_output_indices.len(),
462 });
463 }
464 if output_indices.len() != usize::from(num_output_arguments) {
465 return Err(TransactionPayloadBuilderError::NumberOfOutputsMismatch {
466 expected: num_output_arguments,
467 actual: output_indices.len(),
468 });
469 }
470 for &output_index in slot_output_indices
471 .iter()
472 .chain(input_output_indices)
473 .flatten()
474 .chain(output_indices)
475 {
476 if usize::from(output_index) >= MAX_OUTPUTS {
477 return Err(TransactionPayloadBuilderError::InvalidOutputIndex(
478 output_index,
479 ));
480 }
481 }
482
483 // `TransactionOutput` for each output, the return value last
484 let mut output_tags = [0; MAX_TOTAL_METHOD_ARGS as usize];
485 for ((output_offset, &output_index), output_tag) in
486 output_indices.iter().enumerate().zip(&mut output_tags)
487 {
488 let output = if return_value_offset == Some(output_offset) {
489 TransactionOutput::new_return_value(output_index)
490 } else {
491 TransactionOutput::new_output(output_index)
492 }
493 .ok_or(TransactionPayloadBuilderError::InvalidOutputIndex(
494 output_index,
495 ))?;
496 *output_tag = output.into_u8();
497 }
498 // SAFETY: `num_output_arguments` is at most `MAX_TOTAL_METHOD_ARGS`, as for the details of
499 // outputs above, and the loop above wrote a tag for each of the as many output indices
500 let output_tags = unsafe { output_tags.get_unchecked(..usize::from(num_output_arguments)) };
501
502 // The rules of the payload format about outputs, checked in the same order as decoders do,
503 // with `declared_outputs` in the state that decoders have after the earlier method calls
504 for &output_index in slot_output_indices.iter().flatten() {
505 slot_output_ref(self.declared_outputs(), output_index, output_tags)
506 .map_err(TransactionPayloadBuilderError::PayloadRule)?;
507 }
508 for &output_index in input_output_indices.iter().flatten() {
509 output_ref(self.declared_outputs(), output_index, output_tags)
510 .map_err(TransactionPayloadBuilderError::PayloadRule)?;
511 }
512 // Every output writes a declared output, each at most once, as
513 // `PayloadDecoderCore::write_outputs()` checks. Its other rules hold by construction: the
514 // tags are valid, and the return value is the last output.
515 let mut remaining_output_tags = output_tags;
516 while let Some((&output_tag, later_output_tags)) = remaining_output_tags.split_first() {
517 remaining_output_tags = later_output_tags;
518
519 let output_index = TransactionOutput::tag_output_index(output_tag);
520 if output_index >= self.num_outputs {
521 return Err(TransactionPayloadBuilderError::PayloadRule(
522 TransactionPayloadDecoderError::OutputIndexNotFound(output_index),
523 ));
524 }
525 if writes_output(later_output_tags, output_index) {
526 return Err(TransactionPayloadBuilderError::PayloadRule(
527 TransactionPayloadDecoderError::OutputUsedTwice(output_index),
528 ));
529 }
530 }
531
532 self.extend_payload_with_alignment(contract.as_bytes(), Alignment::of::<Address>())?;
533 self.extend_payload_with_alignment(
534 method_fingerprint.as_bytes(),
535 Alignment::of::<MethodFingerprint>(),
536 )?;
537 self.push_payload_byte(method_context as u8)?;
538
539 // Store number of slots and `TransactionSlot` for each slot
540 self.push_payload_byte(num_slot_arguments)?;
541 for slot_offset in 0..usize::from(num_slot_arguments) {
542 let slot_type = if let Some(&Some(output_index)) = slot_output_indices.get(slot_offset)
543 {
544 TransactionSlot::new_output_index(output_index).ok_or(
545 TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
546 )?
547 } else {
548 TransactionSlot::new_address()
549 };
550 self.push_payload_byte(slot_type.into_u8())?;
551 }
552
553 // Store number of inputs and `TransactionInput` for each input
554 self.push_payload_byte(num_input_arguments)?;
555 for (input_offset, type_details) in input_type_details.iter().enumerate() {
556 let input_type =
557 if let Some(&Some(output_index)) = input_output_indices.get(input_offset) {
558 // Only the output index is written, so the alignment of the input isn't used
559 TransactionInput::new_output_index(output_index).ok_or(
560 TransactionPayloadBuilderError::InvalidOutputIndex(output_index),
561 )?
562 } else {
563 TransactionInput::new_value(type_details.alignment)
564 };
565 self.push_payload_byte(input_type.into_u8())?;
566 }
567
568 // Store number of outputs and `TransactionOutput` for each output
569 self.push_payload_byte(num_output_arguments)?;
570 for &output_tag in output_tags {
571 self.push_payload_byte(output_tag)?;
572 }
573
574 for slot_offset in 0..usize::from(num_slot_arguments) {
575 // SAFETY: Method description requires the layout to correspond to metadata, where all
576 // slots come before inputs and outputs (see `ArgumentsMetadataDecoder::decode_next()`),
577 // and the pointer to be non-null even when it dangles
578 let address = unsafe { read_external_args::<NonNull<Address>>(external_args) };
579
580 if slot_output_indices
581 .get(slot_offset)
582 .copied()
583 .flatten()
584 .is_none()
585 {
586 // SAFETY: Method description requires the layout to correspond to metadata, and
587 // the slot doesn't refer to a declared output, so it points to an address
588 let address = unsafe { address.as_ref() };
589 self.extend_payload_with_alignment(address.as_bytes(), Alignment::of::<Address>())?;
590 }
591 }
592
593 for (input_offset, type_details) in input_type_details.iter().enumerate() {
594 // SAFETY: Method description requires the layout to correspond to metadata, where the
595 // inputs come right after the slots (see `ArgumentsMetadataDecoder::decode_next()`),
596 // and the pointer to be non-null even when it dangles
597 let FfiDataSizeCapacityRo {
598 data_ptr,
599 size,
600 capacity: _,
601 } = unsafe { read_external_args(external_args) };
602
603 if input_output_indices
604 .get(input_offset)
605 .copied()
606 .flatten()
607 .is_none()
608 {
609 // SAFETY: Method description requires the layout to correspond to metadata, and
610 // the input doesn't refer to a declared output, so it points to `size` bytes
611 let data =
612 unsafe { slice::from_raw_parts(data_ptr.as_ptr().cast_const(), size as usize) };
613 self.extend_payload_with_alignment(&size.to_le_bytes(), Alignment::of::<u32>())?;
614 self.extend_payload_with_alignment(data, type_details.alignment)?;
615 }
616 }
617
618 if let Some(size) = return_value_size {
619 self.extend_payload_with_alignment(&size.to_le_bytes(), Alignment::of::<u32>())?;
620 }
621
622 // Only after the whole method call was added, so that declared outputs don't change if it
623 // doesn't fit. An `#[output]` needs its recommended capacity, and the recommended capacity
624 // of a return value is its size.
625 for (&output_tag, type_details) in output_tags.iter().zip(output_type_details) {
626 // SAFETY: The tag was made from a `TransactionOutput` above, and every output index was
627 // checked above to refer to a declared output
628 unsafe {
629 let output = TransactionOutput::from_u8(output_tag).unwrap_unchecked();
630 self.write_output(output, type_details.recommended_capacity);
631 }
632 }
633
634 Ok(())
635 }
636
637 /// Returns 16-byte aligned bytes, padded to a multiple of `u128` size.
638 ///
639 /// The contents is a header with the declared outputs, followed by a concatenated sequence of
640 /// method calls with their arguments. All data structures are correctly aligned in the
641 /// returned byte buffer with `0` used as padding when necessary.
642 /// [`OwnedAlignedBuffer::as_u128_slice()`] returns the whole payload as `u128`s. Every payload
643 /// starts with the header, also without declared outputs and method calls.
644 ///
645 /// The capacity of every declared output is the largest that a method call writing it needs:
646 /// the recommended capacity of an `#[output]`, the size of a return value, or `0` for the state
647 /// returned by an `#[init]` method.
648 ///
649 /// Returns [`TransactionPayloadBuilderError::UnusedOutput`] with the first declared output that
650 /// no method call writes, since its memory would never be used, which most likely means that a
651 /// method call refers to the wrong output index. [`TransactionPayloadDecoder`] and
652 /// [`TransactionPayloadCaller`] accept such a payload.
653 ///
654 /// The header:
655 /// * Number of declared outputs: `u8`
656 /// * For each declared output:
657 /// * Its capacity as little-endian `u32`
658 ///
659 /// Each method is serialized in the following way:
660 /// * Contract to call: [`Address`]
661 /// * Fingerprint of the method to call: [`MethodFingerprint`]
662 /// * Method context: [`TransactionMethodContext`]
663 /// * Number of `#[slot]` arguments: `u8`
664 /// * For each `#[slot]` argument:
665 /// * [`TransactionSlot`] as `u8`
666 /// * Number of `#[input]` arguments: `u8`
667 /// * For each `#[input]` argument:
668 /// * [`TransactionInput`] as `u8`
669 /// * Number of `#[output]` arguments, including the return value: `u8`
670 /// * For each `#[output]` argument, followed by the return value if there is one:
671 /// * [`TransactionOutput`] as `u8`
672 /// * For each [`TransactionSlot`], whose type is [`TransactionSlotType::Address`]:
673 /// * [`Address`]
674 /// * For each [`TransactionInput`], whose type is [`TransactionInputType::Value`]:
675 /// * Input size as little-endian `u32` followed by the input itself
676 /// * If the last [`TransactionOutput`] is a return value:
677 /// * Its size as little-endian `u32`
678 ///
679 /// [`TransactionPayloadDecoder`]: crate::payload::decoder::TransactionPayloadDecoder
680 /// [`TransactionPayloadCaller`]: crate::payload::TransactionPayloadCaller
681 /// [`TransactionSlotType::Address`]: crate::payload::TransactionSlotType::Address
682 /// [`TransactionInputType::Value`]: crate::payload::TransactionInputType::Value
683 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
684 pub fn into_aligned_bytes(
685 mut self,
686 ) -> Result<OwnedAlignedBuffer, TransactionPayloadBuilderError<'static>> {
687 for (output_index, declared_output) in (0..=u8::MAX).zip(self.declared_outputs()) {
688 if declared_output.state == OutputState::NotWritten {
689 return Err(TransactionPayloadBuilderError::UnusedOutput(output_index));
690 }
691 }
692
693 // SAFETY: Constructor guarantees that capacity is a multiple of `Alignment::MAX` and is
694 // never exceeded
695 unsafe {
696 self.ensure_alignment(Alignment::MAX).unwrap_unchecked();
697 }
698
699 Ok(self.payload)
700 }
701
702 /// Record that a method call writes `output`, which needs `capacity` bytes, and raise the
703 /// capacity of its declared output in the header to that if it is smaller.
704 ///
705 /// # Safety
706 /// The output index of `output` must refer to a declared output.
707 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
708 unsafe fn write_output(&mut self, output: TransactionOutput, capacity: u32) {
709 let output_index = usize::from(output.output_index());
710 // Capacities follow the number of declared outputs and 3 bytes of padding
711 let offset = size_of::<u32>() * (1 + output_index);
712 // SAFETY: The output is declared (function contract), and `Self::new()` declares at most
713 // `MAX_OUTPUTS` outputs. `Self::new()` wrote a capacity for every declared output into the
714 // header, and the payload only grows after that.
715 unsafe {
716 hint::assert_unchecked(
717 output_index < MAX_OUTPUTS
718 && offset + size_of::<u32>() <= self.payload.len() as usize,
719 );
720 }
721
722 let declared_output = self
723 .declared_outputs
724 .get_mut(output_index)
725 .expect("Below `MAX_OUTPUTS`, as asserted above; qed");
726 declared_output.capacity = declared_output.capacity.max(capacity);
727 let kind = output.kind();
728 let size = match kind {
729 TransactionOutputKind::Output => 0,
730 // The capacity that a return value needs is its size
731 TransactionOutputKind::ReturnValue => capacity,
732 };
733 declared_output.set_written(kind, size);
734
735 let declared_capacity = self.payload.as_mut_slice()[offset..]
736 .first_chunk_mut::<{ size_of::<u32>() }>()
737 .expect("The header has a capacity for every declared output; qed");
738 *declared_capacity = declared_output.capacity.to_le_bytes();
739 }
740
741 /// Outputs that the payload declares
742 #[inline(always)]
743 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
744 fn declared_outputs(&self) -> &[DeclaredOutput] {
745 // SAFETY: `Self::new()` sets `num_outputs` to at most `MAX_OUTPUTS`, the length of
746 // `declared_outputs`, and it never changes after that
747 unsafe {
748 self.declared_outputs
749 .get_unchecked(..usize::from(self.num_outputs))
750 }
751 }
752
753 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
754 fn extend_payload_with_alignment(
755 &mut self,
756 bytes: &[u8],
757 alignment: Alignment,
758 ) -> Result<(), TransactionPayloadBuilderError<'static>> {
759 self.ensure_alignment(alignment)?;
760 self.extend_payload(bytes)
761 }
762
763 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
764 fn ensure_alignment(
765 &mut self,
766 alignment: Alignment,
767 ) -> Result<(), TransactionPayloadBuilderError<'static>> {
768 let alignment = usize::from(alignment.get());
769
770 let unaligned_by = self.payload.len() as usize % alignment;
771 if unaligned_by > 0 {
772 let padding_bytes = alignment - unaligned_by;
773 // Byte by byte rather than as a slice of zeroes, which the compiler can't prove to be
774 // in bounds
775 for _ in 0..padding_bytes {
776 self.push_payload_byte(0)?;
777 }
778 }
779
780 Ok(())
781 }
782
783 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
784 fn push_payload_byte(
785 &mut self,
786 byte: u8,
787 ) -> Result<(), TransactionPayloadBuilderError<'static>> {
788 self.extend_payload(&[byte])
789 }
790
791 /// Append `bytes` to the payload if they fit into its capacity, it is never reallocated
792 #[cfg_attr(feature = "no-panic", no_panic_const::no_panic)]
793 fn extend_payload(
794 &mut self,
795 bytes: &[u8],
796 ) -> Result<(), TransactionPayloadBuilderError<'static>> {
797 // The same checks as in `OwnedAlignedBuffer::append()`, so the compiler can prove that it
798 // never reallocates
799 let fits = u32::try_from(bytes.len())
800 .ok()
801 .and_then(|len| self.payload.len().checked_add(len))
802 .is_some_and(|new_len| new_len <= self.payload.capacity());
803 if !(fits && self.payload.append(bytes)) {
804 return Err(TransactionPayloadBuilderError::PayloadTooLarge);
805 }
806
807 Ok(())
808 }
809}