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 #[expect(
137 clippy::cast_ptr_alignment,
138 reason = "False-positive, see https://github.com/rust-lang/rust-clippy/issues/17636"
139 )]
140 let bytes_ptr = bytes.as_ptr().cast::<u128>();
141 if !bytes_ptr.is_aligned()
142 || bytes.len()
143 < size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>()
144 {
145 return None;
146 }
147
148 // SAFETY: Checked above that there are enough bytes and they are correctly aligned
149 let lengths = unsafe {
150 bytes_ptr
151 .byte_add(size_of::<TransactionHeader>())
152 .cast::<SerializedTransactionLengths>()
153 .read()
154 };
155 let SerializedTransactionLengths {
156 read_slots,
157 write_slots,
158 payload,
159 seal,
160 padding,
161 } = lengths;
162
163 if padding != [0; _] {
164 return None;
165 }
166
167 if !payload.is_multiple_of(u128::SIZE) {
168 return None;
169 }
170
171 let size = (size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>())
172 .checked_add(usize::from(read_slots) * size_of::<TransactionSlot>())?
173 .checked_add(usize::from(write_slots) * size_of::<TransactionSlot>())?
174 .checked_add(payload as usize * size_of::<u128>())?
175 .checked_add(seal as usize)?;
176
177 if bytes.len() < size {
178 return None;
179 }
180
181 // SAFETY: Size and alignment checked above
182 let transaction = unsafe { Self::from_bytes_unchecked(bytes) };
183 let remainder = bytes.split_off(transaction.encoded_size()..)?;
184
185 Some((transaction, remainder))
186 }
187
188 /// Create an instance from provided bytes without performing any checks for size or alignment.
189 ///
190 /// The internal layout of the owned transaction is following data structures concatenated as
191 /// bytes (they are carefully picked to ensure alignment):
192 /// * [`TransactionHeader`]
193 /// * [`SerializedTransactionLengths`] (with values set to correspond to below contents)
194 /// * All read [`TransactionSlot`]
195 /// * All write [`TransactionSlot`]
196 /// * Payload as `u128`s
197 /// * Seal as `u8`s
198 ///
199 /// # Safety
200 /// Caller must ensure provided bytes are 16-bytes aligned and of sufficient length. Extra bytes
201 /// beyond necessary are silently ignored if provided.
202 #[inline]
203 #[expect(
204 clippy::cast_ptr_alignment,
205 reason = "Unchecked method contract guarantees size and alignment"
206 )]
207 pub unsafe fn from_bytes_unchecked(bytes: &'a [u8]) -> Transaction<'a> {
208 // SAFETY: Method contract guarantees size and alignment
209 let lengths = unsafe {
210 bytes
211 .as_ptr()
212 .add(size_of::<TransactionHeader>())
213 .cast::<SerializedTransactionLengths>()
214 .read()
215 };
216 let SerializedTransactionLengths {
217 read_slots,
218 write_slots,
219 payload,
220 seal,
221 padding: _,
222 } = lengths;
223
224 Self {
225 // SAFETY: Any bytes are valid for `TransactionHeader` and all method contract
226 // guarantees there are enough correctly aligned bytes for header in the buffer
227 header: unsafe {
228 bytes
229 .as_ptr()
230 .cast::<TransactionHeader>()
231 .as_ref_unchecked()
232 },
233 // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
234 // there are enough correctly aligned bytes for read slots in the buffer
235 read_slots: unsafe {
236 slice::from_raw_parts(
237 bytes
238 .as_ptr()
239 .add(size_of::<TransactionHeader>())
240 .add(size_of::<SerializedTransactionLengths>())
241 .cast::<TransactionSlot>(),
242 usize::from(read_slots),
243 )
244 },
245 // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
246 // there are enough correctly aligned bytes for write slots in the buffer
247 write_slots: unsafe {
248 slice::from_raw_parts(
249 bytes
250 .as_ptr()
251 .add(size_of::<TransactionHeader>())
252 .add(size_of::<SerializedTransactionLengths>())
253 .cast::<TransactionSlot>()
254 .add(usize::from(read_slots)),
255 usize::from(write_slots),
256 )
257 },
258 // SAFETY: Any bytes are valid for `payload` and all method contract guarantees there
259 // are enough correctly aligned bytes for payload in the buffer
260 payload: unsafe {
261 slice::from_raw_parts(
262 bytes
263 .as_ptr()
264 .add(size_of::<TransactionHeader>())
265 .add(size_of::<SerializedTransactionLengths>())
266 .add(
267 size_of::<TransactionSlot>()
268 * (usize::from(read_slots) + usize::from(write_slots)),
269 )
270 .cast::<u128>(),
271 payload as usize,
272 )
273 },
274 // SAFETY: Any bytes are valid for `seal` and all method contract guarantees there are
275 // enough bytes for seal in the buffer
276 seal: unsafe {
277 slice::from_raw_parts(
278 bytes
279 .as_ptr()
280 .add(size_of::<TransactionHeader>())
281 .add(size_of::<SerializedTransactionLengths>())
282 .add(
283 size_of::<TransactionSlot>()
284 * (usize::from(read_slots) + usize::from(write_slots))
285 + payload as usize,
286 ),
287 seal as usize,
288 )
289 },
290 }
291 }
292
293 /// Create an owned version of this transaction
294 #[cfg(feature = "alloc")]
295 #[inline(always)]
296 pub fn to_owned(self) -> Result<OwnedTransaction, OwnedTransactionError> {
297 OwnedTransaction::from_transaction(self)
298 }
299
300 /// Size of the encoded transaction in bytes
301 pub const fn encoded_size(&self) -> usize {
302 size_of::<TransactionHeader>()
303 + size_of::<SerializedTransactionLengths>()
304 + size_of_val(self.read_slots)
305 + size_of_val(self.write_slots)
306 + size_of_val(self.payload)
307 + size_of_val(self.seal)
308 }
309
310 /// Compute transaction hash.
311 ///
312 /// Note: this computes transaction hash on every call, so worth caching if it is expected to be
313 /// called often.
314 pub fn hash(&self) -> TransactionHash {
315 // TODO: Keyed hash
316 let mut hasher = Hasher::new();
317
318 hasher.update(self.header.as_bytes());
319 // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
320 hasher.update(unsafe {
321 slice::from_raw_parts(
322 self.read_slots.as_ptr().cast::<u8>(),
323 size_of_val(self.read_slots),
324 )
325 });
326 // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
327 hasher.update(unsafe {
328 slice::from_raw_parts(
329 self.write_slots.as_ptr().cast::<u8>(),
330 size_of_val(self.write_slots),
331 )
332 });
333 // SAFETY: `u128` and can be treated as bytes
334 hasher.update(unsafe {
335 slice::from_raw_parts(
336 self.payload.as_ptr().cast::<u8>(),
337 size_of_val(self.payload),
338 )
339 });
340 hasher.update(self.seal);
341
342 TransactionHash(Blake3Hash::from(hasher.finalize()))
343 }
344
345 /// Read slots touched by the transaction.
346 ///
347 /// In contrast to `read_slots` property, this includes implicitly used slots.
348 pub fn read_slots(&self) -> impl Iterator<Item = TransactionSlot> {
349 // Slots included implicitly that are always used
350 let implicit_slots = [
351 TransactionSlot {
352 owner: self.header.contract,
353 contract: Address::SYSTEM_CODE,
354 },
355 // TODO: Uncomment once system token contract exists
356 // TransactionSlot {
357 // owner: self.header.contract,
358 // contract: Address::SYSTEM_TOKEN,
359 // },
360 ];
361
362 implicit_slots
363 .into_iter()
364 .chain(self.read_slots.iter().copied())
365 }
366
367 /// All slots touched by the transaction.
368 ///
369 /// In contrast to `read_slots` and `write_slots` properties, this includes implicitly used
370 /// slots.
371 pub fn slots(&self) -> impl Iterator<Item = TransactionSlot> {
372 self.read_slots().chain(self.write_slots.iter().copied())
373 }
374}