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}