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: Some more complex field?
61 /// Transaction version
62 pub version: u64,
63 /// Block root at which transaction was created
64 pub block_root: BlockRoot,
65 /// Gas limit
66 pub gas_limit: Gas,
67 /// Contract implementing `TxHandler` trait to use for transaction verification and execution
68 pub contract: Address,
69}
70
71impl TransactionHeader {
72 /// The only supported transaction version right now
73 pub const TRANSACTION_VERSION: u64 = 0;
74}
75
76/// Transaction slot
77#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, TrivialType)]
78#[repr(C)]
79pub struct TransactionSlot {
80 /// Slot owner
81 pub owner: Address,
82 /// Contract that manages the slot
83 pub contract: Address,
84}
85
86/// Lengths of various components in a serialized version of [`Transaction`]
87#[derive(Debug, Default, Copy, Clone, TrivialType)]
88#[repr(C)]
89pub struct SerializedTransactionLengths {
90 /// Number of read-only slots
91 pub read_slots: u16,
92 /// Number of read-write slots
93 pub write_slots: u16,
94 /// Payload length
95 pub payload: u32,
96 /// Seal length
97 pub seal: u32,
98 /// Not used and must be set to `0`
99 pub padding: [u8; 4],
100}
101
102/// Similar to `Transaction`, but doesn't require `allow` or data ownership.
103///
104/// Can be created with `Transaction::as_ref()` call.
105#[derive(Debug, Copy, Clone)]
106pub struct Transaction<'a> {
107 /// Transaction header
108 pub header: &'a TransactionHeader,
109 /// Slots in the form of [`TransactionSlot`] that may be read during transaction processing.
110 ///
111 /// These are the only slots that can be used in authorization code.
112 ///
113 /// The code slot of the contract that is being executed and balance of native token are
114 /// implicitly included and doesn't need to be specified (see [`Transaction::read_slots()`].
115 /// Also slots that may also be written to do not need to be repeated in the read slots.
116 pub read_slots: &'a [TransactionSlot],
117 /// Slots in the form of [`TransactionSlot`] that may be written during transaction processing
118 pub write_slots: &'a [TransactionSlot],
119 /// Transaction payload
120 pub payload: &'a [u128],
121 /// Transaction seal
122 pub seal: &'a [u8],
123}
124
125impl<'a> Transaction<'a> {
126 /// Create an instance from provided correctly aligned bytes.
127 ///
128 /// `bytes` should be 16-bytes aligned.
129 ///
130 /// See [`Self::from_bytes_unchecked()`] for layout details.
131 ///
132 /// Returns an instance and remaining bytes on success.
133 #[inline]
134 pub fn try_from_bytes(mut bytes: &'a [u8]) -> Option<(Self, &'a [u8])> {
135 if !bytes.as_ptr().cast::<u128>().is_aligned()
136 || bytes.len()
137 < size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>()
138 {
139 return None;
140 }
141
142 // SAFETY: Checked above that there are enough bytes and they are correctly aligned
143 let lengths = unsafe {
144 bytes
145 .as_ptr()
146 .add(size_of::<TransactionHeader>())
147 .cast::<SerializedTransactionLengths>()
148 .read()
149 };
150 let SerializedTransactionLengths {
151 read_slots,
152 write_slots,
153 payload,
154 seal,
155 padding,
156 } = lengths;
157
158 if padding != [0; _] {
159 return None;
160 }
161
162 if payload % u128::SIZE != 0 {
163 return None;
164 }
165
166 let size = (size_of::<TransactionHeader>() + size_of::<SerializedTransactionLengths>())
167 .checked_add(usize::from(read_slots) * size_of::<TransactionSlot>())?
168 .checked_add(usize::from(write_slots) * size_of::<TransactionSlot>())?
169 .checked_add(payload as usize * size_of::<u128>())?
170 .checked_add(seal as usize)?;
171
172 if bytes.len() < size {
173 return None;
174 }
175
176 // SAFETY: Size and alignment checked above
177 let transaction = unsafe { Self::from_bytes_unchecked(bytes) };
178 let remainder = bytes.split_off(transaction.encoded_size()..)?;
179
180 Some((transaction, remainder))
181 }
182
183 /// Create an instance from provided bytes without performing any checks for size or alignment.
184 ///
185 /// The internal layout of the owned transaction is following data structures concatenated as
186 /// bytes (they are carefully picked to ensure alignment):
187 /// * [`TransactionHeader`]
188 /// * [`SerializedTransactionLengths`] (with values set to correspond to below contents)
189 /// * All read [`TransactionSlot`]
190 /// * All write [`TransactionSlot`]
191 /// * Payload as `u128`s
192 /// * Seal as `u8`s
193 ///
194 /// # Safety
195 /// Caller must ensure provided bytes are 16-bytes aligned and of sufficient length. Extra bytes
196 /// beyond necessary are silently ignored if provided.
197 #[inline]
198 pub unsafe fn from_bytes_unchecked(bytes: &'a [u8]) -> Transaction<'a> {
199 // SAFETY: Method contract guarantees size and alignment
200 let lengths = unsafe {
201 bytes
202 .as_ptr()
203 .add(size_of::<TransactionHeader>())
204 .cast::<SerializedTransactionLengths>()
205 .read()
206 };
207 let SerializedTransactionLengths {
208 read_slots,
209 write_slots,
210 payload,
211 seal,
212 padding: _,
213 } = lengths;
214
215 Self {
216 // SAFETY: Any bytes are valid for `TransactionHeader` and all method contract
217 // guarantees there are enough bytes for header in the buffer
218 header: unsafe {
219 bytes
220 .as_ptr()
221 .cast::<TransactionHeader>()
222 .as_ref_unchecked()
223 },
224 // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
225 // there are enough bytes for read slots in the buffer
226 read_slots: unsafe {
227 slice::from_raw_parts(
228 bytes
229 .as_ptr()
230 .add(size_of::<TransactionHeader>())
231 .add(size_of::<SerializedTransactionLengths>())
232 .cast::<TransactionSlot>(),
233 usize::from(read_slots),
234 )
235 },
236 // SAFETY: Any bytes are valid for `TransactionSlot` and all method contract guarantees
237 // there are enough bytes for write slots in the buffer
238 write_slots: unsafe {
239 slice::from_raw_parts(
240 bytes
241 .as_ptr()
242 .add(size_of::<TransactionHeader>())
243 .add(size_of::<SerializedTransactionLengths>())
244 .cast::<TransactionSlot>()
245 .add(usize::from(read_slots)),
246 usize::from(write_slots),
247 )
248 },
249 // SAFETY: Any bytes are valid for `payload` and all method contract guarantees there
250 // are enough bytes for payload in the buffer
251 payload: unsafe {
252 slice::from_raw_parts(
253 bytes
254 .as_ptr()
255 .add(size_of::<TransactionHeader>())
256 .add(size_of::<SerializedTransactionLengths>())
257 .add(
258 size_of::<TransactionSlot>()
259 * (usize::from(read_slots) + usize::from(write_slots)),
260 )
261 .cast::<u128>(),
262 payload as usize,
263 )
264 },
265 // SAFETY: Any bytes are valid for `seal` and all method contract guarantees there are
266 // enough bytes for seal in the buffer
267 seal: unsafe {
268 slice::from_raw_parts(
269 bytes
270 .as_ptr()
271 .add(size_of::<TransactionHeader>())
272 .add(size_of::<SerializedTransactionLengths>())
273 .add(
274 size_of::<TransactionSlot>()
275 * (usize::from(read_slots) + usize::from(write_slots))
276 + payload as usize,
277 ),
278 seal as usize,
279 )
280 },
281 }
282 }
283
284 /// Create an owned version of this transaction
285 #[cfg(feature = "alloc")]
286 #[inline(always)]
287 pub fn to_owned(self) -> Result<OwnedTransaction, OwnedTransactionError> {
288 OwnedTransaction::from_transaction(self)
289 }
290
291 /// Size of the encoded transaction in bytes
292 pub const fn encoded_size(&self) -> usize {
293 size_of::<TransactionHeader>()
294 + size_of::<SerializedTransactionLengths>()
295 + size_of_val(self.read_slots)
296 + size_of_val(self.write_slots)
297 + size_of_val(self.payload)
298 + size_of_val(self.seal)
299 }
300
301 /// Compute transaction hash.
302 ///
303 /// Note: this computes transaction hash on every call, so worth caching if it is expected to be
304 /// called often.
305 pub fn hash(&self) -> TransactionHash {
306 // TODO: Keyed hash
307 let mut hasher = Hasher::new();
308
309 hasher.update(self.header.as_bytes());
310 // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
311 hasher.update(unsafe {
312 slice::from_raw_parts(
313 self.read_slots.as_ptr().cast::<u8>(),
314 size_of_val(self.read_slots),
315 )
316 });
317 // SAFETY: `TransactionSlot` is `TrivialType` and can be treated as bytes
318 hasher.update(unsafe {
319 slice::from_raw_parts(
320 self.write_slots.as_ptr().cast::<u8>(),
321 size_of_val(self.write_slots),
322 )
323 });
324 // SAFETY: `u128` and can be treated as bytes
325 hasher.update(unsafe {
326 slice::from_raw_parts(
327 self.payload.as_ptr().cast::<u8>(),
328 size_of_val(self.payload),
329 )
330 });
331 hasher.update(self.seal);
332
333 TransactionHash(Blake3Hash::from(hasher.finalize()))
334 }
335
336 /// Read slots touched by the transaction.
337 ///
338 /// In contrast to `read_slots` property, this includes implicitly used slots.
339 pub fn read_slots(&self) -> impl Iterator<Item = TransactionSlot> {
340 // Slots included implicitly that are always used
341 let implicit_slots = [
342 TransactionSlot {
343 owner: self.header.contract,
344 contract: Address::SYSTEM_CODE,
345 },
346 // TODO: Uncomment once system token contract exists
347 // TransactionSlot {
348 // owner: self.header.contract,
349 // contract: Address::SYSTEM_TOKEN,
350 // },
351 ];
352
353 implicit_slots
354 .into_iter()
355 .chain(self.read_slots.iter().copied())
356 }
357
358 /// All slots touched by the transaction.
359 ///
360 /// In contrast to `read_slots` and `write_slots` properties, this includes implicitly used
361 /// slots.
362 pub fn slots(&self) -> impl Iterator<Item = TransactionSlot> {
363 self.read_slots().chain(self.write_slots.iter().copied())
364 }
365}