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 core::ops::Deref;
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 in bytes, must be a multiple of `u128` size
96    pub payload: u32,
97    /// Seal length in bytes
98    pub seal: u32,
99    /// Not used and must be set to `0`
100    pub padding: [u8; 4],
101}
102
103/// Similar to `OwnedTransaction`, but doesn't require `alloc` or data ownership.
104///
105/// Can be created with `Transaction::try_from_bytes()` or borrowed from
106/// `OwnedTransaction::transaction()`.
107#[derive(Debug, Clone)]
108#[expect(
109    clippy::partial_pub_fields,
110    reason = "Intentionally exposing decoded fields and hiding the bytes they were decoded from"
111)]
112pub struct Transaction<'a> {
113    /// Transaction header
114    pub mut(self) header: &'a TransactionHeader,
115    /// Slots in the form of [`TransactionSlot`] that may be read during transaction processing.
116    ///
117    /// These are the only slots that can be used in authorization code.
118    ///
119    /// The code slot of the contract that is being executed and balance of native token are
120    /// implicitly included and don't need to be specified (see [`Transaction::read_slots()`]).
121    /// Slots that may also be written to do not need to be repeated in the read slots.
122    pub mut(self) read_slots: &'a [TransactionSlot],
123    /// Slots in the form of [`TransactionSlot`] that may be written during transaction processing
124    pub mut(self) write_slots: &'a [TransactionSlot],
125    /// Transaction payload
126    pub mut(self) payload: &'a [u128],
127    /// Transaction seal
128    pub mut(self) seal: &'a [u8],
129    /// Serialized transaction the fields above were parsed from
130    bytes: &'a [u8],
131    #[cfg(all(feature = "alloc", any(target_os = "none", target_os = "unknown")))]
132    cached_hash: once_cell::race::OnceBox<TransactionHash>,
133    #[cfg(not(any(target_os = "none", target_os = "unknown")))]
134    cached_hash: std::sync::OnceLock<TransactionHash>,
135}
136
137impl<'a> Transaction<'a> {
138    /// Create an instance from provided correctly aligned bytes.
139    ///
140    /// `bytes` should be 16-bytes aligned.
141    ///
142    /// The layout is following data structures concatenated as bytes (they are carefully picked to
143    /// ensure alignment):
144    /// * [`TransactionHeader`]
145    /// * [`SerializedTransactionLengths`] (with values set to correspond to below contents)
146    /// * All read [`TransactionSlot`]
147    /// * All write [`TransactionSlot`]
148    /// * Payload as `u128`s
149    /// * Seal as `u8`s
150    ///
151    /// Payload and seal lengths in [`SerializedTransactionLengths`] are in bytes.
152    ///
153    /// Returns an instance and remaining bytes on success.
154    #[inline]
155    pub fn try_from_bytes(bytes: &'a [u8]) -> Option<(Self, &'a [u8])> {
156        const {
157            // Slots after `SerializedTransactionLengths` and payload after slots must be aligned
158            assert!(
159                (size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>())
160                    .is_multiple_of(usize::from(Alignment::MAX.get()))
161            );
162            assert!(size_of::<TransactionSlot>().is_multiple_of(usize::from(Alignment::MAX.get())));
163        }
164
165        if !Alignment::MAX.is_slice_aligned(bytes) {
166            return None;
167        }
168
169        let (header, remainder) = TransactionHeader::from_bytes(bytes)?;
170        let (lengths, remainder) = SerializedTransactionLengths::from_bytes(remainder)?;
171        let SerializedTransactionLengths {
172            read_slots,
173            write_slots,
174            payload,
175            seal,
176            padding,
177        } = *lengths;
178
179        if padding != [0; _] {
180            return None;
181        }
182
183        if !payload.is_multiple_of(u128::SIZE) {
184            return None;
185        }
186
187        // Lossless, the crate only compiles for targets where `usize` is at least 32 bits (see the
188        // assertion in the crate root)
189        let payload = payload as usize;
190        let seal = seal as usize;
191
192        let (read_slots, remainder) =
193            TransactionSlot::slice_from_bytes(remainder, usize::from(read_slots))?;
194        let (write_slots, remainder) =
195            TransactionSlot::slice_from_bytes(remainder, usize::from(write_slots))?;
196        let (payload, remainder) = u128::slice_from_bytes(remainder, payload / size_of::<u128>())?;
197        let (seal, remainder) = remainder.split_at_checked(seal)?;
198        // `remainder` is the end of `bytes`, so the difference doesn't underflow
199        let (bytes, _remainder) = bytes.split_at(bytes.len() - remainder.len());
200
201        let transaction = Self {
202            header,
203            read_slots,
204            write_slots,
205            payload,
206            seal,
207            bytes,
208            #[cfg(all(feature = "alloc", any(target_os = "none", target_os = "unknown")))]
209            cached_hash: once_cell::race::OnceBox::new(),
210            #[cfg(not(any(target_os = "none", target_os = "unknown")))]
211            cached_hash: std::sync::OnceLock::new(),
212        };
213
214        Some((transaction, remainder))
215    }
216
217    /// Create an owned version of this transaction
218    #[cfg(feature = "alloc")]
219    #[inline(always)]
220    pub fn to_owned(self) -> Result<OwnedTransaction, OwnedTransactionError> {
221        OwnedTransaction::from_transaction(self)
222    }
223
224    /// Size of the encoded transaction in bytes
225    #[inline(always)]
226    pub const fn encoded_size(&self) -> usize {
227        self.bytes.len()
228    }
229
230    /// Serialized transaction
231    #[cfg(feature = "alloc")]
232    #[inline(always)]
233    pub(crate) const fn bytes(&self) -> &'a [u8] {
234        self.bytes
235    }
236
237    /// Compute transaction hash.
238    ///
239    /// Note that this method hashes the whole serialized transaction. The result is then cached if
240    /// `alloc` feature is enabled or when compiled for OS target that is not `none`.
241    #[inline]
242    pub fn hash(&self) -> impl Deref<Target = TransactionHash> + Send + Sync {
243        let compute_hash = || TransactionHash(Blake3Hash::from(blake3::hash(self.bytes)));
244
245        #[cfg(not(any(target_os = "none", target_os = "unknown")))]
246        {
247            self.cached_hash.get_or_init(compute_hash)
248        }
249        #[cfg(all(feature = "alloc", any(target_os = "none", target_os = "unknown")))]
250        {
251            self.cached_hash
252                .get_or_init(|| alloc::boxed::Box::new(compute_hash()))
253        }
254        #[cfg(all(not(feature = "alloc"), any(target_os = "none", target_os = "unknown")))]
255        {
256            struct Wrapper(TransactionHash);
257
258            impl Deref for Wrapper {
259                type Target = TransactionHash;
260
261                #[inline(always)]
262                fn deref(&self) -> &Self::Target {
263                    &self.0
264                }
265            }
266
267            Wrapper(compute_hash())
268        }
269    }
270
271    /// Read slots touched by the transaction.
272    ///
273    /// In contrast to `read_slots` property, this includes implicitly used slots.
274    pub fn read_slots(&self) -> impl Iterator<Item = TransactionSlot> {
275        // Slots included implicitly that are always used
276        let implicit_slots = [
277            TransactionSlot {
278                owner: self.header.contract,
279                contract: Address::SYSTEM_CODE,
280            },
281            // TODO: Uncomment once system token contract exists
282            // TransactionSlot {
283            //     owner: self.header.contract,
284            //     contract: Address::SYSTEM_TOKEN,
285            // },
286        ];
287
288        implicit_slots
289            .into_iter()
290            .chain(self.read_slots.iter().copied())
291    }
292
293    /// All slots touched by the transaction.
294    ///
295    /// In contrast to `read_slots` and `write_slots` properties, this includes implicitly used
296    /// slots.
297    pub fn slots(&self) -> impl Iterator<Item = TransactionSlot> {
298        self.read_slots().chain(self.write_slots.iter().copied())
299    }
300}