arb_storage/storage.rs
1use std::{marker::PhantomData, ptr::NonNull};
2
3use alloy_primitives::{Address, B256, U256};
4use arb_storage_errors::StorageError;
5use revm::Database;
6
7use crate::{
8 slot::{derive_sub_key, storage_key_map, storage_key_map_b256},
9 state_ops::{ARBOS_STATE_ADDRESS, read_storage_at, write_storage_at},
10};
11
12/// Phantom backend used by read paths that drive ArbOS accessors through a
13/// [`StorageBackend`] (such as the precompile handlers reading via
14/// `EvmInternals`). Direct state-pointer I/O on a `Storage<Detached>` is
15/// unreachable by construction — the type does not implement [`Database`].
16pub enum Detached {}
17
18/// Hierarchical storage abstraction over EVM account state.
19///
20/// Subspaces are derived from `base_key` using keccak-based mixing; direct
21/// state I/O (when available) targets `account`. The struct is parameterised
22/// over the executor's `Database` so the same shape can be inhabited by an
23/// executor `&'a mut State<D>` or by [`Detached`] for read paths that operate
24/// purely through a [`StorageBackend`].
25///
26/// The lifetime `'a` ties the handle to the originating `&mut State<D>` borrow
27/// so a `Storage` value cannot outlive the state it references. The state is
28/// held as a [`NonNull`] internally (with a [`PhantomData`] for the lifetime)
29/// so multiple disjoint subspaces over the same state can coexist; direct I/O
30/// goes through one `unsafe` deref controlled by the same SAFETY invariant
31/// described below.
32///
33/// # Safety
34///
35/// All direct I/O methods on `Storage<'a, D>` materialise `&mut *state` for the
36/// duration of a single SLOAD/SSTORE. The executor runs each block on a single
37/// thread and the EVM call graph is sequential, so no two such borrows overlap
38/// at runtime. The lifetime `'a` ensures the underlying `State<D>` cannot be
39/// dropped while `Storage` handles into it are alive.
40pub struct Storage<'a, D> {
41 state: NonNull<revm::database::State<D>>,
42 base_key: B256,
43 account: Address,
44 _marker: PhantomData<&'a mut revm::database::State<D>>,
45}
46
47impl<'a, D> Storage<'a, D> {
48 /// Creates a new Storage backed by the ArbOS state account.
49 pub fn new(state: &'a mut revm::database::State<D>, base_key: B256) -> Self {
50 Self {
51 // SAFETY: `state` is a live `&mut` so `NonNull::new_unchecked` is sound.
52 state: unsafe { NonNull::new_unchecked(state as *mut _) },
53 base_key,
54 account: ARBOS_STATE_ADDRESS,
55 _marker: PhantomData,
56 }
57 }
58
59 /// Creates a new Storage backed by a specific account.
60 pub fn new_with_account(
61 state: &'a mut revm::database::State<D>,
62 base_key: B256,
63 account: Address,
64 ) -> Self {
65 Self {
66 // SAFETY: `state` is a live `&mut` so `NonNull::new_unchecked` is sound.
67 state: unsafe { NonNull::new_unchecked(state as *mut _) },
68 base_key,
69 account,
70 _marker: PhantomData,
71 }
72 }
73
74 /// Opens a child subspace by hashing the parent key with the child ID.
75 pub fn open_sub_storage(&self, sub_key: &[u8]) -> Storage<'a, D> {
76 let new_key = derive_sub_key(self.base_key, sub_key);
77 Storage {
78 state: self.state,
79 base_key: new_key,
80 account: self.account,
81 _marker: PhantomData,
82 }
83 }
84
85 /// Opens a child subspace using a pre-derived key, avoiding a keccak hash.
86 pub fn open_sub_storage_with_key(&self, key: B256) -> Storage<'a, D> {
87 Storage {
88 state: self.state,
89 base_key: key,
90 account: self.account,
91 _marker: PhantomData,
92 }
93 }
94
95 /// Opens a sibling handle bound to the same backing state but targeting a
96 /// different account and `base_key`. Used when constructing accessors for
97 /// well-known non-ArbOS addresses (e.g. the filtered-transactions store).
98 pub fn open_account(&self, account: Address, base_key: B256) -> Storage<'a, D> {
99 Storage {
100 state: self.state,
101 base_key,
102 account,
103 _marker: PhantomData,
104 }
105 }
106
107 fn storage_key(&self) -> &[u8] {
108 if self.base_key == B256::ZERO {
109 &[]
110 } else {
111 self.base_key.as_slice()
112 }
113 }
114
115 fn compute_slot(&self, offset: u64) -> U256 {
116 storage_key_map(self.storage_key(), offset)
117 }
118
119 fn compute_slot_for_key(&self, key: B256) -> U256 {
120 storage_key_map_b256(self.storage_key(), &key.0)
121 }
122
123 /// Creates a StorageSlot handle for a specific offset.
124 pub fn new_slot(&self, offset: u64) -> U256 {
125 self.compute_slot(offset)
126 }
127
128 /// Computes the EVM slot for a `B256`-keyed entry under this subspace.
129 pub fn slot_for_key(&self, key: B256) -> U256 {
130 self.compute_slot_for_key(key)
131 }
132
133 /// Returns the base key for this storage subspace.
134 pub fn base_key(&self) -> B256 {
135 self.base_key
136 }
137
138 /// Returns the account address this storage subspace is bound to.
139 pub fn account(&self) -> Address {
140 self.account
141 }
142}
143
144impl Storage<'static, Detached> {
145 /// Builds a `Storage` handle that has no executor state pointer.
146 ///
147 /// All reads and writes must be routed through a [`StorageBackend`];
148 /// direct I/O methods on `Storage` are gated on `D: Database` and are
149 /// therefore inaccessible here.
150 pub fn detached(account: Address, base_key: B256) -> Self {
151 Self {
152 // SAFETY: `Detached` storage never has its state dereferenced —
153 // there is no `Database` impl that would expose the direct I/O
154 // surface. The pointer value here is intentionally dangling and
155 // unused.
156 state: NonNull::dangling(),
157 base_key,
158 account,
159 _marker: PhantomData,
160 }
161 }
162}
163
164impl<'a, D: Database> Storage<'a, D> {
165 /// Reads a 32-byte value by uint64 offset.
166 pub fn get_by_uint64(&self, offset: u64) -> Result<B256, StorageError> {
167 let slot = self.compute_slot(offset);
168 // SAFETY: see struct-level invariant.
169 let state = unsafe { &mut *self.state.as_ptr() };
170 read_storage_at(state, self.account, slot).map(B256::from)
171 }
172
173 /// Writes a 32-byte value by uint64 offset.
174 pub fn set_by_uint64(&self, offset: u64, value: B256) -> Result<(), StorageError> {
175 let slot = self.compute_slot(offset);
176 let value_u256 = U256::from_be_bytes(value.0);
177 // SAFETY: see struct-level invariant.
178 let state = unsafe { &mut *self.state.as_ptr() };
179 write_storage_at(state, self.account, slot, value_u256)
180 }
181
182 /// Reads a `u64` by uint64 offset, truncating values that exceed `u64::MAX`.
183 pub fn get_uint64_by_uint64(&self, offset: u64) -> Result<u64, StorageError> {
184 let slot = self.compute_slot(offset);
185 // SAFETY: see struct-level invariant.
186 let state = unsafe { &mut *self.state.as_ptr() };
187 let value = read_storage_at(state, self.account, slot)?;
188 Ok(value.try_into().unwrap_or(0))
189 }
190
191 /// Writes a `u64` by uint64 offset.
192 pub fn set_uint64_by_uint64(&self, offset: u64, value: u64) -> Result<(), StorageError> {
193 let slot = self.compute_slot(offset);
194 // SAFETY: see struct-level invariant.
195 let state = unsafe { &mut *self.state.as_ptr() };
196 write_storage_at(state, self.account, slot, U256::from(value))
197 }
198
199 /// Reads a 32-byte value by B256 key using mapAddress algorithm.
200 pub fn get(&self, key: B256) -> Result<B256, StorageError> {
201 let slot = self.compute_slot_for_key(key);
202 // SAFETY: see struct-level invariant.
203 let state = unsafe { &mut *self.state.as_ptr() };
204 read_storage_at(state, self.account, slot).map(B256::from)
205 }
206
207 /// Writes a 32-byte value by B256 key using mapAddress algorithm.
208 pub fn set(&self, key: B256, value: B256) -> Result<(), StorageError> {
209 let slot = self.compute_slot_for_key(key);
210 let value_u256 = U256::from_be_bytes(value.0);
211 // SAFETY: see struct-level invariant.
212 let state = unsafe { &mut *self.state.as_ptr() };
213 write_storage_at(state, self.account, slot, value_u256)
214 }
215}
216
217impl<'a, D> Storage<'a, D> {
218 /// Re-borrows the underlying state for direct `State<D>`-level operations
219 /// (e.g. account-level reads such as `get_account_balance` and writes such
220 /// as `set_account_nonce`/`set_account_code`).
221 ///
222 /// The returned reference inherits the lifetime parameter `'a` of the
223 /// `Storage`, so it is decoupled from any temporary borrow on `&self`.
224 /// This shape lets callers thread the same backing state through methods
225 /// that take `&mut self` without artificial borrow conflicts.
226 ///
227 /// # Safety
228 ///
229 /// arbreth's executor runs each block on a single thread and the EVM call
230 /// graph is sequential, so no two such borrows are live at the same time
231 /// at runtime. Callers must not nest two `state_mut()` returns from
232 /// overlapping `Storage` handles.
233 pub unsafe fn state_mut(&self) -> &'a mut revm::database::State<D> {
234 // SAFETY: see method-level invariant. `'a` ensures lifetime safety.
235 unsafe { &mut *self.state.as_ptr() }
236 }
237}
238
239impl<D> Clone for Storage<'_, D> {
240 fn clone(&self) -> Self {
241 Self {
242 state: self.state,
243 base_key: self.base_key,
244 account: self.account,
245 _marker: PhantomData,
246 }
247 }
248}
249
250// SAFETY: `Storage` over a `D: Send` state is safe to send between threads
251// when no concurrent access occurs. The arbreth executor runs each block on a
252// single thread; reentrant Stylus calls execute synchronously on the same
253// thread. The lifetime `'a` prevents the handle from outliving the source
254// state borrow.
255unsafe impl<D: Send> Send for Storage<'_, D> {}
256unsafe impl<D: Sync> Sync for Storage<'_, D> {}