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