arb_context/
lib.rs

1//! Per-block and per-tx context threaded into Arbitrum precompile handlers.
2
3use std::{
4    collections::HashMap,
5    sync::{
6        Arc, OnceLock,
7        atomic::{AtomicBool, AtomicU64, AtomicUsize},
8    },
9};
10
11use alloy_primitives::{Address, B256, U256};
12use arb_primitives::multigas::MultiGas;
13use arb_storage::{Detached, SystemStateBackend};
14use arbos::{
15    arbos_state::{ArbosState, ArbosStateError, arbos_from_input_system},
16    burn::SystemBurner,
17};
18use parking_lot::Mutex;
19
20/// LRU cache of recently invoked Stylus program codehashes.
21#[derive(Debug, Default)]
22pub struct RecentWasms {
23    entries: Vec<B256>,
24    capacity: usize,
25}
26
27impl RecentWasms {
28    pub fn new(capacity: usize) -> Self {
29        Self {
30            entries: Vec::new(),
31            capacity,
32        }
33    }
34
35    pub fn reset(&mut self, capacity: usize) {
36        self.entries.clear();
37        self.capacity = capacity;
38    }
39
40    /// Insert a hash. Returns `true` if it was already present.
41    pub fn insert(&mut self, hash: B256) -> bool {
42        let was_present = if let Some(pos) = self.entries.iter().position(|h| *h == hash) {
43            self.entries.remove(pos);
44            true
45        } else {
46            false
47        };
48        self.entries.push(hash);
49        if self.capacity > 0 && self.entries.len() > self.capacity {
50            self.entries.remove(0);
51        }
52        was_present
53    }
54}
55
56/// Caches shared across blocks via an `Arc` clone into each [`BlockCtx`].
57#[derive(Debug, Default)]
58pub struct ChainCaches {
59    pub l1_block_numbers: Mutex<HashMap<u64, u64>>,
60    pub l2_block_hashes: Mutex<HashMap<u64, B256>>,
61}
62
63/// Per-block parameters populated once at block start.
64pub struct BlockCtx {
65    pub arbos_version: u64,
66    pub block_timestamp: u64,
67    /// L1 block number observed by the EVM `NUMBER` opcode and precompiles
68    /// that surface the recorded L1 height.
69    pub l1_block_number_for_evm: u64,
70    pub l2_block_number: u64,
71    pub allow_debug_precompiles: bool,
72    /// Live counter mutated by the executor between transactions in the same block.
73    pub current_gas_backlog: AtomicU64,
74    /// Set by an owner setter when a transaction changes a per-tx state
75    /// parameter (fee collectors, minimum base fee, brotli level, calldata
76    /// pricing); the executor refreshes its cached values after the transaction.
77    pub state_params_dirty: AtomicBool,
78    pub chain_caches: Arc<ChainCaches>,
79    pub recent_wasms: Mutex<RecentWasms>,
80    /// Per-block descriptor cache for the detached [`ArbosState`].
81    arbos_state: OnceLock<Result<ArbosState<'static, Detached, SystemBurner>, ArbosStateError>>,
82}
83
84impl Default for BlockCtx {
85    fn default() -> Self {
86        Self::new_with_caches(0, 0, 0, 0, false, Arc::new(ChainCaches::default()))
87    }
88}
89
90impl std::fmt::Debug for BlockCtx {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("BlockCtx")
93            .field("arbos_version", &self.arbos_version)
94            .field("block_timestamp", &self.block_timestamp)
95            .field("l1_block_number_for_evm", &self.l1_block_number_for_evm)
96            .field("l2_block_number", &self.l2_block_number)
97            .field("allow_debug_precompiles", &self.allow_debug_precompiles)
98            .field("current_gas_backlog", &self.current_gas_backlog)
99            .field("chain_caches", &self.chain_caches)
100            .field("recent_wasms", &self.recent_wasms)
101            .field("arbos_state_cached", &self.arbos_state.get().is_some())
102            .finish()
103    }
104}
105
106impl BlockCtx {
107    pub fn new(
108        arbos_version: u64,
109        block_timestamp: u64,
110        l1_block_number_for_evm: u64,
111        l2_block_number: u64,
112        allow_debug_precompiles: bool,
113    ) -> Self {
114        Self::new_with_caches(
115            arbos_version,
116            block_timestamp,
117            l1_block_number_for_evm,
118            l2_block_number,
119            allow_debug_precompiles,
120            Arc::new(ChainCaches::default()),
121        )
122    }
123
124    pub fn new_with_caches(
125        arbos_version: u64,
126        block_timestamp: u64,
127        l1_block_number_for_evm: u64,
128        l2_block_number: u64,
129        allow_debug_precompiles: bool,
130        chain_caches: Arc<ChainCaches>,
131    ) -> Self {
132        Self {
133            arbos_version,
134            block_timestamp,
135            l1_block_number_for_evm,
136            l2_block_number,
137            allow_debug_precompiles,
138            current_gas_backlog: AtomicU64::new(0),
139            state_params_dirty: AtomicBool::new(false),
140            chain_caches,
141            recent_wasms: Mutex::new(RecentWasms::default()),
142            arbos_state: OnceLock::new(),
143        }
144    }
145
146    /// Borrow the cached [`ArbosState`], constructing it on first call via
147    /// [`arbos_from_input_system`]. Returns a clone of the cached error if
148    /// the first call failed; the cache is not cleared on error since the
149    /// version slot does not change within a single block.
150    pub fn arbos_state<S: SystemStateBackend>(
151        &self,
152        backend: &mut S,
153    ) -> Result<&ArbosState<'static, Detached, SystemBurner>, ArbosStateError> {
154        self.arbos_state
155            .get_or_init(|| arbos_from_input_system(backend, SystemBurner::new(None, false)))
156            .as_ref()
157            .map_err(Clone::clone)
158    }
159
160    pub fn current_gas_backlog(&self) -> u64 {
161        self.current_gas_backlog
162            .load(std::sync::atomic::Ordering::Relaxed)
163    }
164
165    pub fn set_current_gas_backlog(&self, value: u64) {
166        self.current_gas_backlog
167            .store(value, std::sync::atomic::Ordering::Relaxed);
168    }
169
170    /// Insert an L1 block number into the cache, retaining a rolling window
171    /// of recent L2 heights.
172    pub fn cache_l1_block_number(&self, l2_block: u64, l1_block: u64) {
173        let mut map = self.chain_caches.l1_block_numbers.lock();
174        map.insert(l2_block, l1_block);
175        if l2_block > 100 {
176            map.retain(|&k, _| k >= l2_block - 100);
177        }
178    }
179
180    pub fn cached_l1_block_number(&self, l2_block: u64) -> Option<u64> {
181        self.chain_caches
182            .l1_block_numbers
183            .lock()
184            .get(&l2_block)
185            .copied()
186    }
187
188    pub fn cache_l2_block_hash(&self, l2_block: u64, hash: B256) {
189        let mut map = self.chain_caches.l2_block_hashes.lock();
190        map.insert(l2_block, hash);
191        // Bound to the arbBlockHash validity window (`requested + 256 >= current`).
192        if l2_block > 256 {
193            map.retain(|&k, _| k >= l2_block - 256);
194        }
195    }
196
197    pub fn cached_l2_block_hash(&self, l2_block: u64) -> Option<B256> {
198        self.chain_caches
199            .l2_block_hashes
200            .lock()
201            .get(&l2_block)
202            .copied()
203    }
204
205    pub fn reset_recent_wasms(&self, capacity: usize) {
206        self.recent_wasms.lock().reset(capacity);
207    }
208
209    pub fn insert_recent_wasm(&self, hash: B256) -> bool {
210        self.recent_wasms.lock().insert(hash)
211    }
212}
213
214/// Per-tx scratch written by the executor between transactions.
215#[derive(Debug, Default, Clone)]
216pub struct TxCtx {
217    pub sender: Address,
218    pub effective_gas_price: u128,
219    pub poster_fee: u128,
220    pub poster_balance_correction: u128,
221    pub retryable_id: B256,
222    pub redeemer: Address,
223    pub tx_is_aliased: bool,
224    pub stylus_activation_addr: Option<Address>,
225    pub stylus_keepalive_hash: Option<B256>,
226    pub stylus_activation_data_fee: U256,
227    pub stylus_call_value: U256,
228    pub stylus_program_counts: HashMap<Address, u32>,
229    pub stylus_pages_open: u16,
230    pub stylus_pages_ever: u16,
231    pub stylus_multi_gas: MultiGas,
232    pub precompile_multi_gas: MultiGas,
233    pub stylus_upfront_oog_gas: u64,
234    pub cancel_escrow_sweep: Option<(Address, Address)>,
235}
236
237impl TxCtx {
238    /// Redeemer address packed into the low 20 bytes of a 32-byte word.
239    pub fn redeemer_word(&self) -> U256 {
240        U256::from_be_bytes(B256::left_padding_from(self.redeemer.as_slice()).0)
241    }
242}
243
244/// Handle threaded into precompile handlers. Cheap to clone (all `Arc`).
245#[derive(Debug, Default, Clone)]
246pub struct ArbPrecompileCtx {
247    pub block: Arc<BlockCtx>,
248    pub tx: Arc<Mutex<TxCtx>>,
249    /// EVM call depth at the most recent precompile dispatch. Mirrors the
250    /// journal depth surfaced by revm to the precompile provider.
251    pub evm_depth: Arc<AtomicUsize>,
252    /// Caller addresses by depth, pushed at each frame boundary so that
253    /// precompile handlers can resolve the on-chain caller at arbitrary
254    /// depth (alloy-evm's `EvmInternals` does not surface this).
255    pub caller_stack: Arc<Mutex<Vec<Address>>>,
256    /// Number of Stylus program frames currently on the call stack. Lets a
257    /// frame tell whether it has a Stylus ancestor.
258    pub stylus_frame_depth: Arc<AtomicUsize>,
259}
260
261impl ArbPrecompileCtx {
262    pub fn new() -> Self {
263        Self::default()
264    }
265
266    pub fn with_block(block: Arc<BlockCtx>) -> Self {
267        Self {
268            block,
269            tx: Arc::new(Mutex::new(TxCtx::default())),
270            evm_depth: Arc::new(AtomicUsize::new(0)),
271            caller_stack: Arc::new(Mutex::new(Vec::new())),
272            stylus_frame_depth: Arc::new(AtomicUsize::new(0)),
273        }
274    }
275
276    /// Snapshot the current per-tx scratch.
277    pub fn tx_snapshot(&self) -> TxCtx {
278        self.tx.lock().clone()
279    }
280
281    /// Reset per-tx scratch between transactions.
282    pub fn reset_tx(&self) {
283        *self.tx.lock() = TxCtx::default();
284    }
285
286    pub fn set_sender(&self, sender: Address) {
287        self.tx.lock().sender = sender;
288    }
289
290    pub fn set_effective_gas_price(&self, price: u128) {
291        self.tx.lock().effective_gas_price = price;
292    }
293
294    pub fn set_poster_fee(&self, fee: u128) {
295        self.tx.lock().poster_fee = fee;
296    }
297
298    pub fn set_poster_balance_correction(&self, correction: u128) {
299        self.tx.lock().poster_balance_correction = correction;
300    }
301
302    pub fn set_retryable_id(&self, id: B256) {
303        self.tx.lock().retryable_id = id;
304    }
305
306    pub fn set_redeemer(&self, redeemer: Address) {
307        self.tx.lock().redeemer = redeemer;
308    }
309
310    pub fn set_tx_is_aliased(&self, aliased: bool) {
311        self.tx.lock().tx_is_aliased = aliased;
312    }
313
314    pub fn tx_is_aliased(&self) -> bool {
315        self.tx.lock().tx_is_aliased
316    }
317
318    pub fn set_evm_depth(&self, depth: usize) {
319        self.evm_depth
320            .store(depth, std::sync::atomic::Ordering::Relaxed);
321    }
322
323    pub fn evm_depth(&self) -> usize {
324        self.evm_depth.load(std::sync::atomic::Ordering::Relaxed)
325    }
326
327    /// Marks entry to a Stylus frame, returning the new Stylus call depth (1 for
328    /// the outermost Stylus frame).
329    pub fn enter_stylus_frame(&self) -> usize {
330        self.stylus_frame_depth
331            .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
332            + 1
333    }
334
335    /// Marks exit from a Stylus frame.
336    pub fn exit_stylus_frame(&self) {
337        self.stylus_frame_depth
338            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
339    }
340
341    pub fn push_caller(&self, caller: Address) {
342        self.caller_stack.lock().push(caller);
343    }
344
345    pub fn pop_caller(&self) {
346        self.caller_stack.lock().pop();
347    }
348
349    pub fn reset_caller_stack(&self) {
350        self.caller_stack.lock().clear();
351    }
352
353    /// Return the caller at the given depth (1-indexed). Mirrors the
354    /// frame-depth conventions used by ArbSys.
355    pub fn caller_at_depth(&self, depth: usize) -> Option<Address> {
356        if depth == 0 {
357            return None;
358        }
359        self.caller_stack.lock().get(depth - 1).copied()
360    }
361
362    pub fn set_stylus_activation_addr(&self, addr: Option<Address>) {
363        self.tx.lock().stylus_activation_addr = addr;
364    }
365
366    pub fn take_stylus_activation_addr(&self) -> Option<Address> {
367        self.tx.lock().stylus_activation_addr.take()
368    }
369
370    pub fn set_cancel_escrow_sweep(&self, escrow: Address, beneficiary: Address) {
371        self.tx.lock().cancel_escrow_sweep = Some((escrow, beneficiary));
372    }
373
374    pub fn take_cancel_escrow_sweep(&self) -> Option<(Address, Address)> {
375        self.tx.lock().cancel_escrow_sweep.take()
376    }
377
378    pub fn set_stylus_keepalive_hash(&self, hash: Option<B256>) {
379        self.tx.lock().stylus_keepalive_hash = hash;
380    }
381
382    pub fn take_stylus_keepalive_hash(&self) -> Option<B256> {
383        self.tx.lock().stylus_keepalive_hash.take()
384    }
385
386    pub fn set_stylus_activation_data_fee(&self, fee: U256) {
387        self.tx.lock().stylus_activation_data_fee = fee;
388    }
389
390    pub fn take_stylus_activation_data_fee(&self) -> U256 {
391        std::mem::replace(&mut self.tx.lock().stylus_activation_data_fee, U256::ZERO)
392    }
393
394    pub fn set_stylus_call_value(&self, value: U256) {
395        self.tx.lock().stylus_call_value = value;
396    }
397
398    pub fn stylus_call_value(&self) -> U256 {
399        self.tx.lock().stylus_call_value
400    }
401
402    pub fn add_stylus_multi_gas(&self, gas: MultiGas) {
403        let mut tx = self.tx.lock();
404        tx.stylus_multi_gas = tx.stylus_multi_gas.saturating_add(gas);
405    }
406
407    pub fn stylus_multi_gas(&self) -> MultiGas {
408        self.tx.lock().stylus_multi_gas
409    }
410
411    pub fn add_stylus_upfront_oog_gas(&self, gas: u64) {
412        let mut tx = self.tx.lock();
413        tx.stylus_upfront_oog_gas = tx.stylus_upfront_oog_gas.saturating_add(gas);
414    }
415
416    pub fn stylus_upfront_oog_gas(&self) -> u64 {
417        self.tx.lock().stylus_upfront_oog_gas
418    }
419
420    /// Accumulate per-dimension gas for a precompile charge. The single-gas
421    /// total still flows through the precompile's own `gas_used`; this records
422    /// only the resource breakdown for the v60 pricing backlog.
423    pub fn add_precompile_multi_gas(
424        &self,
425        kind: arb_primitives::multigas::ResourceKind,
426        amount: u64,
427    ) {
428        let mut tx = self.tx.lock();
429        tx.precompile_multi_gas
430            .saturating_increment_into(kind, amount);
431    }
432
433    pub fn precompile_multi_gas(&self) -> MultiGas {
434        self.tx.lock().precompile_multi_gas
435    }
436
437    /// Capture the current `precompile_multi_gas` so callers can later restore
438    /// it. Used by precompiles whose body gas is intentionally discarded at the
439    /// receipt (mirroring the reference, where access-controlled methods report
440    /// zero gas) so the per-dimension contributions they recorded are dropped
441    /// before the result is returned.
442    pub fn snapshot_precompile_multi_gas(&self) -> MultiGas {
443        self.tx.lock().precompile_multi_gas
444    }
445
446    /// Restore `precompile_multi_gas` to a previously captured snapshot.
447    pub fn restore_precompile_multi_gas(&self, snapshot: MultiGas) {
448        self.tx.lock().precompile_multi_gas = snapshot;
449    }
450
451    /// Increment the reentrancy counter for `addr` and return `true` if this
452    /// is a reentrant entry (counter was already > 0).
453    pub fn push_stylus_program(&self, addr: Address) -> bool {
454        let mut tx = self.tx.lock();
455        let count = tx.stylus_program_counts.entry(addr).or_insert(0);
456        *count += 1;
457        *count > 1
458    }
459
460    pub fn stylus_program_count(&self, addr: Address) -> u32 {
461        self.tx
462            .lock()
463            .stylus_program_counts
464            .get(&addr)
465            .copied()
466            .unwrap_or(0)
467    }
468
469    pub fn pop_stylus_program(&self, addr: Address) {
470        let mut tx = self.tx.lock();
471        if let Some(count) = tx.stylus_program_counts.get_mut(&addr) {
472            *count = count.saturating_sub(1);
473            if *count == 0 {
474                tx.stylus_program_counts.remove(&addr);
475            }
476        }
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    fn ctx_sharing(caches: &Arc<ChainCaches>) -> BlockCtx {
485        BlockCtx::new_with_caches(60, 0, 0, 0, false, caches.clone())
486    }
487
488    #[test]
489    fn chain_caches_share_l2_block_hashes_across_block_ctxs() {
490        let caches = Arc::new(ChainCaches::default());
491        let block_a = ctx_sharing(&caches);
492        let block_b = ctx_sharing(&caches);
493
494        let hash = B256::repeat_byte(0x42);
495        block_a.cache_l2_block_hash(100, hash);
496        assert_eq!(block_b.cached_l2_block_hash(100), Some(hash));
497    }
498
499    #[test]
500    fn cache_l2_block_hash_evicts_entries_outside_window() {
501        let block = BlockCtx::new_with_caches(60, 0, 0, 0, false, Arc::new(ChainCaches::default()));
502        for n in 0..=512u64 {
503            block.cache_l2_block_hash(n, B256::from(U256::from(n)));
504        }
505        assert_eq!(
506            block.cached_l2_block_hash(256),
507            Some(B256::from(U256::from(256)))
508        );
509        assert_eq!(
510            block.cached_l2_block_hash(512),
511            Some(B256::from(U256::from(512)))
512        );
513        assert_eq!(block.cached_l2_block_hash(255), None);
514        assert_eq!(block.cached_l2_block_hash(0), None);
515    }
516
517    #[test]
518    fn block_ctx_new_creates_isolated_caches() {
519        let a = BlockCtx::new(60, 0, 0, 0, false);
520        let b = BlockCtx::new(60, 0, 0, 0, false);
521        a.cache_l2_block_hash(1, B256::repeat_byte(0x01));
522        assert!(b.cached_l2_block_hash(1).is_none());
523    }
524}