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        #[expect(
137            clippy::cast_ptr_alignment,
138            reason = "False-positive, see https://github.com/rust-lang/rust-clippy/issues/17636"
139        )]
140        let bytes_ptr = bytes.as_ptr().cast::<u128>();
141        if !bytes_ptr.is_aligned()
142            || bytes.len()
143                < size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>()
144        {
145            return None;
146        }
147
148        // SAFETY: Checked above that there are enough bytes and they are correctly aligned
149        let lengths = unsafe {
150            bytes_ptr
151                .byte_add(size_of::<TransactionHeader>())
152                .cast::<SerializedTransactionLengths>()
153                .read()
154        };
155        let SerializedTransactionLengths {
156            read_slots,
157            write_slots,
158            payload,
159            seal,
160            padding,
161        } = lengths;
162
163        if padding != [0; _] {
164            return None;
165        }
166
167        if !payload.is_multiple_of(u128::SIZE) {
168            return None;
169        }
170
171        let size = (size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>())
172            .checked_add(usize::from(read_slots) * size_of::<TransactionSlot>())?
173            .checked_add(usize::from(write_slots) * size_of::<TransactionSlot>())?
174            .checked_add(payload as usize * size_of::<u128>())?
175            .checked_add(seal as usize)?;
176
177        if bytes.len() < size {
178            return None;
179        }
180
181        // SAFETY: Size and alignment checked above
182        let transaction = unsafe { Self::from_bytes_unchecked(bytes) };
183        let remainder = bytes.split_off(transaction.encoded_size()..)?;
184
185        Some((transaction, remainder))
186    }
187
188    /// Create an instance from provided bytes without performing any checks for size or alignment.
189    ///
190    /// The internal layout of the owned transaction is following data structures concatenated as
191    /// bytes (they are carefully picked to ensure alignment):
192    /// * [`TransactionHeader`]
193    /// * [`SerializedTransactionLengths`] (with values set to correspond to below contents)
194    /// * All read [`TransactionSlot`]
195    /// * All write [`TransactionSlot`]
196    /// * Payload as `u128`s
197    /// * Seal as `u8`s
198    ///
199    /// # Safety
200    /// Caller must ensure provided bytes are 16-bytes aligned and of sufficient length. Extra bytes
201    /// beyond necessary are silently ignored if provided.
202    #[inline]
203    #[expect(
204        clippy::cast_ptr_alignment,
205        reason = "Unchecked method contract guarantees size and alignment"
206    )]
207    pub unsafe fn from_bytes_unchecked(bytes: &'a [u8]) -> Transaction<'a> {
208        // SAFETY: Method contract guarantees size and alignment
209        let lengths = unsafe {
210            bytes
211                .as_ptr()
212                .add(size_of::<TransactionHeader>())
213                .cast::<SerializedTransactionLengths>()
214                .read()
215        };
216        let SerializedTransactionLengths {
217            read_slots,
218            write_slots,
219            payload,
220            seal,
221            padding: _,
222        } = lengths;
223
224        Self {
225            // SAFETY: Any bytes are valid for `TransactionHeader` and all method contract
226            // guarantees there are enough correctly aligned bytes for header in the buffer
227            header: unsafe {
228                bytes
229                    .as_ptr()
230                    .cast::<TransactionHeader>()
231                    .as_ref_unchecked()
232            },
233            // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
234            // there are enough correctly aligned bytes for read slots in the buffer
235            read_slots: unsafe {
236                slice::from_raw_parts(
237                    bytes
238                        .as_ptr()
239                        .add(size_of::<TransactionHeader>())
240                        .add(size_of::<SerializedTransactionLengths>())
241                        .cast::<TransactionSlot>(),
242                    usize::from(read_slots),
243                )
244            },
245            // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
246            // there are enough correctly aligned bytes for write slots in the buffer
247            write_slots: unsafe {
248                slice::from_raw_parts(
249                    bytes
250                        .as_ptr()
251                        .add(size_of::<TransactionHeader>())
252                        .add(size_of::<SerializedTransactionLengths>())
253                        .cast::<TransactionSlot>()
254                        .add(usize::from(read_slots)),
255                    usize::from(write_slots),
256                )
257            },
258            // SAFETY: Any bytes are valid for `payload` and all method contract guarantees there
259            // are enough correctly aligned bytes for payload in the buffer
260            payload: unsafe {
261                slice::from_raw_parts(
262                    bytes
263                        .as_ptr()
264                        .add(size_of::<TransactionHeader>())
265                        .add(size_of::<SerializedTransactionLengths>())
266                        .add(
267                            size_of::<TransactionSlot>()
268                                * (usize::from(read_slots) + usize::from(write_slots)),
269                        )
270                        .cast::<u128>(),
271                    payload as usize,
272                )
273            },
274            // SAFETY: Any bytes are valid for `seal` and all method contract guarantees there are
275            // enough bytes for seal in the buffer
276            seal: unsafe {
277                slice::from_raw_parts(
278                    bytes
279                        .as_ptr()
280                        .add(size_of::<TransactionHeader>())
281                        .add(size_of::<SerializedTransactionLengths>())
282                        .add(
283                            size_of::<TransactionSlot>()
284                                * (usize::from(read_slots) + usize::from(write_slots))
285                                + payload as usize,
286                        ),
287                    seal as usize,
288                )
289            },
290        }
291    }
292
293    /// Create an owned version of this transaction
294    #[cfg(feature = "alloc")]
295    #[inline(always)]
296    pub fn to_owned(self) -> Result<OwnedTransaction, OwnedTransactionError> {
297        OwnedTransaction::from_transaction(self)
298    }
299
300    /// Size of the encoded transaction in bytes
301    pub const fn encoded_size(&self) -> usize {
302        size_of::<TransactionHeader>()
303            + size_of::<SerializedTransactionLengths>()
304            + size_of_val(self.read_slots)
305            + size_of_val(self.write_slots)
306            + size_of_val(self.payload)
307            + size_of_val(self.seal)
308    }
309
310    /// Compute transaction hash.
311    ///
312    /// Note: this computes transaction hash on every call, so worth caching if it is expected to be
313    /// called often.
314    pub fn hash(&self) -> TransactionHash {
315        // TODO: Keyed hash
316        let mut hasher = Hasher::new();
317
318        hasher.update(self.header.as_bytes());
319        // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
320        hasher.update(unsafe {
321            slice::from_raw_parts(
322                self.read_slots.as_ptr().cast::<u8>(),
323                size_of_val(self.read_slots),
324            )
325        });
326        // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
327        hasher.update(unsafe {
328            slice::from_raw_parts(
329                self.write_slots.as_ptr().cast::<u8>(),
330                size_of_val(self.write_slots),
331            )
332        });
333        // SAFETY: `u128` and can be treated as bytes
334        hasher.update(unsafe {
335            slice::from_raw_parts(
336                self.payload.as_ptr().cast::<u8>(),
337                size_of_val(self.payload),
338            )
339        });
340        hasher.update(self.seal);
341
342        TransactionHash(Blake3Hash::from(hasher.finalize()))
343    }
344
345    /// Read slots touched by the transaction.
346    ///
347    /// In contrast to `read_slots` property, this includes implicitly used slots.
348    pub fn read_slots(&self) -> impl Iterator<Item = TransactionSlot> {
349        // Slots included implicitly that are always used
350        let implicit_slots = [
351            TransactionSlot {
352                owner: self.header.contract,
353                contract: Address::SYSTEM_CODE,
354            },
355            // TODO: Uncomment once system token contract exists
356            // TransactionSlot {
357            //     owner: self.header.contract,
358            //     contract: Address::SYSTEM_TOKEN,
359            // },
360        ];
361
362        implicit_slots
363            .into_iter()
364            .chain(self.read_slots.iter().copied())
365    }
366
367    /// All slots touched by the transaction.
368    ///
369    /// In contrast to `read_slots` and `write_slots` properties, this includes implicitly used
370    /// slots.
371    pub fn slots(&self) -> impl Iterator<Item = TransactionSlot> {
372        self.read_slots().chain(self.write_slots.iter().copied())
373    }
374}