ab_contracts_slots/
slots.rs

1use crate::aligned_buffer::{OwnedAlignedBuffer, SharedAlignedBuffer};
2use ab_contracts_common::Address;
3use alloc::boxed::Box;
4use smallvec::SmallVec;
5use tracing::debug;
6
7/// Small number of elements to store without heap allocation in some data structures.
8///
9/// This is both large enough for many practical use cases and small enough to bring significant
10/// performance improvement.
11const INLINE_SIZE: usize = 8;
12/// It should be rare that more than 2 contracts are created in the same transaction
13const NEW_CONTRACTS_INLINE: usize = 2;
14
15#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
16pub struct SlotKey {
17    pub owner: Address,
18    pub contract: Address,
19}
20
21#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
22pub struct SlotIndex(usize);
23
24impl From<SlotIndex> for usize {
25    #[inline(always)]
26    fn from(value: SlotIndex) -> Self {
27        value.0
28    }
29}
30
31#[derive(Debug, Clone)]
32enum Slot {
33    /// Original slot as given to the execution environment, not accessed yet
34    Original(SharedAlignedBuffer),
35    /// Original slot as given to the execution environment that is currently being accessed
36    OriginalAccessed(SharedAlignedBuffer),
37    /// Previously modified slot
38    Modified(SharedAlignedBuffer),
39    /// Previously modified slot that is currently being accessed for reads
40    ModifiedAccessed(SharedAlignedBuffer),
41    /// Original slot as given to the execution environment that is currently being modified
42    ReadWriteOriginal {
43        buffer: OwnedAlignedBuffer,
44        /// What it was in [`Self::Original`] before becoming [`Self::ReadWriteOriginal`]
45        previous: SharedAlignedBuffer,
46    },
47    /// Previously modified slot that is currently being modified
48    ReadWriteModified {
49        buffer: OwnedAlignedBuffer,
50        /// What it was in [`Self::ReadOnlyModified`] before becoming [`Self::ReadWriteModified`]
51        previous: SharedAlignedBuffer,
52    },
53}
54
55#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
56struct SlotAccess {
57    slot_index: SlotIndex,
58    /// `false` for read-only and `true` for read-write
59    read_write: bool,
60}
61
62#[derive(Debug)]
63struct Inner {
64    slots: SmallVec<[(SlotKey, Slot); INLINE_SIZE]>,
65    slot_access: SmallVec<[SlotAccess; INLINE_SIZE]>,
66    /// The list of new addresses that were created during transaction processing and couldn't be
67    /// known beforehand.
68    ///
69    /// Addresses in this list are allowed to create slots for any owner, and other contacts are
70    /// allowed to create slots owned by these addresses.
71    new_contracts: SmallVec<[Address; NEW_CONTRACTS_INLINE]>,
72}
73
74/// Container for `Slots` just to not expose this enum to the outside
75#[derive(Debug)]
76enum SlotsInner<'a> {
77    /// Original means instance was just created, no other related [`Slots`] instances (sharing the
78    /// same [`Inner`]) are accessible right now
79    Original { inner: Box<Inner> },
80    /// Similar to [`Self::Original`], but has a parent (another read-write instance or original)
81    ReadWrite {
82        inner: &'a mut Inner,
83        parent_slot_access_len: usize,
84    },
85    /// Read-only instance, non-exclusive access to [`Inner`], but not allowed to modify anything
86    ReadOnly { inner: &'a Inner },
87}
88
89// TODO: API for serialization/deserialization or some kind of access to internal contents
90#[derive(Debug)]
91pub struct Slots<'a>(SlotsInner<'a>);
92
93impl<'a> Drop for Slots<'a> {
94    #[inline(always)]
95    fn drop(&mut self) {
96        let (inner, parent_slot_access_len) = match &mut self.0 {
97            SlotsInner::Original { .. } | SlotsInner::ReadOnly { .. } => {
98                // No need to integrate changes into the parent
99                return;
100            }
101            SlotsInner::ReadWrite {
102                inner,
103                parent_slot_access_len,
104            } => (&mut **inner, *parent_slot_access_len),
105        };
106
107        let slots = &mut inner.slots;
108        let slot_access = &mut inner.slot_access;
109
110        // Fix-up slots that were modified during access
111        for slot_access in slot_access.drain(parent_slot_access_len..) {
112            let slot = &mut slots
113                .get_mut(usize::from(slot_access.slot_index))
114                .expect("Accessed slot exists; qed")
115                .1;
116
117            take_mut::take(slot, |slot| match slot {
118                Slot::Original(_buffer) => {
119                    unreachable!("Slot can't be in Original state after being accessed")
120                }
121                Slot::OriginalAccessed(buffer) => Slot::Original(buffer),
122                Slot::Modified(buffer) => Slot::Modified(buffer),
123                Slot::ModifiedAccessed(buffer) => Slot::Modified(buffer),
124                Slot::ReadWriteOriginal { buffer, .. } | Slot::ReadWriteModified { buffer, .. } => {
125                    Slot::Modified(buffer.into_shared())
126                }
127            })
128        }
129    }
130}
131
132// TODO: Method for getting all slots and modified slots, take `#[tmp]` into consideration
133impl<'a> Slots<'a> {
134    /// Create a new instance from a hashmap containing existing slots.
135    ///
136    /// Only slots that are present in the input can be modified. The only exception is slots for
137    /// owners created during runtime and initialized with [`Self::add_new_contract()`].
138    ///
139    /// "Empty" slots must still have a value in the form of an empty [`SharedAlignedBuffer`].
140    #[inline(always)]
141    pub fn new<I>(slots: I) -> Self
142    where
143        I: IntoIterator<Item = (SlotKey, SharedAlignedBuffer)>,
144    {
145        let slots = slots
146            .into_iter()
147            .filter_map(|(slot_key, slot)| {
148                // `Address::NULL` is used for `#[tmp]` and is ephemeral. Reads and writes are
149                // allowed for any owner, and they will all be thrown away after transaction
150                // processing if finished.
151                if slot_key.contract == Address::NULL {
152                    return None;
153                }
154
155                Some((slot_key, Slot::Original(slot)))
156            })
157            .collect();
158
159        let inner = Inner {
160            slots,
161            slot_access: SmallVec::new(),
162            new_contracts: SmallVec::new(),
163        };
164
165        Self(SlotsInner::Original {
166            inner: Box::new(inner),
167        })
168    }
169
170    #[inline(always)]
171    fn inner_ro(&self) -> &Inner {
172        match &self.0 {
173            SlotsInner::Original { inner } => inner,
174            SlotsInner::ReadWrite { inner, .. } => inner,
175            SlotsInner::ReadOnly { inner } => inner,
176        }
177    }
178
179    #[inline(always)]
180    fn inner_rw(&mut self) -> Option<&mut Inner> {
181        match &mut self.0 {
182            SlotsInner::Original { inner } => Some(inner),
183            SlotsInner::ReadWrite { inner, .. } => Some(inner),
184            SlotsInner::ReadOnly { .. } => None,
185        }
186    }
187
188    /// Create a new nested read-write slots instance.
189    ///
190    /// Nested instance will integrate its changes into the parent slot when dropped (or changes can
191    /// be reset with [`Self::reset()`]).
192    ///
193    /// Returns `None` when attempted on read-only instance.
194    #[inline(always)]
195    pub fn new_nested_rw<'b>(&'b mut self) -> Option<Slots<'b>>
196    where
197        'a: 'b,
198    {
199        let inner = match &mut self.0 {
200            SlotsInner::Original { inner } => inner.as_mut(),
201            SlotsInner::ReadWrite { inner, .. } => inner,
202            SlotsInner::ReadOnly { .. } => {
203                return None;
204            }
205        };
206
207        let parent_slot_access_len = inner.slot_access.len();
208
209        Some(Slots(SlotsInner::ReadWrite {
210            inner,
211            parent_slot_access_len,
212        }))
213    }
214
215    /// Create a new nested read-only slots instance
216    #[inline(always)]
217    pub fn new_nested_ro<'b>(&'b self) -> Slots<'b>
218    where
219        'a: 'b,
220    {
221        let inner = match &self.0 {
222            SlotsInner::Original { inner } => inner.as_ref(),
223            SlotsInner::ReadWrite { inner, .. } => inner,
224            SlotsInner::ReadOnly { inner } => inner,
225        };
226
227        Slots(SlotsInner::ReadOnly { inner })
228    }
229
230    /// Add a new contract that didn't exist before.
231    ///
232    /// In contrast to contracts in [`Self::new()`], this contract will be allowed to have any slots
233    /// related to it being modified.
234    ///
235    /// Returns `false` if a contract already exits in a map, which is also considered as an access
236    /// violation.
237    #[must_use]
238    #[inline(always)]
239    pub fn add_new_contract(&mut self, owner: Address) -> bool {
240        let Some(inner) = self.inner_rw() else {
241            debug!(%owner, "`add_new_contract` access violation");
242            return false;
243        };
244
245        let new_contracts = &mut inner.new_contracts;
246
247        if new_contracts.contains(&owner) {
248            debug!(%owner, "Not adding new contract duplicate");
249            return false;
250        }
251
252        new_contracts.push(owner);
253        true
254    }
255
256    /// Get code for `owner`.
257    ///
258    /// The biggest difference from [`Self::use_ro()`] is that the slot is not marked as used,
259    /// instead the current code is cloned and returned.
260    ///
261    /// Returns `None` in case of access violation or if code is missing.
262    #[inline(always)]
263    pub fn get_code(&self, owner: Address) -> Option<SharedAlignedBuffer> {
264        let result = self.get_code_internal(owner);
265
266        if result.is_none() {
267            debug!(%owner, "`get_code` access violation");
268        }
269
270        result
271    }
272
273    #[inline(always)]
274    fn get_code_internal(&self, owner: Address) -> Option<SharedAlignedBuffer> {
275        let inner = self.inner_ro();
276        let slots = &inner.slots;
277        let slot_access = &inner.slot_access;
278
279        let contract = Address::SYSTEM_CODE;
280
281        let slot_index = slots.iter().position(|(slot_key, _slot)| {
282            slot_key.owner == owner && slot_key.contract == contract
283        })?;
284        let slot_index = SlotIndex(slot_index);
285
286        // Ensure code is not currently being written to
287        if slot_access
288            .iter()
289            .any(|slot_access| slot_access.slot_index == slot_index && slot_access.read_write)
290        {
291            return None;
292        }
293
294        let buffer = match &slots
295            .get(usize::from(slot_index))
296            .expect("Just found; qed")
297            .1
298        {
299            Slot::Original(buffer)
300            | Slot::OriginalAccessed(buffer)
301            | Slot::Modified(buffer)
302            | Slot::ModifiedAccessed(buffer) => buffer,
303            Slot::ReadWriteOriginal { .. } | Slot::ReadWriteModified { .. } => {
304                return None;
305            }
306        };
307
308        Some(buffer.clone())
309    }
310
311    /// Read-only access to a slot with specified owner and contract, marks it as used.
312    ///
313    /// Returns `None` in case of access violation.
314    #[inline(always)]
315    pub fn use_ro(&mut self, slot_key: SlotKey) -> Option<&SharedAlignedBuffer> {
316        let inner_rw = match &mut self.0 {
317            SlotsInner::Original { inner, .. } => inner.as_mut(),
318            SlotsInner::ReadWrite { inner, .. } => inner,
319            SlotsInner::ReadOnly { inner } => {
320                // Simplified version that doesn't do access tracking
321                let result = Self::use_ro_internal_read_only(
322                    slot_key,
323                    &inner.slots,
324                    &inner.slot_access,
325                    &inner.new_contracts,
326                );
327
328                if result.is_none() {
329                    debug!(?slot_key, "`use_ro` access violation");
330                }
331
332                return result;
333            }
334        };
335
336        let result = Self::use_ro_internal(
337            slot_key,
338            &mut inner_rw.slots,
339            &mut inner_rw.slot_access,
340            &inner_rw.new_contracts,
341        );
342
343        if result.is_none() {
344            debug!(?slot_key, "`use_ro` access violation");
345        }
346
347        result
348    }
349
350    #[inline(always)]
351    fn use_ro_internal<'b>(
352        slot_key: SlotKey,
353        slots: &'b mut SmallVec<[(SlotKey, Slot); INLINE_SIZE]>,
354        slot_access: &mut SmallVec<[SlotAccess; INLINE_SIZE]>,
355        new_contracts: &[Address],
356    ) -> Option<&'b SharedAlignedBuffer> {
357        let maybe_slot_index = slots
358            .iter()
359            .position(|(slot_key_candidate, _slot)| slot_key_candidate == &slot_key)
360            .map(SlotIndex);
361
362        if let Some(slot_index) = maybe_slot_index {
363            // Ensure that slot is not currently being written to
364            if let Some(read_write) = slot_access.iter().find_map(|slot_access| {
365                (slot_access.slot_index == slot_index).then_some(slot_access.read_write)
366            }) {
367                if read_write {
368                    return None;
369                }
370            } else {
371                slot_access.push(SlotAccess {
372                    slot_index,
373                    read_write: false,
374                });
375            }
376
377            let slot = &mut slots
378                .get_mut(usize::from(slot_index))
379                .expect("Just found; qed")
380                .1;
381
382            // The slot that is currently being written to is not allowed for read access
383            match slot {
384                Slot::Original(buffer) => {
385                    let buffer = buffer.clone();
386                    *slot = Slot::OriginalAccessed(buffer);
387                    let Slot::OriginalAccessed(buffer) = slot else {
388                        unreachable!("Just inserted; qed");
389                    };
390                    Some(buffer)
391                }
392                Slot::OriginalAccessed(buffer) | Slot::ModifiedAccessed(buffer) => Some(buffer),
393                Slot::Modified(buffer) => {
394                    let buffer = buffer.clone();
395                    *slot = Slot::ModifiedAccessed(buffer);
396                    let Slot::ModifiedAccessed(buffer) = slot else {
397                        unreachable!("Just inserted; qed");
398                    };
399                    Some(buffer)
400                }
401                Slot::ReadWriteOriginal { .. } | Slot::ReadWriteModified { .. } => None,
402            }
403        } else {
404            // `Address::NULL` is used for `#[tmp]` and is ephemeral. Reads and writes are allowed
405            // for any owner, and they will all be thrown away after transaction processing if
406            // finished.
407            if !(slot_key.contract == Address::NULL
408                || new_contracts
409                    .iter()
410                    .any(|candidate| candidate == slot_key.owner || candidate == slot_key.contract))
411            {
412                return None;
413            }
414
415            slot_access.push(SlotAccess {
416                slot_index: SlotIndex(slots.len()),
417                read_write: false,
418            });
419
420            let slot = Slot::OriginalAccessed(SharedAlignedBuffer::default());
421            slots.push((slot_key, slot));
422            let slot = &slots.last().expect("Just inserted; qed").1;
423            let Slot::OriginalAccessed(buffer) = slot else {
424                unreachable!("Just inserted; qed");
425            };
426
427            Some(buffer)
428        }
429    }
430
431    /// Similar to [`Self::use_ro_internal()`], but for read-only instance
432    #[inline(always)]
433    fn use_ro_internal_read_only<'b>(
434        slot_key: SlotKey,
435        slots: &'b SmallVec<[(SlotKey, Slot); INLINE_SIZE]>,
436        slot_access: &SmallVec<[SlotAccess; INLINE_SIZE]>,
437        new_contracts: &[Address],
438    ) -> Option<&'b SharedAlignedBuffer> {
439        let maybe_slot_index = slots
440            .iter()
441            .position(|(slot_key_candidate, _slot)| slot_key_candidate == &slot_key)
442            .map(SlotIndex);
443
444        if let Some(slot_index) = maybe_slot_index {
445            // Ensure that slot is not currently being written to
446            if let Some(read_write) = slot_access.iter().find_map(|slot_access| {
447                (slot_access.slot_index == slot_index).then_some(slot_access.read_write)
448            }) {
449                if read_write {
450                    return None;
451                }
452            }
453
454            let slot = &slots
455                .get(usize::from(slot_index))
456                .expect("Just found; qed")
457                .1;
458
459            // The slot that is currently being written to is not allowed for read access
460            match slot {
461                Slot::Original(buffer)
462                | Slot::OriginalAccessed(buffer)
463                | Slot::ModifiedAccessed(buffer)
464                | Slot::Modified(buffer) => Some(buffer),
465                Slot::ReadWriteOriginal { .. } | Slot::ReadWriteModified { .. } => None,
466            }
467        } else {
468            // `Address::NULL` is used for `#[tmp]` and is ephemeral. Reads and writes are
469            // allowed for any owner, and they will all be thrown away after transaction
470            // processing if finished.
471            if !(slot_key.contract == Address::NULL
472                || new_contracts
473                    .iter()
474                    .any(|candidate| candidate == slot_key.owner || candidate == slot_key.contract))
475            {
476                return None;
477            }
478
479            Some(SharedAlignedBuffer::empty_ref())
480        }
481    }
482
483    /// Read-write access to a slot with specified owner and contract, marks it as used.
484    ///
485    /// The returned slot is no longer accessible through [`Self::use_ro()`] or [`Self::use_rw()`]
486    /// during the lifetime of this `Slot` instance (and can be safely turned into a pointer). The
487    /// only way to get another mutable reference is to call [`Self::access_used_rw()`].
488    ///
489    /// Returns `None` in case of access violation.
490    #[inline(always)]
491    pub fn use_rw(
492        &mut self,
493        slot_key: SlotKey,
494        capacity: u32,
495    ) -> Option<(SlotIndex, &mut OwnedAlignedBuffer)> {
496        let inner = self.inner_rw()?;
497        let slots = &mut inner.slots;
498        let slot_access = &mut inner.slot_access;
499        let new_contracts = &inner.new_contracts;
500
501        let result = Self::use_rw_internal(slot_key, capacity, slots, slot_access, new_contracts);
502
503        if result.is_none() {
504            debug!(?slot_key, "`use_rw` access violation");
505        }
506
507        result
508    }
509
510    #[inline(always)]
511    fn use_rw_internal<'b>(
512        slot_key: SlotKey,
513        capacity: u32,
514        slots: &'b mut SmallVec<[(SlotKey, Slot); INLINE_SIZE]>,
515        slot_access: &mut SmallVec<[SlotAccess; INLINE_SIZE]>,
516        new_contracts: &[Address],
517    ) -> Option<(SlotIndex, &'b mut OwnedAlignedBuffer)> {
518        let maybe_slot_index = slots
519            .iter()
520            .position(|(slot_key_candidate, _slot)| slot_key_candidate == &slot_key)
521            .map(SlotIndex);
522
523        if let Some(slot_index) = maybe_slot_index {
524            // Ensure that slot is not accessed right now
525            if slot_access
526                .iter()
527                .any(|slot_access| slot_access.slot_index == slot_index)
528            {
529                return None;
530            }
531
532            slot_access.push(SlotAccess {
533                slot_index,
534                read_write: true,
535            });
536
537            let slot = &mut slots
538                .get_mut(usize::from(slot_index))
539                .expect("Just found; qed")
540                .1;
541
542            // The slot that is currently being accessed to is not allowed for writing
543            let buffer = match slot {
544                Slot::OriginalAccessed(_buffer) | Slot::ModifiedAccessed(_buffer) => {
545                    return None;
546                }
547                Slot::Original(buffer) => {
548                    let mut new_buffer =
549                        OwnedAlignedBuffer::with_capacity(capacity.max(buffer.len()));
550                    new_buffer.copy_from_slice(buffer.as_slice());
551
552                    *slot = Slot::ReadWriteOriginal {
553                        buffer: new_buffer,
554                        previous: buffer.clone(),
555                    };
556                    let Slot::ReadWriteOriginal { buffer, .. } = slot else {
557                        unreachable!("Just inserted; qed");
558                    };
559                    buffer
560                }
561                Slot::Modified(buffer) => {
562                    let mut new_buffer =
563                        OwnedAlignedBuffer::with_capacity(capacity.max(buffer.len()));
564                    new_buffer.copy_from_slice(buffer.as_slice());
565
566                    *slot = Slot::ReadWriteModified {
567                        buffer: new_buffer,
568                        previous: buffer.clone(),
569                    };
570                    let Slot::ReadWriteModified { buffer, .. } = slot else {
571                        unreachable!("Just inserted; qed");
572                    };
573                    buffer
574                }
575                Slot::ReadWriteOriginal { buffer, .. } | Slot::ReadWriteModified { buffer, .. } => {
576                    buffer.ensure_capacity(capacity);
577                    buffer
578                }
579            };
580
581            Some((slot_index, buffer))
582        } else {
583            // `Address::NULL` is used for `#[tmp]` and is ephemeral. Reads and writes are allowed
584            // for any owner, and they will all be thrown away after transaction processing if
585            // finished.
586            if !(slot_key.contract == Address::NULL
587                || new_contracts
588                    .iter()
589                    .any(|candidate| candidate == slot_key.owner || candidate == slot_key.contract))
590            {
591                return None;
592            }
593
594            let slot_index = SlotIndex(slots.len());
595            slot_access.push(SlotAccess {
596                slot_index,
597                read_write: true,
598            });
599
600            let slot = Slot::ReadWriteOriginal {
601                buffer: OwnedAlignedBuffer::with_capacity(capacity),
602                previous: SharedAlignedBuffer::default(),
603            };
604            slots.push((slot_key, slot));
605            let slot = &mut slots.last_mut().expect("Just inserted; qed").1;
606            let Slot::ReadWriteOriginal { buffer, .. } = slot else {
607                unreachable!("Just inserted; qed");
608            };
609
610            Some((slot_index, buffer))
611        }
612    }
613
614    /// Read-write access to a slot with specified owner and contract, that is currently marked as
615    /// used due to earlier call to [`Self::use_rw()`].
616    ///
617    /// NOTE: Calling this method means that any pointers that might have been stored to the result
618    /// of [`Self::use_rw()`] call are now invalid!
619    ///
620    /// Returns `None` in case of access violation.
621    pub fn access_used_rw(&mut self, slot_index: SlotIndex) -> Option<&mut OwnedAlignedBuffer> {
622        let maybe_slot = self
623            .inner_rw()?
624            .slots
625            .get_mut(usize::from(slot_index))
626            .map(|(_slot_key, slot)| slot);
627
628        let Some(slot) = maybe_slot else {
629            debug!(?slot_index, "`access_used_rw` access violation (not found)");
630            return None;
631        };
632
633        // Must be currently accessed for writing
634        match slot {
635            Slot::Original(_buffer)
636            | Slot::OriginalAccessed(_buffer)
637            | Slot::Modified(_buffer)
638            | Slot::ModifiedAccessed(_buffer) => {
639                debug!(?slot_index, "`access_used_rw` access violation (read only)");
640                None
641            }
642            Slot::ReadWriteOriginal { buffer, .. } | Slot::ReadWriteModified { buffer, .. } => {
643                Some(buffer)
644            }
645        }
646    }
647
648    /// Reset any changes that might have been done on this level
649    #[cold]
650    pub fn reset(&mut self) {
651        let (inner, parent_slot_access_len) = match &mut self.0 {
652            SlotsInner::Original { .. } | SlotsInner::ReadOnly { .. } => {
653                // No need to integrate changes into the parent
654                return;
655            }
656            SlotsInner::ReadWrite {
657                inner,
658                parent_slot_access_len,
659            } => (&mut **inner, parent_slot_access_len),
660        };
661
662        let slots = &mut inner.slots;
663        let slot_access = &mut inner.slot_access;
664
665        // Fix-up slots that were modified during access
666        for slot_access in slot_access.drain(*parent_slot_access_len..) {
667            let slot = &mut slots
668                .get_mut(usize::from(slot_access.slot_index))
669                .expect("Accessed slot exists; qed")
670                .1;
671            take_mut::take(slot, |slot| match slot {
672                Slot::Original(_buffer) => {
673                    unreachable!("Slot can't be in Original state after being accessed")
674                }
675                Slot::OriginalAccessed(buffer) => Slot::Original(buffer),
676                Slot::Modified(buffer) => Slot::Modified(buffer),
677                Slot::ModifiedAccessed(buffer) => Slot::Modified(buffer),
678                Slot::ReadWriteOriginal { previous, .. } => Slot::Original(previous),
679                Slot::ReadWriteModified { previous, .. } => Slot::Modified(previous),
680            });
681        }
682
683        *parent_slot_access_len = 0;
684    }
685}