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}