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