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 % u128::SIZE != 0 {
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    pub unsafe fn from_bytes_unchecked(bytes: &'a [u8]) -> Transaction<'a> {
200        // SAFETY: Method contract guarantees size and alignment
201        let lengths = unsafe {
202            bytes
203                .as_ptr()
204                .add(size_of::<TransactionHeader>())
205                .cast::<SerializedTransactionLengths>()
206                .read()
207        };
208        let SerializedTransactionLengths {
209            read_slots,
210            write_slots,
211            payload,
212            seal,
213            padding: _,
214        } = lengths;
215
216        Self {
217            // SAFETY: Any bytes are valid for `TransactionHeader` and all method contract
218            // guarantees there are enough bytes for header in the buffer
219            header: unsafe {
220                bytes
221                    .as_ptr()
222                    .cast::<TransactionHeader>()
223                    .as_ref_unchecked()
224            },
225            // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
226            // there are enough bytes for read slots in the buffer
227            read_slots: unsafe {
228                slice::from_raw_parts(
229                    bytes
230                        .as_ptr()
231                        .add(size_of::<TransactionHeader>())
232                        .add(size_of::<SerializedTransactionLengths>())
233                        .cast::<TransactionSlot>(),
234                    usize::from(read_slots),
235                )
236            },
237            // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
238            // there are enough bytes for write slots in the buffer
239            write_slots: unsafe {
240                slice::from_raw_parts(
241                    bytes
242                        .as_ptr()
243                        .add(size_of::<TransactionHeader>())
244                        .add(size_of::<SerializedTransactionLengths>())
245                        .cast::<TransactionSlot>()
246                        .add(usize::from(read_slots)),
247                    usize::from(write_slots),
248                )
249            },
250            // SAFETY: Any bytes are valid for `payload` and all method contract guarantees there
251            // are enough bytes for payload in the buffer
252            payload: unsafe {
253                slice::from_raw_parts(
254                    bytes
255                        .as_ptr()
256                        .add(size_of::<TransactionHeader>())
257                        .add(size_of::<SerializedTransactionLengths>())
258                        .add(
259                            size_of::<TransactionSlot>()
260                                * (usize::from(read_slots) + usize::from(write_slots)),
261                        )
262                        .cast::<u128>(),
263                    payload as usize,
264                )
265            },
266            // SAFETY: Any bytes are valid for `seal` and all method contract guarantees there are
267            // enough bytes for seal in the buffer
268            seal: 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                                + payload as usize,
278                        ),
279                    seal as usize,
280                )
281            },
282        }
283    }
284
285    /// Create an owned version of this transaction
286    #[cfg(feature = "alloc")]
287    #[inline(always)]
288    pub fn to_owned(self) -> Result<OwnedTransaction, OwnedTransactionError> {
289        OwnedTransaction::from_transaction(self)
290    }
291
292    /// Size of the encoded transaction in bytes
293    pub const fn encoded_size(&self) -> usize {
294        size_of::<TransactionHeader>()
295            + size_of::<SerializedTransactionLengths>()
296            + size_of_val(self.read_slots)
297            + size_of_val(self.write_slots)
298            + size_of_val(self.payload)
299            + size_of_val(self.seal)
300    }
301
302    /// Compute transaction hash.
303    ///
304    /// Note: this computes transaction hash on every call, so worth caching if it is expected to be
305    /// called often.
306    pub fn hash(&self) -> TransactionHash {
307        // TODO: Keyed hash
308        let mut hasher = Hasher::new();
309
310        hasher.update(self.header.as_bytes());
311        // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
312        hasher.update(unsafe {
313            slice::from_raw_parts(
314                self.read_slots.as_ptr().cast::<u8>(),
315                size_of_val(self.read_slots),
316            )
317        });
318        // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
319        hasher.update(unsafe {
320            slice::from_raw_parts(
321                self.write_slots.as_ptr().cast::<u8>(),
322                size_of_val(self.write_slots),
323            )
324        });
325        // SAFETY: `u128` and can be treated as bytes
326        hasher.update(unsafe {
327            slice::from_raw_parts(
328                self.payload.as_ptr().cast::<u8>(),
329                size_of_val(self.payload),
330            )
331        });
332        hasher.update(self.seal);
333
334        TransactionHash(Blake3Hash::from(hasher.finalize()))
335    }
336
337    /// Read slots touched by the transaction.
338    ///
339    /// In contrast to `read_slots` property, this includes implicitly used slots.
340    pub fn read_slots(&self) -> impl Iterator<Item = TransactionSlot> {
341        // Slots included implicitly that are always used
342        let implicit_slots = [
343            TransactionSlot {
344                owner: self.header.contract,
345                contract: Address::SYSTEM_CODE,
346            },
347            // TODO: Uncomment once system token contract exists
348            // TransactionSlot {
349            //     owner: self.header.contract,
350            //     contract: Address::SYSTEM_TOKEN,
351            // },
352        ];
353
354        implicit_slots
355            .into_iter()
356            .chain(self.read_slots.iter().copied())
357    }
358
359    /// All slots touched by the transaction.
360    ///
361    /// In contrast to `read_slots` and `write_slots` properties, this includes implicitly used
362    /// slots.
363    pub fn slots(&self) -> impl Iterator<Item = TransactionSlot> {
364        self.read_slots().chain(self.write_slots.iter().copied())
365    }
366}