arb_evm/
context.rs

1use alloy_primitives::{Address, B256, U256};
2use std::collections::{HashMap, HashSet};
3
4/// Arbitrum-specific block execution context.
5///
6/// Carries L1 information and Arbitrum state needed during block execution.
7/// This is passed as `ExecutionCtx` through reth's block executor pipeline.
8#[derive(Debug, Clone, Default)]
9pub struct ArbBlockExecutionCtx {
10    /// Hash of the parent block.
11    pub parent_hash: B256,
12    /// Parent beacon block root (for EIP-4788).
13    pub parent_beacon_block_root: Option<B256>,
14    /// Header extra data (carries send root).
15    pub extra_data: Vec<u8>,
16    /// Number of delayed messages read (from header nonce).
17    pub delayed_messages_read: u64,
18    /// L1 block number (from header mix_hash bytes 8-15).
19    pub l1_block_number: u64,
20    /// L2 block number (header number). Distinct from block_env.number which
21    /// holds L1 block number for the NUMBER opcode.
22    pub l2_block_number: u64,
23    /// Chain ID.
24    pub chain_id: u64,
25    /// Block timestamp.
26    pub block_timestamp: u64,
27    /// Block base fee.
28    pub basefee: U256,
29    /// Time elapsed since parent block (seconds).
30    pub time_passed: u64,
31    /// L1 base fee from the incoming message header.
32    pub l1_base_fee: U256,
33    /// L1 pricing: price per unit from L1PricingState.
34    pub l1_price_per_unit: U256,
35    /// L1 pricing: brotli compression level from ArbOS state.
36    pub brotli_compression_level: u64,
37    /// ArbOS version.
38    pub arbos_version: u64,
39    /// Network fee account address.
40    pub network_fee_account: Address,
41    /// Infrastructure fee account address.
42    pub infra_fee_account: Address,
43    /// Minimum L2 base fee.
44    pub min_base_fee: U256,
45    /// Block coinbase (poster address / beneficiary).
46    pub coinbase: Address,
47}
48
49/// Attributes for building the next Arbitrum block.
50///
51/// Contains values that cannot be derived from the parent block alone.
52#[derive(Debug, Clone)]
53pub struct ArbNextBlockEnvCtx {
54    /// L1 poster address (becomes the coinbase).
55    pub suggested_fee_recipient: Address,
56    /// Block timestamp.
57    pub timestamp: u64,
58    /// Mix hash encoding L1 block info and ArbOS version.
59    pub prev_randao: B256,
60    /// Extra data (carries send root).
61    pub extra_data: Vec<u8>,
62    /// Parent beacon block root (for EIP-4788).
63    pub parent_beacon_block_root: Option<B256>,
64}
65
66/// WASM activation info for a newly activated Stylus program.
67#[derive(Debug, Clone)]
68pub struct ActivatedWasm {
69    /// Compiled ASM per target.
70    pub asm: HashMap<String, Vec<u8>>,
71    /// Raw WASM module.
72    pub module: Vec<u8>,
73}
74
75/// LRU-style set of recently seen WASM module hashes.
76///
77/// Used to avoid redundant compilation of recently activated modules.
78#[derive(Debug, Clone, Default)]
79pub struct RecentWasms {
80    hashes: Vec<B256>,
81    max_entries: usize,
82}
83
84impl RecentWasms {
85    pub fn new(max_entries: usize) -> Self {
86        Self {
87            hashes: Vec::new(),
88            max_entries,
89        }
90    }
91
92    /// Insert a hash, returning `true` if it was already present.
93    pub fn insert(&mut self, hash: B256) -> bool {
94        let was_present = if let Some(pos) = self.hashes.iter().position(|h| *h == hash) {
95            self.hashes.remove(pos);
96            true
97        } else {
98            false
99        };
100        self.hashes.push(hash);
101        if self.hashes.len() > self.max_entries {
102            self.hashes.remove(0);
103        }
104        was_present
105    }
106
107    pub fn contains(&self, hash: &B256) -> bool {
108        self.hashes.contains(hash)
109    }
110}
111
112/// Extra per-block state tracked during Arbitrum execution.
113///
114/// In geth this is `ArbitrumExtraData` on StateDB. In reth it lives
115/// alongside the block executor as mutable state.
116#[derive(Debug, Clone, Default)]
117pub struct ArbitrumExtraData {
118    /// Net balance change across all accounts (tracks ETH minting/burning).
119    pub unexpected_balance_delta: i128,
120    /// WASM modules encountered during execution (for recording).
121    pub user_wasms: HashMap<B256, ActivatedWasm>,
122    /// Number of WASM memory pages currently open (Stylus).
123    pub open_wasm_pages: u16,
124    /// Peak number of WASM memory pages allocated during this tx.
125    pub ever_wasm_pages: u16,
126    /// Newly activated WASM modules during this block.
127    pub activated_wasms: HashMap<B256, ActivatedWasm>,
128    /// Recently activated WASM modules (LRU).
129    pub recent_wasms: RecentWasms,
130    /// Whether transaction filtering is active.
131    pub arb_tx_filter: bool,
132    /// Zombie accounts: addresses that were self-destructed then touched by
133    /// a zero-value transfer on pre-Stylus ArbOS (< v30). These must be
134    /// preserved as empty accounts during finalization to match canonical behavior.
135    pub zombie_accounts: HashSet<Address>,
136}
137
138impl ArbitrumExtraData {
139    /// Record a WASM activation for the given module hash.
140    ///
141    /// Validates that if the same module hash was already activated in this block,
142    /// the new activation has the same set of targets. This prevents inconsistent
143    /// compilations for different architectures within a single block.
144    pub fn activate_wasm(
145        &mut self,
146        module_hash: B256,
147        asm: HashMap<String, Vec<u8>>,
148        module: Vec<u8>,
149    ) -> Result<(), String> {
150        if let Some(existing) = self.activated_wasms.get(&module_hash) {
151            // Validate target consistency: the new activation must have the
152            // same set of targets as the prior one.
153            let existing_targets: Vec<&String> = existing.asm.keys().collect();
154            let new_targets: Vec<&String> = asm.keys().collect();
155            if existing_targets.len() != new_targets.len()
156                || !new_targets.iter().all(|t| existing.asm.contains_key(*t))
157            {
158                return Err(format!(
159                    "inconsistent WASM targets for module {module_hash}: \
160                     existing has {:?}, new has {:?}",
161                    existing.asm.keys().collect::<Vec<_>>(),
162                    asm.keys().collect::<Vec<_>>(),
163                ));
164            }
165        }
166        self.activated_wasms
167            .insert(module_hash, ActivatedWasm { asm, module });
168        Ok(())
169    }
170
171    /// Register a balance burn from SELFDESTRUCT or native token burn.
172    ///
173    /// Adjusts `unexpected_balance_delta` so that post-block balance verification
174    /// accounts for the burned amount (adds to delta).
175    pub fn expect_balance_burn(&mut self, amount: u128) {
176        self.unexpected_balance_delta =
177            self.unexpected_balance_delta.saturating_add(amount as i128);
178    }
179
180    /// Register a balance mint from native token minting.
181    ///
182    /// Adjusts `unexpected_balance_delta` so that post-block balance verification
183    /// accounts for the minted amount (subtracts from delta).
184    pub fn expect_balance_mint(&mut self, amount: u128) {
185        self.unexpected_balance_delta =
186            self.unexpected_balance_delta.saturating_sub(amount as i128);
187    }
188
189    /// Returns the current unexpected balance delta.
190    pub fn unexpected_balance_delta(&self) -> i128 {
191        self.unexpected_balance_delta
192    }
193
194    // --- Stylus WASM page tracking ---
195
196    /// Returns (open_pages, ever_pages) for Stylus memory accounting.
197    pub fn get_stylus_pages(&self) -> (u16, u16) {
198        (self.open_wasm_pages, self.ever_wasm_pages)
199    }
200
201    /// Returns the current number of open WASM memory pages.
202    pub fn get_stylus_pages_open(&self) -> u16 {
203        self.open_wasm_pages
204    }
205
206    /// Sets the current number of open WASM memory pages.
207    pub fn set_stylus_pages_open(&mut self, pages: u16) {
208        self.open_wasm_pages = pages;
209    }
210
211    /// Adds WASM pages, saturating at u16::MAX.
212    /// Returns the previous (open, ever) values.
213    pub fn add_stylus_pages(&mut self, new_pages: u16) -> (u16, u16) {
214        let prev = (self.open_wasm_pages, self.ever_wasm_pages);
215        self.open_wasm_pages = self.open_wasm_pages.saturating_add(new_pages);
216        self.ever_wasm_pages = self.ever_wasm_pages.max(self.open_wasm_pages);
217        prev
218    }
219
220    /// Adds to the ever-pages high watermark, saturating at u16::MAX.
221    pub fn add_stylus_pages_ever(&mut self, new_pages: u16) {
222        self.ever_wasm_pages = self.ever_wasm_pages.saturating_add(new_pages);
223    }
224
225    /// Resets per-transaction Stylus page counters (called at tx start).
226    pub fn reset_stylus_pages(&mut self) {
227        self.open_wasm_pages = 0;
228        self.ever_wasm_pages = 0;
229    }
230
231    // --- Transaction filter ---
232
233    /// Mark transaction as filtered (will be excluded at commit).
234    pub fn filter_tx(&mut self) {
235        self.arb_tx_filter = true;
236    }
237
238    /// Clear the transaction filter flag.
239    pub fn clear_tx_filter(&mut self) {
240        self.arb_tx_filter = false;
241    }
242
243    /// Returns whether a transaction is currently filtered.
244    pub fn is_tx_filtered(&self) -> bool {
245        self.arb_tx_filter
246    }
247
248    // --- Zombie accounts ---
249
250    /// On pre-Stylus ArbOS (< v30), a zero-value transfer touching a
251    /// self-destructed address creates a "zombie" empty account that must
252    /// survive finalization. Call this when the condition is met.
253    pub fn create_zombie(&mut self, addr: Address) {
254        self.zombie_accounts.insert(addr);
255    }
256
257    /// Returns whether the address is a zombie that should be preserved.
258    pub fn is_zombie(&self, addr: &Address) -> bool {
259        self.zombie_accounts.contains(addr)
260    }
261
262    /// Begin recording WASM modules for block validation.
263    pub fn start_recording(&mut self) {
264        self.user_wasms.clear();
265    }
266
267    /// Record a WASM module's compiled ASM for persistence.
268    pub fn record_program(&mut self, module_hash: B256, wasm: ActivatedWasm) {
269        self.user_wasms.insert(module_hash, wasm);
270    }
271}