arb_evm/
context.rs

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