arb_evm/
config.rs

1use alloc::sync::Arc;
2use core::{convert::Infallible, fmt::Debug};
3
4use alloy_consensus::{BlockHeader, Header};
5use alloy_eips::Decodable2718;
6use alloy_evm::eth::{EthBlockExecutionCtx, spec::EthExecutorSpec};
7use alloy_primitives::{Address, B256, Bytes, U256};
8use alloy_rpc_types_engine::ExecutionData;
9use arb_chainspec::ArbitrumChainSpec;
10use arb_primitives::ArbPrimitives;
11use reth_chainspec::{EthChainSpec, Hardforks};
12use reth_evm::{
13    ConfigureEngineEvm, ConfigureEvm, EvmEnv, EvmEnvFor, ExecutableTxIterator, ExecutionCtxFor,
14    NextBlockEnvAttributes,
15};
16use reth_primitives_traits::{SealedBlock, SealedHeader, SignedTransaction, TxTy};
17use reth_storage_errors::any::AnyError;
18use revm::{
19    context::{BlockEnv, CfgEnv},
20    primitives::hardfork::SpecId,
21};
22
23use crate::{
24    assembler::ArbBlockAssembler, build::ArbBlockExecutorFactory, context::ArbBlockExecutionCtx,
25    evm::ArbEvmFactory, receipt::ArbReceiptBuilder,
26};
27
28/// Arbitrum EVM configuration.
29///
30/// Wraps the Ethereum EVM config and overrides environment construction
31/// to use ArbOS versioning from the mix_hash field.
32#[derive(Debug, Clone)]
33pub struct ArbEvmConfig<ChainSpec = reth_chainspec::ChainSpec> {
34    pub executor_factory: ArbBlockExecutorFactory<ArbReceiptBuilder, Arc<ChainSpec>, ArbEvmFactory>,
35    pub block_assembler: ArbBlockAssembler<ChainSpec>,
36    chain_spec: Arc<ChainSpec>,
37}
38
39impl<ChainSpec> ArbEvmConfig<ChainSpec>
40where
41    ChainSpec: EthChainSpec + 'static,
42{
43    /// Creates a new Arbitrum EVM configuration with the given chain spec.
44    pub fn new(chain_spec: Arc<ChainSpec>) -> Self {
45        Self::with_allow_debug_precompiles(chain_spec, false)
46    }
47
48    /// Creates a configuration that opts into ArbDebug/ArbosTest precompiles.
49    pub fn with_allow_debug_precompiles(chain_spec: Arc<ChainSpec>, allow_debug: bool) -> Self {
50        Self::build(chain_spec, allow_debug, ArbEvmFactory::new())
51    }
52
53    /// Configuration for offline parallel block execution; cloned workers get
54    /// an isolated per-block context, unlike the live node/RPC path.
55    pub fn for_offline_execution(chain_spec: Arc<ChainSpec>, allow_debug: bool) -> Self {
56        Self::build(chain_spec, allow_debug, ArbEvmFactory::isolated())
57    }
58
59    fn build(chain_spec: Arc<ChainSpec>, allow_debug: bool, evm_factory: ArbEvmFactory) -> Self {
60        Self {
61            executor_factory: ArbBlockExecutorFactory::new(
62                ArbReceiptBuilder,
63                chain_spec.clone(),
64                evm_factory,
65            )
66            .with_allow_debug_precompiles(allow_debug),
67            block_assembler: ArbBlockAssembler::new(chain_spec.clone()),
68            chain_spec,
69        }
70    }
71
72    /// Returns a reference to the chain spec.
73    pub fn chain_spec(&self) -> &Arc<ChainSpec> {
74        &self.chain_spec
75    }
76}
77
78impl<ChainSpec> ConfigureEvm for ArbEvmConfig<ChainSpec>
79where
80    ChainSpec:
81        EthExecutorSpec + EthChainSpec<Header = Header> + ArbitrumChainSpec + Hardforks + 'static,
82{
83    type Primitives = ArbPrimitives;
84    type Error = Infallible;
85    type NextBlockEnvCtx = NextBlockEnvAttributes;
86    type BlockExecutorFactory =
87        ArbBlockExecutorFactory<ArbReceiptBuilder, Arc<ChainSpec>, ArbEvmFactory>;
88    type BlockAssembler = ArbBlockAssembler<ChainSpec>;
89
90    fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
91        &self.executor_factory
92    }
93
94    fn block_assembler(&self) -> &Self::BlockAssembler {
95        &self.block_assembler
96    }
97
98    fn evm_env(&self, header: &Header) -> Result<EvmEnv<SpecId>, Self::Error> {
99        let chain_id = self.chain_spec.chain().id();
100        let mix_hash = header.mix_hash().unwrap_or_default();
101        let arbos_version = arbos_version_from_mix_hash(&mix_hash);
102        let spec = self.chain_spec.spec_id_by_arbos_version(arbos_version);
103
104        // Arbitrum overrides NUMBER to return the L1 block number, not L2.
105        let l1_block_number = l1_block_number_from_mix_hash(&mix_hash);
106
107        stage_rpc_block_ctx(
108            self.executor_factory.arb_evm_factory(),
109            arbos_version,
110            header.timestamp(),
111            l1_block_number,
112            header.number(),
113            self.executor_factory.allow_debug_precompiles(),
114        );
115
116        let cfg_env = arb_cfg_env(chain_id, spec, arbos_version);
117        // Arbitrum sets PREVRANDAO to BigToHash(difficulty), which is 0x...0001.
118        let prevrandao = B256::from(U256::from(1));
119        let block_env = BlockEnv {
120            number: U256::from(l1_block_number),
121            beneficiary: header.beneficiary(),
122            timestamp: U256::from(header.timestamp()),
123            difficulty: header.difficulty(),
124            prevrandao: Some(prevrandao),
125            gas_limit: header.gas_limit(),
126            basefee: header.base_fee_per_gas().unwrap_or_default(),
127            blob_excess_gas_and_price: if spec.is_enabled_in(SpecId::CANCUN) {
128                Some(revm::context_interface::block::BlobExcessGasAndPrice {
129                    excess_blob_gas: 0,
130                    blob_gasprice: 0,
131                })
132            } else {
133                None
134            },
135        };
136
137        Ok(EvmEnv { cfg_env, block_env })
138    }
139
140    fn next_evm_env(
141        &self,
142        parent: &Header,
143        attributes: &NextBlockEnvAttributes,
144    ) -> Result<EvmEnv<SpecId>, Self::Error> {
145        let chain_id = self.chain_spec.chain().id();
146        let arbos_version = arbos_version_from_mix_hash(&attributes.prev_randao);
147        let spec = self.chain_spec.spec_id_by_arbos_version(arbos_version);
148
149        let l1_block_number_initial = l1_block_number_from_mix_hash(&attributes.prev_randao);
150        stage_rpc_block_ctx(
151            self.executor_factory.arb_evm_factory(),
152            arbos_version,
153            attributes.timestamp,
154            l1_block_number_initial,
155            parent.number().saturating_add(1),
156            self.executor_factory.allow_debug_precompiles(),
157        );
158
159        let cfg_env = arb_cfg_env(chain_id, spec, arbos_version);
160        // Arbitrum sets PREVRANDAO to BigToHash(difficulty), which is 0x...0001.
161        let prevrandao = B256::from(U256::from(1));
162        let block_env = BlockEnv {
163            number: U256::from(l1_block_number_initial),
164            beneficiary: attributes.suggested_fee_recipient,
165            timestamp: U256::from(attributes.timestamp),
166            difficulty: U256::from(1),
167            prevrandao: Some(prevrandao),
168            gas_limit: attributes.gas_limit,
169            basefee: parent.base_fee_per_gas().unwrap_or_default(),
170            blob_excess_gas_and_price: if spec.is_enabled_in(SpecId::CANCUN) {
171                Some(revm::context_interface::block::BlobExcessGasAndPrice {
172                    excess_blob_gas: 0,
173                    blob_gasprice: 0,
174                })
175            } else {
176                None
177            },
178        };
179
180        Ok(EvmEnv { cfg_env, block_env })
181    }
182
183    fn context_for_block<'a>(
184        &self,
185        block: &'a SealedBlock<alloy_consensus::Block<arb_primitives::ArbTransactionSigned>>,
186    ) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
187        // Encode delayed_messages_read (from header nonce) as bytes 32-39 of extra_data,
188        // and L2 block number as bytes 40-47.
189        let mut extra = block.header().extra_data.to_vec();
190        extra.extend_from_slice(&block.header().nonce.0);
191        extra.extend_from_slice(&block.header().number.to_be_bytes());
192        Ok(EthBlockExecutionCtx {
193            tx_count_hint: Some(block.transaction_count()),
194            parent_hash: block.header().parent_hash,
195            parent_beacon_block_root: block.header().parent_beacon_block_root,
196            ommers: &[],
197            withdrawals: None,
198            extra_data: extra.into(),
199        })
200    }
201
202    fn context_for_next_block(
203        &self,
204        parent: &SealedHeader<Header>,
205        attributes: NextBlockEnvAttributes,
206    ) -> Result<EthBlockExecutionCtx<'_>, Self::Error> {
207        Ok(EthBlockExecutionCtx {
208            tx_count_hint: None,
209            parent_hash: parent.hash(),
210            parent_beacon_block_root: attributes.parent_beacon_block_root,
211            ommers: &[],
212            withdrawals: None,
213            extra_data: attributes.extra_data,
214        })
215    }
216}
217
218impl<ChainSpec> ConfigureEngineEvm<ExecutionData> for ArbEvmConfig<ChainSpec>
219where
220    ChainSpec:
221        EthExecutorSpec + EthChainSpec<Header = Header> + ArbitrumChainSpec + Hardforks + 'static,
222{
223    fn evm_env_for_payload(&self, payload: &ExecutionData) -> Result<EvmEnvFor<Self>, Self::Error> {
224        let prev_randao = payload.payload.as_v1().prev_randao;
225        let arbos_version = arbos_version_from_mix_hash(&prev_randao);
226        let spec = self.chain_spec.spec_id_by_arbos_version(arbos_version);
227
228        // Arbitrum overrides NUMBER to return the L1 block number, not L2.
229        let l1_block_number = l1_block_number_from_mix_hash(&prev_randao);
230        stage_rpc_block_ctx(
231            self.executor_factory.arb_evm_factory(),
232            arbos_version,
233            payload.payload.timestamp(),
234            l1_block_number,
235            payload.payload.block_number(),
236            self.executor_factory.allow_debug_precompiles(),
237        );
238
239        let cfg_env = arb_cfg_env(self.chain_spec.chain().id(), spec, arbos_version);
240
241        // Arbitrum sets PREVRANDAO to BigToHash(difficulty), which is 0x...0001.
242        let prevrandao = B256::from(U256::from(1));
243        let block_env = BlockEnv {
244            number: U256::from(l1_block_number),
245            beneficiary: payload.payload.fee_recipient(),
246            timestamp: U256::from(payload.payload.timestamp()),
247            difficulty: U256::from(1),
248            prevrandao: Some(prevrandao),
249            gas_limit: payload.payload.gas_limit(),
250            basefee: payload.payload.saturated_base_fee_per_gas(),
251            blob_excess_gas_and_price: if spec.is_enabled_in(SpecId::CANCUN) {
252                Some(revm::context_interface::block::BlobExcessGasAndPrice {
253                    excess_blob_gas: 0,
254                    blob_gasprice: 0,
255                })
256            } else {
257                None
258            },
259        };
260
261        Ok(EvmEnv { cfg_env, block_env })
262    }
263
264    fn context_for_payload<'a>(
265        &self,
266        payload: &'a ExecutionData,
267    ) -> Result<ExecutionCtxFor<'a, Self>, Self::Error> {
268        Ok(EthBlockExecutionCtx {
269            tx_count_hint: Some(payload.payload.transactions().len()),
270            parent_hash: payload.parent_hash(),
271            parent_beacon_block_root: payload.sidecar.parent_beacon_block_root(),
272            ommers: &[],
273            withdrawals: None,
274            extra_data: payload.payload.as_v1().extra_data.clone(),
275        })
276    }
277
278    fn tx_iterator_for_payload(
279        &self,
280        payload: &ExecutionData,
281    ) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
282        let txs = payload.payload.transactions().clone();
283        let convert = |tx: Bytes| {
284            let tx =
285                TxTy::<Self::Primitives>::decode_2718_exact(tx.as_ref()).map_err(AnyError::new)?;
286            let signer = tx.try_recover().map_err(AnyError::new)?;
287            Ok::<_, AnyError>(tx.with_signer(signer))
288        };
289        Ok((txs, convert))
290    }
291}
292
293impl<ChainSpec> ArbEvmConfig<ChainSpec>
294where
295    ChainSpec: EthChainSpec + 'static,
296{
297    /// Build an `ArbBlockExecutionCtx` from a sealed block header.
298    pub fn arb_context_for_block(
299        &self,
300        header: &Header,
301        parent_hash: B256,
302    ) -> ArbBlockExecutionCtx {
303        let mix_hash = header.mix_hash;
304        ArbBlockExecutionCtx {
305            parent_hash,
306            parent_beacon_block_root: header.parent_beacon_block_root,
307            extra_data: header.extra_data.to_vec(),
308            delayed_messages_read: u64::from_be_bytes(header.nonce.0),
309            l1_block_number: l1_block_number_from_mix_hash(&mix_hash),
310            l2_block_number: header.number,
311            chain_id: self.chain_spec.chain().id(),
312            block_timestamp: header.timestamp,
313            basefee: U256::from(header.base_fee_per_gas.unwrap_or_default()),
314            time_passed: 0,
315            l1_base_fee: U256::ZERO,
316            arbos_version: arbos_version_from_mix_hash(&mix_hash),
317            coinbase: header.beneficiary,
318            // State-derived fields populated by block executor after state open.
319            l1_price_per_unit: U256::ZERO,
320            brotli_compression_level: 0,
321            network_fee_account: Address::ZERO,
322            infra_fee_account: Address::ZERO,
323            min_base_fee: U256::ZERO,
324        }
325    }
326
327    /// Build an `ArbBlockExecutionCtx` from next-block attributes.
328    pub fn arb_context_for_next_block(
329        &self,
330        parent: &SealedHeader<Header>,
331        prev_randao: &B256,
332        extra_data: &[u8],
333    ) -> ArbBlockExecutionCtx {
334        let l1_block_number = l1_block_number_from_mix_hash(prev_randao);
335        ArbBlockExecutionCtx {
336            parent_hash: parent.hash(),
337            parent_beacon_block_root: parent.parent_beacon_block_root(),
338            extra_data: extra_data.to_vec(),
339            delayed_messages_read: 0,
340            l1_block_number,
341            l2_block_number: parent.number().saturating_add(1),
342            chain_id: self.chain_spec.chain().id(),
343            block_timestamp: parent.timestamp(),
344            basefee: U256::from(parent.base_fee_per_gas().unwrap_or_default()),
345            time_passed: 0,
346            l1_base_fee: U256::ZERO,
347            arbos_version: 0,
348            coinbase: Address::ZERO,
349            l1_price_per_unit: U256::ZERO,
350            brotli_compression_level: 0,
351            network_fee_account: Address::ZERO,
352            infra_fee_account: Address::ZERO,
353            min_base_fee: U256::ZERO,
354        }
355    }
356}
357
358/// Stage a per-block context on the EVM factory so that `create_evm`
359/// installs it on the EVM-execution thread. This is required because
360/// reth's RPC dispatcher (`spawn_blocking`) runs `evm_env` and
361/// `create_evm` on different threads, so a thread-local install on the
362/// `evm_env` thread would not survive.
363fn stage_rpc_block_ctx(
364    factory: &ArbEvmFactory,
365    arbos_version: u64,
366    block_timestamp: u64,
367    l1_block_number: u64,
368    l2_block_number: u64,
369    allow_debug: bool,
370) {
371    let block_ctx = arb_context::BlockCtx::new_with_caches(
372        arbos_version,
373        block_timestamp,
374        l1_block_number,
375        l2_block_number,
376        allow_debug,
377        factory.chain_caches().clone(),
378    );
379    let ctx = std::sync::Arc::new(arb_context::ArbPrecompileCtx {
380        block: std::sync::Arc::new(block_ctx),
381        tx: std::sync::Arc::new(parking_lot::Mutex::new(arb_context::TxCtx::default())),
382        caller_stack: std::sync::Arc::new(parking_lot::Mutex::new(Vec::new())),
383        evm_depth: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
384        stylus_frame_depth: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
385    });
386    factory.stage_ctx(ctx);
387}
388
389/// Build a `CfgEnv` with Arbitrum-specific overrides.
390///
391/// Disables EIP-3541 (0xEF rejection) for Stylus-era blocks so that
392/// Stylus WASM programs can be deployed. Disables the priority fee
393/// ordering check (Arbitrum tips are always dropped). Disables EIP-7623
394/// increased calldata cost (irrelevant on L2 without blobs).
395fn arb_cfg_env(chain_id: u64, spec: SpecId, arbos_version: u64) -> CfgEnv {
396    let mut cfg = CfgEnv::new()
397        .with_chain_id(chain_id)
398        .with_spec_and_mainnet_gas_params(spec);
399    // Arbitrum drops tips — max_priority_fee can exceed max_fee.
400    cfg.disable_priority_fee_check = true;
401    // EIP-7623 increases calldata cost for blob-less chains; irrelevant on L2.
402    cfg.disable_eip7623 = true;
403    // EIP-3607 rejects txs from senders with deployed code. Arbitrum L1-to-L2
404    // tx types (ContractTx, RetryTx) may have L1 contract alias senders with
405    // code on L2. skipTransactionChecks() skips this for those types.
406    cfg.disable_eip3607 = true;
407    // Stylus programs start with 0xEF; allow deployment once Stylus is live.
408    // Non-Stylus 0xEF prefixes are re-rejected in ArbEvm::frame_return_result.
409    if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS {
410        cfg.disable_eip3541 = true;
411    }
412    // Disable revm's nonce and balance validation globally. Arbitrum's internal,
413    // deposit, and retryable tx types need to bypass these checks (special
414    // balance/nonce semantics). We manually validate balance for user txs in
415    // execute_transaction_without_commit. disable_nonce_check only disables
416    // validation, not increment — the nonce is still incremented after execution.
417    cfg.disable_balance_check = true;
418    cfg.disable_nonce_check = true;
419    // Disable base fee validation for Arbitrum tx types (RetryTx, etc.)
420    // whose gas_fee_cap may not follow standard EIP-1559 rules.
421    // Also needed for debug_traceTransaction to replay these tx types.
422    cfg.disable_base_fee = true;
423    // Disable EIP-7825 per-tx gas cap. Arbitrum uses ArbOS-controlled
424    // PerTxGasLimit instead, applied during the gas-charging hook.
425    cfg.tx_gas_limit_cap = Some(u64::MAX);
426    cfg
427}
428
429/// Extract ArbOS version from header mix_hash (bytes 16-23).
430pub fn arbos_version_from_mix_hash(mix_hash: &B256) -> u64 {
431    let mut buf = [0u8; 8];
432    buf.copy_from_slice(&mix_hash.0[16..24]);
433    u64::from_be_bytes(buf)
434}
435
436/// Extract L1 block number from header mix_hash (bytes 8-15).
437pub fn l1_block_number_from_mix_hash(mix_hash: &B256) -> u64 {
438    let mut buf = [0u8; 8];
439    buf.copy_from_slice(&mix_hash.0[8..16]);
440    u64::from_be_bytes(buf)
441}