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