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