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