Skip to main content

tempo_precompiles/storage/
thread_local.rs

1use alloy::primitives::{Address, B256, LogData, U256};
2use alloy_evm::{Database, EvmInternals};
3use revm::{
4    context::{Block, CfgEnv, JournalTr, Transaction, journaled_state::JournalCheckpoint},
5    state::{AccountInfo, Bytecode},
6};
7use scoped_tls::scoped_thread_local;
8use std::{cell::RefCell, fmt::Debug};
9use tempo_chainspec::hardfork::TempoHardfork;
10
11use crate::{
12    Precompile,
13    error::{Result, TempoPrecompileError},
14    storage::{PrecompileStorageProvider, evm::EvmPrecompileStorageProvider},
15};
16
17scoped_thread_local!(static STORAGE: RefCell<&mut dyn PrecompileStorageProvider>);
18
19/// Thread-local storage accessor that implements `PrecompileStorageProvider` without the trait bound.
20///
21/// This is the only type that exposes access to the thread-local `STORAGE` static.
22///
23/// # Important
24///
25/// Since it provides access to the current thread-local storage context, it MUST be used within
26/// a `StorageCtx::enter` closure.
27///
28/// # Sync with `PrecompileStorageProvider`
29///
30/// This type mirrors `PrecompileStorageProvider` methods but with split mutability:
31/// - Read operations (staticcall) take `&self`
32/// - Write operations take `&mut self`
33#[derive(Debug, Default, Clone, Copy)]
34pub struct StorageCtx;
35
36impl StorageCtx {
37    /// Enter storage context. All storage operations must happen within the closure.
38    ///
39    /// # IMPORTANT
40    ///
41    /// The caller must ensure that:
42    /// 1. Only one `enter` call is active at a time, in the same thread.
43    /// 2. If multiple storage providers are instantiated in parallel threads,
44    ///    they CANNOT point to the same storage addresses.
45    pub fn enter<S, R>(storage: &mut S, f: impl FnOnce() -> R) -> R
46    where
47        S: PrecompileStorageProvider,
48    {
49        // SAFETY: `scoped_tls` ensures the pointer is only accessible within the closure scope.
50        let storage: &mut dyn PrecompileStorageProvider = storage;
51        let storage_static: &mut (dyn PrecompileStorageProvider + 'static) =
52            unsafe { std::mem::transmute(storage) };
53        let cell = RefCell::new(storage_static);
54        STORAGE.set(&cell, f)
55    }
56
57    /// Execute an infallible function with access to the current thread-local storage provider.
58    ///
59    /// # Panics
60    /// Panics if no storage context is set.
61    fn with_storage<F, R>(f: F) -> R
62    where
63        F: FnOnce(&mut dyn PrecompileStorageProvider) -> R,
64    {
65        assert!(
66            STORAGE.is_set(),
67            "No storage context. 'StorageCtx::enter' must be called first"
68        );
69        STORAGE.with(|cell| {
70            // SAFETY: `scoped_tls` ensures the pointer is only accessible within the closure scope.
71            // Holding the guard prevents re-entrant borrows.
72            let mut guard = cell.borrow_mut();
73            f(&mut **guard)
74        })
75    }
76
77    /// Execute a (fallible) function with access to the current thread-local storage provider.
78    fn try_with_storage<F, R>(f: F) -> Result<R>
79    where
80        F: FnOnce(&mut dyn PrecompileStorageProvider) -> Result<R>,
81    {
82        if !STORAGE.is_set() {
83            return Err(TempoPrecompileError::Fatal(
84                "No storage context. 'StorageCtx::enter' must be called first".to_string(),
85            ));
86        }
87        STORAGE.with(|cell| {
88            // SAFETY: `scoped_tls` ensures the pointer is only accessible within the closure scope.
89            // Holding the guard prevents re-entrant borrows.
90            let mut guard = cell.borrow_mut();
91            f(&mut **guard)
92        })
93    }
94
95    // `PrecompileStorageProvider` methods (with modified mutability for read-only methods)
96
97    /// Executes a closure with access to the account info, returning the closure's result.
98    ///
99    /// This is an ergonomic wrapper that flattens the Result, avoiding double `?`.
100    pub fn with_account_info<T>(
101        &self,
102        address: Address,
103        mut f: impl FnMut(&AccountInfo) -> Result<T>,
104    ) -> Result<T> {
105        let mut result: Option<Result<T>> = None;
106        Self::try_with_storage(|s| {
107            s.with_account_info(address, &mut |info| {
108                result = Some(f(info));
109            })
110        })?;
111        result.unwrap()
112    }
113
114    /// Returns the chain ID.
115    pub fn chain_id(&self) -> u64 {
116        Self::with_storage(|s| s.chain_id())
117    }
118
119    /// Returns the current block timestamp.
120    pub fn timestamp(&self) -> U256 {
121        Self::with_storage(|s| s.timestamp())
122    }
123
124    /// Returns the current block beneficiary (coinbase).
125    pub fn beneficiary(&self) -> Address {
126        Self::with_storage(|s| s.beneficiary())
127    }
128
129    /// Returns the current block number.
130    pub fn block_number(&self) -> u64 {
131        Self::with_storage(|s| s.block_number())
132    }
133
134    /// Sets the bytecode at the given address.
135    pub fn set_code(&mut self, address: Address, code: Bytecode) -> Result<()> {
136        Self::try_with_storage(|s| s.set_code(address, code))
137    }
138
139    /// Performs an SLOAD operation (persistent storage read).
140    pub fn sload(&self, address: Address, key: U256) -> Result<U256> {
141        Self::try_with_storage(|s| s.sload(address, key))
142    }
143
144    /// Performs a TLOAD operation (transient storage read).
145    pub fn tload(&self, address: Address, key: U256) -> Result<U256> {
146        Self::try_with_storage(|s| s.tload(address, key))
147    }
148
149    /// Performs an SSTORE operation (persistent storage write).
150    pub fn sstore(&mut self, address: Address, key: U256, value: U256) -> Result<()> {
151        Self::try_with_storage(|s| s.sstore(address, key, value))
152    }
153
154    /// Performs a TSTORE operation (transient storage write).
155    pub fn tstore(&mut self, address: Address, key: U256, value: U256) -> Result<()> {
156        Self::try_with_storage(|s| s.tstore(address, key, value))
157    }
158
159    /// Emits an event from the given contract address.
160    pub fn emit_event(&mut self, address: Address, event: LogData) -> Result<()> {
161        Self::try_with_storage(|s| s.emit_event(address, event))
162    }
163
164    /// Adds refund to the gas refund counter.
165    pub fn refund_gas(&mut self, gas: i64) {
166        Self::with_storage(|s| s.refund_gas(gas))
167    }
168
169    /// Returns the gas used so far.
170    pub fn gas_used(&self) -> u64 {
171        Self::with_storage(|s| s.gas_used())
172    }
173
174    /// Returns the gas refunded so far.
175    pub fn gas_refunded(&self) -> i64 {
176        Self::with_storage(|s| s.gas_refunded())
177    }
178
179    /// Returns the currently active hardfork.
180    pub fn spec(&self) -> TempoHardfork {
181        Self::with_storage(|s| s.spec())
182    }
183
184    /// Returns whether the current call context is static.
185    pub fn is_static(&self) -> bool {
186        Self::with_storage(|s| s.is_static())
187    }
188
189    /// Creates a journal checkpoint and returns a RAII guard.
190    ///
191    /// All state mutations after this call will be atomically
192    /// reverted if the guard is dropped without calling
193    /// [`CheckpointGuard::commit`].
194    ///
195    /// # Panics
196    ///
197    /// Panics if no storage context is set.
198    pub fn checkpoint(&mut self) -> CheckpointGuard {
199        // spec: only available +T1C. Prior to that checkpoints are a no-op.
200        let checkpoint = Self::with_storage(|s| {
201            if s.spec().is_t1c() {
202                Some(s.checkpoint())
203            } else {
204                None
205            }
206        });
207
208        CheckpointGuard { checkpoint }
209    }
210
211    /// Deducts gas from the remaining gas and returns an error if insufficient.
212    pub fn deduct_gas(&mut self, gas: u64) -> Result<()> {
213        Self::try_with_storage(|s| s.deduct_gas(gas))
214    }
215
216    /// Computes keccak256 and charges the appropriate gas.
217    ///
218    /// Prefer this over naked `keccak256` to ensure gas is accounted for.
219    pub fn keccak256(&self, data: &[u8]) -> Result<B256> {
220        Self::try_with_storage(|s| s.keccak256(data))
221    }
222
223    /// Recovers the signer address from an ECDSA signature and charges ecrecover gas.
224    /// As per [TIP-1004], it only accepts `v` values of `27` or `28` (no `0`/`1` normalization).
225    ///
226    /// Returns `Ok(None)` on invalid signatures; callers map to domain-specific errors.
227    ///
228    /// [TIP-1004]: <https://github.com/tempoxyz/tempo/blob/main/tips/tip-1004.md#signature-validation>
229    pub fn recover_signer(&self, digest: B256, v: u8, r: B256, s: B256) -> Result<Option<Address>> {
230        Self::try_with_storage(|storage| storage.recover_signer(digest, v, r, s))
231    }
232}
233
234/// RAII guard for atomic state mutation batching.
235///
236/// On drop, automatically reverts all state changes made since the checkpoint
237/// unless [`commit`](CheckpointGuard::commit) was called.
238///
239/// # SPEC
240/// Only active +T1C, previously it is a no-op (no checkpoint is created).
241///
242/// # Examples
243///
244/// ```ignore
245/// let guard = self.storage.checkpoint();
246/// self.sstore(addr, key, value)?;  // reverted on drop (T1C+)
247/// self.emit_event(...)?;
248/// guard.commit();  // finalizes all mutations
249/// ```
250pub struct CheckpointGuard {
251    checkpoint: Option<JournalCheckpoint>,
252}
253
254impl CheckpointGuard {
255    /// Commits all state changes since the checkpoint.
256    pub fn commit(mut self) {
257        if let Some(cp) = self.checkpoint.take() {
258            StorageCtx::with_storage(|s| s.checkpoint_commit(cp));
259        }
260    }
261}
262
263impl Drop for CheckpointGuard {
264    fn drop(&mut self) {
265        if let Some(cp) = self.checkpoint.take() {
266            StorageCtx::with_storage(|s| s.checkpoint_revert(cp));
267        }
268    }
269}
270
271impl<'evm> StorageCtx {
272    /// Generic entry point for EVM-like environments.
273    /// Sets up the storage provider and executes a closure within that context.
274    pub fn enter_evm<J, R>(
275        journal: &'evm mut J,
276        block_env: &'evm dyn Block,
277        cfg: &CfgEnv<TempoHardfork>,
278        tx_env: &'evm impl Transaction,
279        f: impl FnOnce() -> R,
280    ) -> R
281    where
282        J: JournalTr<Database: Database> + Debug,
283    {
284        let internals = EvmInternals::new(journal, block_env, cfg, tx_env);
285        let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, cfg);
286
287        // The core logic of setting up thread-local storage is here.
288        Self::enter(&mut provider, f)
289    }
290
291    /// Entry point for a "canonical" precompile (with unique known address).
292    pub fn enter_precompile<J, P, R>(
293        journal: &'evm mut J,
294        block_env: &'evm dyn Block,
295        cfg: &CfgEnv<TempoHardfork>,
296        tx_env: &'evm impl Transaction,
297        f: impl FnOnce(P) -> R,
298    ) -> R
299    where
300        J: JournalTr<Database: Database> + Debug,
301        P: Precompile + Default,
302    {
303        // Delegate all the setup logic to `enter_evm`.
304        // We just need to provide a closure that `enter_evm` expects.
305        Self::enter_evm(journal, block_env, cfg, tx_env, || f(P::default()))
306    }
307}
308
309#[cfg(any(test, feature = "test-utils"))]
310use crate::storage::hashmap::HashMapStorageProvider;
311
312#[cfg(any(test, feature = "test-utils"))]
313impl StorageCtx {
314    /// Returns a mutable reference to the underlying `HashMapStorageProvider`.
315    ///
316    /// NOTE: takes a non-mutable reference because it's internal. The mutability
317    /// of the storage operation is determined by the public function.
318    #[allow(clippy::mut_from_ref)]
319    fn as_hashmap(&self) -> &mut HashMapStorageProvider {
320        Self::with_storage(|s| {
321            // SAFETY: Test code always uses HashMapStorageProvider.
322            // Reference valid for duration of StorageCtx::enter closure.
323            unsafe {
324                extend_lifetime_mut(
325                    &mut *(s as *mut dyn PrecompileStorageProvider as *mut HashMapStorageProvider),
326                )
327            }
328        })
329    }
330
331    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
332    pub fn get_account_info(&self, address: Address) -> Option<&AccountInfo> {
333        self.as_hashmap().get_account_info(address)
334    }
335
336    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
337    pub fn get_events(&self, address: Address) -> &Vec<LogData> {
338        self.as_hashmap().get_events(address)
339    }
340
341    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
342    pub fn set_nonce(&mut self, address: Address, nonce: u64) {
343        self.as_hashmap().set_nonce(address, nonce)
344    }
345
346    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
347    pub fn set_timestamp(&mut self, timestamp: U256) {
348        self.as_hashmap().set_timestamp(timestamp)
349    }
350
351    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
352    pub fn set_beneficiary(&mut self, beneficiary: Address) {
353        self.as_hashmap().set_beneficiary(beneficiary)
354    }
355
356    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
357    pub fn set_block_number(&mut self, block_number: u64) {
358        self.as_hashmap().set_block_number(block_number)
359    }
360
361    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
362    pub fn set_spec(&mut self, spec: TempoHardfork) {
363        self.as_hashmap().set_spec(spec)
364    }
365
366    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
367    pub fn clear_transient(&mut self) {
368        self.as_hashmap().clear_transient()
369    }
370
371    /// NOTE: assumes storage tests always use the `HashMapStorageProvider`
372    ///
373    /// USAGE: `TIP20Setup` automatically clears events of the configured
374    /// contract when `apply()` is called, unless explicitly asked no to.
375    pub fn clear_events(&mut self, address: Address) {
376        self.as_hashmap().clear_events(address);
377    }
378
379    /// Checks if a contract at the given address has bytecode deployed.
380    pub fn has_bytecode(&self, address: Address) -> Result<bool> {
381        self.with_account_info(address, |info| Ok(!info.is_empty_code_hash()))
382    }
383}
384
385/// Extends the lifetime of a mutable reference: `&'a mut T -> &'b mut T`
386///
387/// SAFETY: the caller must ensure the reference remains valid for the extended lifetime.
388#[cfg(any(test, feature = "test-utils"))]
389unsafe fn extend_lifetime_mut<'b, T: ?Sized>(r: &mut T) -> &'b mut T {
390    unsafe { &mut *(r as *mut T) }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use alloy::primitives::U256;
397    use tempo_chainspec::hardfork::TempoHardfork;
398
399    fn t1c_storage() -> HashMapStorageProvider {
400        HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C)
401    }
402
403    #[test]
404    #[should_panic(expected = "already borrowed")]
405    fn test_reentrant_with_storage_panics() {
406        let mut storage = HashMapStorageProvider::new(1);
407        StorageCtx::enter(&mut storage, || {
408            // first borrow
409            StorageCtx::with_storage(|_| {
410                // re-entrant call should panic
411                StorageCtx::with_storage(|_| ())
412            })
413        });
414    }
415
416    #[test]
417    fn test_checkpoint_commit_and_revert() {
418        let mut storage = t1c_storage();
419        let addr = Address::ZERO;
420        let key = U256::from(1);
421
422        StorageCtx::enter(&mut storage, || {
423            let mut ctx = StorageCtx;
424
425            // commit persists state
426            ctx.sstore(addr, key, U256::from(42)).unwrap();
427            let guard = ctx.checkpoint();
428            ctx.sstore(addr, key, U256::from(99)).unwrap();
429            guard.commit();
430            assert_eq!(ctx.sload(addr, key).unwrap(), U256::from(99));
431
432            // drop reverts state
433            {
434                let _guard = ctx.checkpoint();
435                ctx.sstore(addr, key, U256::from(1)).unwrap();
436            }
437            assert_eq!(ctx.sload(addr, key).unwrap(), U256::from(99));
438        });
439    }
440
441    #[test]
442    fn test_nested_checkpoints_lifo() {
443        let mut storage = t1c_storage();
444        let addr = Address::ZERO;
445        let key = U256::from(1);
446
447        StorageCtx::enter(&mut storage, || {
448            let mut ctx = StorageCtx;
449            ctx.sstore(addr, key, U256::from(10)).unwrap();
450
451            // both committed in LIFO order
452            let outer = ctx.checkpoint();
453            ctx.sstore(addr, key, U256::from(20)).unwrap();
454            let inner = ctx.checkpoint();
455            ctx.sstore(addr, key, U256::from(30)).unwrap();
456            inner.commit();
457            outer.commit();
458            assert_eq!(ctx.sload(addr, key).unwrap(), U256::from(30));
459
460            // inner reverts, outer commits
461            let outer = ctx.checkpoint();
462            ctx.sstore(addr, key, U256::from(40)).unwrap();
463            {
464                let _inner = ctx.checkpoint();
465                ctx.sstore(addr, key, U256::from(50)).unwrap();
466            }
467            outer.commit();
468            assert_eq!(ctx.sload(addr, key).unwrap(), U256::from(40));
469        });
470    }
471
472    #[test]
473    #[should_panic(expected = "out-of-order")]
474    fn test_nested_checkpoints_out_of_order_commit_panics() {
475        let mut storage = t1c_storage();
476
477        StorageCtx::enter(&mut storage, || {
478            let mut ctx = StorageCtx;
479
480            let outer = ctx.checkpoint();
481            let _inner = ctx.checkpoint();
482
483            // Wrong order: committing outer while inner is still active
484            outer.commit();
485        });
486    }
487
488    #[test]
489    fn test_checkpoint_noop_pre_t1c() {
490        let mut storage = HashMapStorageProvider::new(1); // default = T0
491        let addr = Address::ZERO;
492        let key = U256::from(1);
493
494        StorageCtx::enter(&mut storage, || {
495            let mut ctx = StorageCtx;
496
497            ctx.sstore(addr, key, U256::from(42)).unwrap();
498            {
499                let _guard = ctx.checkpoint(); // no-op pre-T1C
500                ctx.sstore(addr, key, U256::from(99)).unwrap();
501                // drop does nothing — no checkpoint was created
502            }
503            // state is NOT reverted because checkpoints are disabled pre-T1C
504            assert_eq!(ctx.sload(addr, key).unwrap(), U256::from(99));
505        });
506    }
507}