Skip to main content

ab_core_primitives/
transaction.rs

1//! Transaction-related primitives
2
3#[cfg(feature = "alloc")]
4pub mod owned;
5
6use crate::address::Address;
7use crate::block::BlockRoot;
8use crate::hashes::Blake3Hash;
9#[cfg(feature = "alloc")]
10use crate::transaction::owned::{OwnedTransaction, OwnedTransactionError};
11use ab_io_type::trivial_type::TrivialType;
12use blake3::Hasher;
13use core::slice;
14use derive_more::{Deref, DerefMut, Display, From, Into};
15
16/// A measure of compute resources, 1 Gas == 1 ns of compute on reference hardware
17#[derive(Debug, Default, Copy, Clone, TrivialType)]
18#[repr(C)]
19pub struct Gas(u64);
20
21/// Transaction hash
22#[derive(
23    Debug,
24    Display,
25    Default,
26    Copy,
27    Clone,
28    Ord,
29    PartialOrd,
30    Eq,
31    PartialEq,
32    Hash,
33    From,
34    Into,
35    Deref,
36    DerefMut,
37    TrivialType,
38)]
39#[repr(C)]
40pub struct TransactionHash(Blake3Hash);
41
42impl AsRef<[u8]> for TransactionHash {
43    #[inline(always)]
44    fn as_ref(&self) -> &[u8] {
45        self.0.as_ref()
46    }
47}
48
49impl AsMut<[u8]> for TransactionHash {
50    #[inline(always)]
51    fn as_mut(&mut self) -> &mut [u8] {
52        self.0.as_mut()
53    }
54}
55
56/// Transaction header
57#[derive(Debug, Copy, Clone, TrivialType)]
58#[repr(C)]
59pub struct TransactionHeader {
60    // TODO: Right now this is primarily used for data alignment, but is it useful in general?
61    // TODO: Some more complex field?
62    /// Transaction version
63    pub version: u64,
64    /// Block root at which transaction was created
65    pub block_root: BlockRoot,
66    /// Gas limit
67    pub gas_limit: Gas,
68    /// Contract implementing `TxHandler` trait to use for transaction verification and execution
69    pub contract: Address,
70}
71
72impl TransactionHeader {
73    /// The only supported transaction version right now
74    pub const TRANSACTION_VERSION: u64 = 0;
75}
76
77/// Transaction slot
78#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, TrivialType)]
79#[repr(C)]
80pub struct TransactionSlot {
81    /// Slot owner
82    pub owner: Address,
83    /// Contract that manages the slot
84    pub contract: Address,
85}
86
87/// Lengths of various components in a serialized version of [`Transaction`]
88#[derive(Debug, Default, Copy, Clone, TrivialType)]
89#[repr(C)]
90pub struct SerializedTransactionLengths {
91    /// Number of read-only slots
92    pub read_slots: u16,
93    /// Number of read-write slots
94    pub write_slots: u16,
95    /// Payload length
96    pub payload: u32,
97    /// Seal length
98    pub seal: u32,
99    /// Not used and must be set to `0`
100    pub padding: [u8; 4],
101}
102
103/// Similar to `Transaction`, but doesn't require `allow` or data ownership.
104///
105/// Can be created with `Transaction::as_ref()` call.
106#[derive(Debug, Copy, Clone)]
107pub struct Transaction<'a> {
108    /// Transaction header
109    pub header: &'a TransactionHeader,
110    /// Slots in the form of [`TransactionSlot`] that may be read during transaction processing.
111    ///
112    /// These are the only slots that can be used in authorization code.
113    ///
114    /// The code slot of the contract that is being executed and balance of native token are
115    /// implicitly included and doesn't need to be specified (see [`Transaction::read_slots()`].
116    /// Also slots that may also be written to do not need to be repeated in the read slots.
117    pub read_slots: &'a [TransactionSlot],
118    /// Slots in the form of [`TransactionSlot`] that may be written during transaction processing
119    pub write_slots: &'a [TransactionSlot],
120    /// Transaction payload
121    pub payload: &'a [u128],
122    /// Transaction seal
123    pub seal: &'a [u8],
124}
125
126impl<'a> Transaction<'a> {
127    /// Create an instance from provided correctly aligned bytes.
128    ///
129    /// `bytes` should be 16-bytes aligned.
130    ///
131    /// See [`Self::from_bytes_unchecked()`] for layout details.
132    ///
133    /// Returns an instance and remaining bytes on success.
134    #[inline]
135    pub fn try_from_bytes(mut bytes: &'a [u8]) -> Option<(Self, &'a [u8])> {
136        if !bytes.as_ptr().cast::<u128>().is_aligned()
137            || bytes.len()
138                < size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>()
139        {
140            return None;
141        }
142
143        // SAFETY: Checked above that there are enough bytes and they are correctly aligned
144        let lengths = unsafe {
145            bytes
146                .as_ptr()
147                .add(size_of::<TransactionHeader>())
148                .cast::<SerializedTransactionLengths>()
149                .read()
150        };
151        let SerializedTransactionLengths {
152            read_slots,
153            write_slots,
154            payload,
155            seal,
156            padding,
157        } = lengths;
158
159        if padding != [0; _] {
160            return None;
161        }
162
163        if !payload.is_multiple_of(u128::SIZE) {
164            return None;
165        }
166
167        let size = (size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>())
168            .checked_add(usize::from(read_slots) * size_of::<TransactionSlot>())?
169            .checked_add(usize::from(write_slots) * size_of::<TransactionSlot>())?
170            .checked_add(payload as usize * size_of::<u128>())?
171            .checked_add(seal as usize)?;
172
173        if bytes.len() < size {
174            return None;
175        }
176
177        // SAFETY: Size and alignment checked above
178        let transaction = unsafe { Self::from_bytes_unchecked(bytes) };
179        let remainder = bytes.split_off(transaction.encoded_size()..)?;
180
181        Some((transaction, remainder))
182    }
183
184    /// Create an instance from provided bytes without performing any checks for size or alignment.
185    ///
186    /// The internal layout of the owned transaction is following data structures concatenated as
187    /// bytes (they are carefully picked to ensure alignment):
188    /// * [`TransactionHeader`]
189    /// * [`SerializedTransactionLengths`] (with values set to correspond to below contents)
190    /// * All read [`TransactionSlot`]
191    /// * All write [`TransactionSlot`]
192    /// * Payload as `u128`s
193    /// * Seal as `u8`s
194    ///
195    /// # Safety
196    /// Caller must ensure provided bytes are 16-bytes aligned and of sufficient length. Extra bytes
197    /// beyond necessary are silently ignored if provided.
198    #[inline]
199    #[expect(
200        clippy::cast_ptr_alignment,
201        reason = "Unchecked method contract guarantees size and alignment"
202    )]
203    pub unsafe fn from_bytes_unchecked(bytes: &'a [u8]) -> Transaction<'a> {
204        // SAFETY: Method contract guarantees size and alignment
205        let lengths = unsafe {
206            bytes
207                .as_ptr()
208                .add(size_of::<TransactionHeader>())
209                .cast::<SerializedTransactionLengths>()
210                .read()
211        };
212        let SerializedTransactionLengths {
213            read_slots,
214            write_slots,
215            payload,
216            seal,
217            padding: _,
218        } = lengths;
219
220        Self {
221            // SAFETY: Any bytes are valid for `TransactionHeader` and all method contract
222            // guarantees there are enough correctly aligned bytes for header in the buffer
223            header: unsafe {
224                bytes
225                    .as_ptr()
226                    .cast::<TransactionHeader>()
227                    .as_ref_unchecked()
228            },
229            // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
230            // there are enough correctly aligned bytes for read slots in the buffer
231            read_slots: unsafe {
232                slice::from_raw_parts(
233                    bytes
234                        .as_ptr()
235                        .add(size_of::<TransactionHeader>())
236                        .add(size_of::<SerializedTransactionLengths>())
237                        .cast::<TransactionSlot>(),
238                    usize::from(read_slots),
239                )
240            },
241            // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
242            // there are enough correctly aligned bytes for write slots in the buffer
243            write_slots: unsafe {
244                slice::from_raw_parts(
245                    bytes
246                        .as_ptr()
247                        .add(size_of::<TransactionHeader>())
248                        .add(size_of::<SerializedTransactionLengths>())
249                        .cast::<TransactionSlot>()
250                        .add(usize::from(read_slots)),
251                    usize::from(write_slots),
252                )
253            },
254            // SAFETY: Any bytes are valid for `payload` and all method contract guarantees there
255            // are enough correctly aligned bytes for payload in the buffer
256            payload: unsafe {
257                slice::from_raw_parts(
258                    bytes
259                        .as_ptr()
260                        .add(size_of::<TransactionHeader>())
261                        .add(size_of::<SerializedTransactionLengths>())
262                        .add(
263                            size_of::<TransactionSlot>()
264                                * (usize::from(read_slots) + usize::from(write_slots)),
265                        )
266                        .cast::<u128>(),
267                    payload as usize,
268                )
269            },
270            // SAFETY: Any bytes are valid for `seal` and all method contract guarantees there are
271            // enough bytes for seal in the buffer
272            seal: unsafe {
273                slice::from_raw_parts(
274                    bytes
275                        .as_ptr()
276                        .add(size_of::<TransactionHeader>())
277                        .add(size_of::<SerializedTransactionLengths>())
278                        .add(
279                            size_of::<TransactionSlot>()
280                                * (usize::from(read_slots) + usize::from(write_slots))
281                                + payload as usize,
282                        ),
283                    seal as usize,
284                )
285            },
286        }
287    }
288
289    /// Create an owned version of this transaction
290    #[cfg(feature = "alloc")]
291    #[inline(always)]
292    pub fn to_owned(self) -> Result<OwnedTransaction, OwnedTransactionError> {
293        OwnedTransaction::from_transaction(self)
294    }
295
296    /// Size of the encoded transaction in bytes
297    pub const fn encoded_size(&self) -> usize {
298        size_of::<TransactionHeader>()
299            + size_of::<SerializedTransactionLengths>()
300            + size_of_val(self.read_slots)
301            + size_of_val(self.write_slots)
302            + size_of_val(self.payload)
303            + size_of_val(self.seal)
304    }
305
306    /// Compute transaction hash.
307    ///
308    /// Note: this computes transaction hash on every call, so worth caching if it is expected to be
309    /// called often.
310    pub fn hash(&self) -> TransactionHash {
311        // TODO: Keyed hash
312        let mut hasher = Hasher::new();
313
314        hasher.update(self.header.as_bytes());
315        // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
316        hasher.update(unsafe {
317            slice::from_raw_parts(
318                self.read_slots.as_ptr().cast::<u8>(),
319                size_of_val(self.read_slots),
320            )
321        });
322        // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
323        hasher.update(unsafe {
324            slice::from_raw_parts(
325                self.write_slots.as_ptr().cast::<u8>(),
326                size_of_val(self.write_slots),
327            )
328        });
329        // SAFETY: `u128` and can be treated as bytes
330        hasher.update(unsafe {
331            slice::from_raw_parts(
332                self.payload.as_ptr().cast::<u8>(),
333                size_of_val(self.payload),
334            )
335        });
336        hasher.update(self.seal);
337
338        TransactionHash(Blake3Hash::from(hasher.finalize()))
339    }
340
341    /// Read slots touched by the transaction.
342    ///
343    /// In contrast to `read_slots` property, this includes implicitly used slots.
344    pub fn read_slots(&self) -> impl Iterator<Item = TransactionSlot> {
345        // Slots included implicitly that are always used
346        let implicit_slots = [
347            TransactionSlot {
348                owner: self.header.contract,
349                contract: Address::SYSTEM_CODE,
350            },
351            // TODO: Uncomment once system token contract exists
352            // TransactionSlot {
353            //     owner: self.header.contract,
354            //     contract: Address::SYSTEM_TOKEN,
355            // },
356        ];
357
358        implicit_slots
359            .into_iter()
360            .chain(self.read_slots.iter().copied())
361    }
362
363    /// All slots touched by the transaction.
364    ///
365    /// In contrast to `read_slots` and `write_slots` properties, this includes implicitly used
366    /// slots.
367    pub fn slots(&self) -> impl Iterator<Item = TransactionSlot> {
368        self.read_slots().chain(self.write_slots.iter().copied())
369    }
370}