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}