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