arb_evm/
assembler.rs

1use alloc::{sync::Arc, vec::Vec};
2use core::marker::PhantomData;
3
4use alloy_consensus::{
5    Block, BlockBody, BlockHeader, EMPTY_OMMER_ROOT_HASH, Header, TxReceipt, proofs,
6};
7use alloy_evm::{
8    block::{BlockExecutionError, BlockExecutionResult, BlockExecutorFactory},
9    eth::EthBlockExecutionCtx,
10};
11use alloy_primitives::{B64, B256, U256};
12use arbos::header::{ArbHeaderInfo, derive_arb_header_info, read_l2_base_fee};
13use reth_evm::execute::{BlockAssembler, BlockAssemblerInput};
14use reth_primitives_traits::{Receipt, SignedTransaction, logs_bloom};
15use revm::context::Block as RevmBlock;
16
17/// Arbitrum block assembler.
18///
19/// Constructs block headers with Arbitrum-specific fields:
20/// - `extra_data`: send root in first 32 bytes
21/// - `mix_hash`: encodes (send_count, l1_block_number, arbos_version)
22/// - `nonce`: delayed_messages_read
23/// - `difficulty`: always 1
24#[derive(Debug, Clone, Default)]
25pub struct ArbBlockAssembler<ChainSpec> {
26    _phantom: PhantomData<ChainSpec>,
27}
28
29impl<ChainSpec> ArbBlockAssembler<ChainSpec> {
30    pub fn new(_chain_spec: Arc<ChainSpec>) -> Self {
31        Self {
32            _phantom: PhantomData,
33        }
34    }
35}
36
37impl<F, ChainSpec> BlockAssembler<F> for ArbBlockAssembler<ChainSpec>
38where
39    F: for<'a> BlockExecutorFactory<
40            ExecutionCtx<'a> = EthBlockExecutionCtx<'a>,
41            Transaction: SignedTransaction,
42            Receipt: Receipt,
43        >,
44    ChainSpec: Send + Sync + Unpin + 'static,
45{
46    type Block = Block<F::Transaction>;
47
48    fn assemble_block(
49        &self,
50        input: BlockAssemblerInput<'_, '_, F>,
51    ) -> Result<Self::Block, BlockExecutionError> {
52        let BlockAssemblerInput {
53            evm_env,
54            execution_ctx: ctx,
55            parent,
56            transactions,
57            output: BlockExecutionResult {
58                receipts, gas_used, ..
59            },
60            bundle_state,
61            state_provider,
62            state_root,
63            ..
64        } = input;
65
66        // L2 block number is parent + 1. We cannot use block_env.number because
67        // Arbitrum overrides it to hold the L1 block number (for the NUMBER opcode).
68        let l2_block_number = parent.number().saturating_add(1);
69
70        let timestamp = evm_env.block_env.timestamp().saturating_to();
71
72        let transactions_root = proofs::calculate_transaction_root(&transactions);
73        let receipts_root = proofs::calculate_receipt_root(
74            &receipts
75                .iter()
76                .map(|r| r.with_bloom_ref())
77                .collect::<Vec<_>>(),
78        );
79        let logs_bloom = logs_bloom(receipts.iter().flat_map(|r| r.logs()));
80
81        // Derive send root, send count, l1 block number, and arbos version
82        // from the post-execution state.
83        let arb_info = derive_header_info_from_state(
84            state_provider,
85            bundle_state,
86            evm_env.block_env.beneficiary(),
87        )?;
88
89        let mix_hash = arb_info
90            .as_ref()
91            .map(|info| info.compute_mix_hash())
92            .unwrap_or_else(|| evm_env.block_env.prevrandao().unwrap_or_default());
93
94        let extra_data = arb_info
95            .as_ref()
96            .map(|info| {
97                let mut data = info.send_root.to_vec();
98                data.resize(32, 0);
99                data.into()
100            })
101            .unwrap_or_else(|| ctx.extra_data.clone());
102
103        // Decode delayed_messages_read from bytes 32-39 of the execution context's extra_data.
104        let extra_bytes = ctx.extra_data.as_ref();
105        let delayed_messages_read = if extra_bytes.len() >= 40 {
106            let mut buf = [0u8; 8];
107            buf.copy_from_slice(&extra_bytes[32..40]);
108            u64::from_be_bytes(buf)
109        } else {
110            0
111        };
112
113        let header = Header {
114            parent_hash: ctx.parent_hash,
115            ommers_hash: EMPTY_OMMER_ROOT_HASH,
116            beneficiary: evm_env.block_env.beneficiary(),
117            state_root,
118            transactions_root,
119            receipts_root,
120            withdrawals_root: None,
121            logs_bloom,
122            timestamp,
123            mix_hash,
124            nonce: B64::from(delayed_messages_read.to_be_bytes()),
125            base_fee_per_gas: Some(
126                read_base_fee_from_state(state_provider, bundle_state)?
127                    .unwrap_or(evm_env.block_env.basefee()),
128            ),
129            number: l2_block_number,
130            gas_limit: evm_env.block_env.gas_limit(),
131            difficulty: U256::from(1),
132            gas_used: *gas_used,
133            extra_data,
134            parent_beacon_block_root: None,
135            blob_gas_used: None,
136            excess_blob_gas: None,
137            requests_hash: None,
138        };
139
140        Ok(Block {
141            header,
142            body: BlockBody {
143                transactions,
144                ommers: Default::default(),
145                withdrawals: None,
146            },
147        })
148    }
149}
150
151/// Read the L2 baseFee from post-execution state.
152///
153/// Checks bundle_state first (pending changes from current block), then falls
154/// back to committed state. The baseFee in L2PricingState at this point is the
155/// value written by the CURRENT block's StartBlock (for the next block).
156/// However, the committed state has the value from BEFORE the current block's
157/// execution — the correct value for the current block's header.
158fn read_base_fee_from_state(
159    state_provider: &dyn reth_storage_api::StateProvider,
160    _bundle_state: &revm_database::BundleState,
161) -> Result<Option<u64>, BlockExecutionError> {
162    // Read from committed state (pre-execution baseFee = current block's header baseFee).
163    let read_slot =
164        |addr: alloy_primitives::Address, slot: B256| state_provider.storage(addr, slot);
165    read_l2_base_fee(&read_slot).map_err(BlockExecutionError::other)
166}
167
168/// Derive ArbHeaderInfo by reading ArbOS state from the post-execution state.
169///
170/// Combines bundle_state (pending changes) with state_provider (committed state)
171/// to read the Merkle accumulator's send root/count and L1 block number.
172fn derive_header_info_from_state(
173    state_provider: &dyn reth_storage_api::StateProvider,
174    bundle_state: &revm_database::BundleState,
175    coinbase: alloy_primitives::Address,
176) -> Result<Option<ArbHeaderInfo>, BlockExecutionError> {
177    let read_slot = |addr: alloy_primitives::Address, slot: B256| {
178        // Check bundle state first (post-execution changes).
179        if let Some(account) = bundle_state.state.get(&addr) {
180            let slot_u256 = U256::from_be_bytes(slot.0);
181            if let Some(storage_slot) = account.storage.get(&slot_u256) {
182                return Ok(Some(storage_slot.present_value));
183            }
184        }
185        // Fall back to the committed state provider.
186        state_provider.storage(addr, slot)
187    };
188
189    derive_arb_header_info(&read_slot, coinbase).map_err(BlockExecutionError::other)
190}