arb_evm/
assembler.rs

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