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        let cfg_env = arb_cfg_env(chain_id, spec, arbos_version);
92        // Arbitrum sets PREVRANDAO to BigToHash(difficulty), which is 0x...0001.
93        let prevrandao = B256::from(U256::from(1));
94        let block_env = BlockEnv {
95            number: U256::from(l1_block_number),
96            beneficiary: header.beneficiary(),
97            timestamp: U256::from(header.timestamp()),
98            difficulty: header.difficulty(),
99            prevrandao: Some(prevrandao),
100            gas_limit: header.gas_limit(),
101            basefee: header.base_fee_per_gas().unwrap_or_default(),
102            // Arbitrum has no blobs — BLOBBASEFEE opcode returns 0.
103            blob_excess_gas_and_price: None,
104        };
105
106        Ok(EvmEnv { cfg_env, block_env })
107    }
108
109    fn next_evm_env(
110        &self,
111        parent: &Header,
112        attributes: &NextBlockEnvAttributes,
113    ) -> Result<EvmEnv<SpecId>, Self::Error> {
114        let chain_id = self.chain_spec.chain().id();
115        let arbos_version = arbos_version_from_mix_hash(&attributes.prev_randao);
116        let spec = self.chain_spec.spec_id_by_arbos_version(arbos_version);
117
118        let cfg_env = arb_cfg_env(chain_id, spec, arbos_version);
119        // Arbitrum overrides NUMBER to return the L1 block number, not L2.
120        let l1_block_number = l1_block_number_from_mix_hash(&attributes.prev_randao);
121        // Arbitrum sets PREVRANDAO to BigToHash(difficulty), which is 0x...0001.
122        let prevrandao = B256::from(U256::from(1));
123        let block_env = BlockEnv {
124            number: U256::from(l1_block_number),
125            beneficiary: attributes.suggested_fee_recipient,
126            timestamp: U256::from(attributes.timestamp),
127            difficulty: U256::from(1),
128            prevrandao: Some(prevrandao),
129            gas_limit: attributes.gas_limit,
130            basefee: parent.base_fee_per_gas().unwrap_or_default(),
131            // Arbitrum has no blobs — BLOBBASEFEE opcode returns 0.
132            blob_excess_gas_and_price: None,
133        };
134
135        Ok(EvmEnv { cfg_env, block_env })
136    }
137
138    fn context_for_block<'a>(
139        &self,
140        block: &'a SealedBlock<alloy_consensus::Block<arb_primitives::ArbTransactionSigned>>,
141    ) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
142        // Encode delayed_messages_read (from header nonce) as bytes 32-39 of extra_data,
143        // and L2 block number as bytes 40-47.
144        let mut extra = block.header().extra_data.to_vec();
145        extra.extend_from_slice(&block.header().nonce.0);
146        extra.extend_from_slice(&block.header().number.to_be_bytes());
147        Ok(EthBlockExecutionCtx {
148            tx_count_hint: Some(block.transaction_count()),
149            parent_hash: block.header().parent_hash,
150            parent_beacon_block_root: block.header().parent_beacon_block_root,
151            ommers: &[],
152            withdrawals: None,
153            extra_data: extra.into(),
154        })
155    }
156
157    fn context_for_next_block(
158        &self,
159        parent: &SealedHeader<Header>,
160        attributes: NextBlockEnvAttributes,
161    ) -> Result<EthBlockExecutionCtx<'_>, Self::Error> {
162        Ok(EthBlockExecutionCtx {
163            tx_count_hint: None,
164            parent_hash: parent.hash(),
165            parent_beacon_block_root: attributes.parent_beacon_block_root,
166            ommers: &[],
167            withdrawals: None,
168            extra_data: attributes.extra_data,
169        })
170    }
171}
172
173impl<ChainSpec> ConfigureEngineEvm<ExecutionData> for ArbEvmConfig<ChainSpec>
174where
175    ChainSpec:
176        EthExecutorSpec + EthChainSpec<Header = Header> + ArbitrumChainSpec + Hardforks + 'static,
177{
178    fn evm_env_for_payload(&self, payload: &ExecutionData) -> Result<EvmEnvFor<Self>, Self::Error> {
179        let prev_randao = payload.payload.as_v1().prev_randao;
180        let arbos_version = arbos_version_from_mix_hash(&prev_randao);
181        let spec = self.chain_spec.spec_id_by_arbos_version(arbos_version);
182
183        let cfg_env = arb_cfg_env(self.chain_spec.chain().id(), spec, arbos_version);
184
185        // Arbitrum overrides NUMBER to return the L1 block number, not L2.
186        let l1_block_number = l1_block_number_from_mix_hash(&prev_randao);
187        // Arbitrum sets PREVRANDAO to BigToHash(difficulty), which is 0x...0001.
188        let prevrandao = B256::from(U256::from(1));
189        let block_env = BlockEnv {
190            number: U256::from(l1_block_number),
191            beneficiary: payload.payload.fee_recipient(),
192            timestamp: U256::from(payload.payload.timestamp()),
193            difficulty: U256::from(1),
194            prevrandao: Some(prevrandao),
195            gas_limit: payload.payload.gas_limit(),
196            basefee: payload.payload.saturated_base_fee_per_gas(),
197            // Arbitrum has no blobs — BLOBBASEFEE opcode returns 0.
198            blob_excess_gas_and_price: None,
199        };
200
201        Ok(EvmEnv { cfg_env, block_env })
202    }
203
204    fn context_for_payload<'a>(
205        &self,
206        payload: &'a ExecutionData,
207    ) -> Result<ExecutionCtxFor<'a, Self>, Self::Error> {
208        Ok(EthBlockExecutionCtx {
209            tx_count_hint: Some(payload.payload.transactions().len()),
210            parent_hash: payload.parent_hash(),
211            parent_beacon_block_root: payload.sidecar.parent_beacon_block_root(),
212            ommers: &[],
213            withdrawals: None,
214            extra_data: payload.payload.as_v1().extra_data.clone(),
215        })
216    }
217
218    fn tx_iterator_for_payload(
219        &self,
220        payload: &ExecutionData,
221    ) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
222        let txs = payload.payload.transactions().clone();
223        let convert = |tx: Bytes| {
224            let tx =
225                TxTy::<Self::Primitives>::decode_2718_exact(tx.as_ref()).map_err(AnyError::new)?;
226            let signer = tx.try_recover().map_err(AnyError::new)?;
227            Ok::<_, AnyError>(tx.with_signer(signer))
228        };
229        Ok((txs, convert))
230    }
231}
232
233impl<ChainSpec> ArbEvmConfig<ChainSpec>
234where
235    ChainSpec: EthChainSpec + 'static,
236{
237    /// Build an `ArbBlockExecutionCtx` from a sealed block header.
238    pub fn arb_context_for_block(
239        &self,
240        header: &Header,
241        parent_hash: B256,
242    ) -> ArbBlockExecutionCtx {
243        let mix_hash = header.mix_hash;
244        ArbBlockExecutionCtx {
245            parent_hash,
246            parent_beacon_block_root: header.parent_beacon_block_root,
247            extra_data: header.extra_data.to_vec(),
248            delayed_messages_read: u64::from_be_bytes(header.nonce.0),
249            l1_block_number: l1_block_number_from_mix_hash(&mix_hash),
250            l2_block_number: header.number,
251            chain_id: self.chain_spec.chain().id(),
252            block_timestamp: header.timestamp,
253            basefee: U256::from(header.base_fee_per_gas.unwrap_or_default()),
254            time_passed: 0,
255            l1_base_fee: U256::ZERO,
256            arbos_version: arbos_version_from_mix_hash(&mix_hash),
257            coinbase: header.beneficiary,
258            // State-derived fields populated by block executor after state open.
259            l1_price_per_unit: U256::ZERO,
260            brotli_compression_level: 0,
261            network_fee_account: Address::ZERO,
262            infra_fee_account: Address::ZERO,
263            min_base_fee: U256::ZERO,
264        }
265    }
266
267    /// Build an `ArbBlockExecutionCtx` from next-block attributes.
268    pub fn arb_context_for_next_block(
269        &self,
270        parent: &SealedHeader<Header>,
271        prev_randao: &B256,
272        extra_data: &[u8],
273    ) -> ArbBlockExecutionCtx {
274        let l1_block_number = l1_block_number_from_mix_hash(prev_randao);
275        ArbBlockExecutionCtx {
276            parent_hash: parent.hash(),
277            parent_beacon_block_root: parent.parent_beacon_block_root(),
278            extra_data: extra_data.to_vec(),
279            delayed_messages_read: 0,
280            l1_block_number,
281            l2_block_number: parent.number().saturating_add(1),
282            chain_id: self.chain_spec.chain().id(),
283            block_timestamp: parent.timestamp(),
284            basefee: U256::from(parent.base_fee_per_gas().unwrap_or_default()),
285            time_passed: 0,
286            l1_base_fee: U256::ZERO,
287            arbos_version: 0,
288            coinbase: Address::ZERO,
289            l1_price_per_unit: U256::ZERO,
290            brotli_compression_level: 0,
291            network_fee_account: Address::ZERO,
292            infra_fee_account: Address::ZERO,
293            min_base_fee: U256::ZERO,
294        }
295    }
296}
297
298/// Build a `CfgEnv` with Arbitrum-specific overrides.
299///
300/// Disables EIP-3541 (0xEF rejection) for Stylus-era blocks so that
301/// Stylus WASM programs can be deployed. Disables the priority fee
302/// ordering check (Arbitrum tips are always dropped). Disables EIP-7623
303/// increased calldata cost (irrelevant on L2 without blobs).
304fn arb_cfg_env(chain_id: u64, spec: SpecId, arbos_version: u64) -> CfgEnv {
305    let mut cfg = CfgEnv::new()
306        .with_chain_id(chain_id)
307        .with_spec_and_mainnet_gas_params(spec);
308    // Arbitrum drops tips — max_priority_fee can exceed max_fee.
309    cfg.disable_priority_fee_check = true;
310    // EIP-7623 increases calldata cost for blob-less chains; irrelevant on L2.
311    cfg.disable_eip7623 = true;
312    // EIP-3607 rejects txs from senders with deployed code. Arbitrum L1-to-L2
313    // tx types (ContractTx, RetryTx) may have L1 contract alias senders with
314    // code on L2. skipTransactionChecks() skips this for those types.
315    cfg.disable_eip3607 = true;
316    // Stylus programs start with 0xEF; allow deployment once Stylus is live.
317    if arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_STYLUS {
318        cfg.disable_eip3541 = true;
319    }
320    // Disable revm's nonce and balance validation globally. Arbitrum's internal,
321    // deposit, and retryable tx types need to bypass these checks (special
322    // balance/nonce semantics). We manually validate balance for user txs in
323    // execute_transaction_without_commit. disable_nonce_check only disables
324    // validation, not increment — the nonce is still incremented after execution.
325    cfg.disable_balance_check = true;
326    cfg.disable_nonce_check = true;
327    // Disable base fee validation for Arbitrum tx types (RetryTx, etc.)
328    // whose gas_fee_cap may not follow standard EIP-1559 rules.
329    // Also needed for debug_traceTransaction to replay these tx types.
330    cfg.disable_base_fee = true;
331    cfg
332}
333
334/// Extract ArbOS version from header mix_hash (bytes 16-23).
335pub fn arbos_version_from_mix_hash(mix_hash: &B256) -> u64 {
336    let mut buf = [0u8; 8];
337    buf.copy_from_slice(&mix_hash.0[16..24]);
338    u64::from_be_bytes(buf)
339}
340
341/// Extract L1 block number from header mix_hash (bytes 8-15).
342pub fn l1_block_number_from_mix_hash(mix_hash: &B256) -> u64 {
343    let mut buf = [0u8; 8];
344    buf.copy_from_slice(&mix_hash.0[8..16]);
345    u64::from_be_bytes(buf)
346}