arbos/
block_processor.rs

1use alloy_primitives::{Address, B256, U256};
2
3use arb_chainspec::arbos_version as arb_ver;
4
5use crate::{header::ArbHeaderInfo, internal_tx::L1Info, l2_pricing::GETH_BLOCK_GAS_LIMIT};
6
7/// Standard Ethereum transaction gas.
8const TX_GAS: u64 = 21_000;
9
10// =====================================================================
11// Conditional options
12// =====================================================================
13
14/// Conditional options that may be attached to a transaction.
15#[derive(Debug, Clone, Default)]
16pub struct ConditionalOptions {
17    pub known_accounts: Vec<(Address, Option<B256>)>,
18    pub block_number_min: Option<u64>,
19    pub block_number_max: Option<u64>,
20    pub timestamp_min: Option<u64>,
21    pub timestamp_max: Option<u64>,
22}
23
24// =====================================================================
25// Sequencing hooks
26// =====================================================================
27
28/// Hooks for the sequencer to control block production.
29pub trait SequencingHooks {
30    /// Returns the next transaction to include, or None if the block is complete.
31    fn next_tx_to_sequence(&mut self) -> Option<Vec<u8>>;
32
33    /// Filters a transaction before execution.
34    fn pre_tx_filter(&self, tx: &[u8]) -> Result<(), String>;
35
36    /// Filters a transaction after execution.
37    fn post_tx_filter(&self, tx: &[u8], result: &[u8]) -> Result<(), String>;
38
39    /// Determines whether to discard invalid txs early.
40    fn discard_invalid_txs_early(&self) -> bool;
41
42    /// Block-level filter applied after all transactions are processed.
43    fn block_filter(
44        &self,
45        _header: &NewHeaderResult,
46        _txs: &[Vec<u8>],
47        _receipts: &[Vec<u8>],
48    ) -> Result<(), String> {
49        Ok(())
50    }
51
52    /// Inserts the error for the last tx.
53    fn insert_last_tx_error(&mut self, _err: String) {}
54}
55
56/// Default no-op implementation for sequencing hooks.
57pub struct NoopSequencingHooks;
58
59impl SequencingHooks for NoopSequencingHooks {
60    fn next_tx_to_sequence(&mut self) -> Option<Vec<u8>> {
61        None
62    }
63
64    fn pre_tx_filter(&self, _tx: &[u8]) -> Result<(), String> {
65        Ok(())
66    }
67
68    fn post_tx_filter(&self, _tx: &[u8], _result: &[u8]) -> Result<(), String> {
69        Ok(())
70    }
71
72    fn discard_invalid_txs_early(&self) -> bool {
73        false
74    }
75}
76
77// =====================================================================
78// Block production types
79// =====================================================================
80
81/// The result of block production.
82#[derive(Debug, Clone)]
83pub struct BlockProductionResult {
84    pub l1_info: L1Info,
85    pub num_txs: usize,
86    pub gas_used: u64,
87}
88
89/// Parameters for creating a new block header.
90#[derive(Debug, Clone)]
91pub struct NewHeaderParams {
92    pub parent_hash: B256,
93    pub parent_number: u64,
94    pub parent_timestamp: u64,
95    pub parent_extra_data: Vec<u8>,
96    pub parent_mix_hash: B256,
97    pub coinbase: Address,
98    pub timestamp: u64,
99    pub base_fee: U256,
100}
101
102/// Computed header fields from `create_new_header`.
103#[derive(Debug, Clone)]
104pub struct NewHeaderResult {
105    pub parent_hash: B256,
106    pub coinbase: Address,
107    pub number: u64,
108    pub gas_limit: u64,
109    pub timestamp: u64,
110    pub extra_data: Vec<u8>,
111    pub mix_hash: B256,
112    pub base_fee: U256,
113    pub difficulty: U256,
114}
115
116// =====================================================================
117// Header creation and finalization
118// =====================================================================
119
120/// Create new header fields for an Arbitrum block.
121///
122/// In reth, the actual header construction is done by the block builder;
123/// this computes the Arbitrum-specific fields.
124pub fn create_new_header(
125    l1_info: Option<&L1Info>,
126    prev_hash: B256,
127    prev_number: u64,
128    prev_timestamp: u64,
129    prev_extra: &[u8],
130    prev_mix_hash: B256,
131    base_fee: U256,
132) -> NewHeaderResult {
133    let mut timestamp = 0u64;
134    let mut coinbase = Address::ZERO;
135
136    if let Some(info) = l1_info {
137        timestamp = info.l1_timestamp;
138        coinbase = info.poster;
139    }
140
141    if timestamp < prev_timestamp {
142        timestamp = prev_timestamp;
143    }
144
145    let mut extra_data = vec![0u8; 32];
146    let copy_len = prev_extra.len().min(32);
147    extra_data[..copy_len].copy_from_slice(&prev_extra[..copy_len]);
148
149    NewHeaderResult {
150        parent_hash: prev_hash,
151        coinbase,
152        number: prev_number + 1,
153        gas_limit: GETH_BLOCK_GAS_LIMIT,
154        timestamp,
155        extra_data,
156        mix_hash: prev_mix_hash,
157        base_fee,
158        difficulty: U256::from(1),
159    }
160}
161
162/// Compute the Arbitrum header info to finalize a block.
163///
164/// This corresponds to `FinalizeBlock` which sets header fields from ArbOS state.
165/// We derive the info and let the block assembler apply it.
166pub fn finalize_block_header_info(
167    send_root: B256,
168    send_count: u64,
169    l1_block_number: u64,
170    arbos_version: u64,
171) -> ArbHeaderInfo {
172    ArbHeaderInfo {
173        send_root,
174        send_count,
175        l1_block_number,
176        arbos_format_version: arbos_version,
177    }
178}
179
180// =====================================================================
181// Block production engine
182// =====================================================================
183
184/// The outcome of attempting to apply a single transaction.
185#[derive(Debug)]
186pub enum TxOutcome {
187    /// Transaction was executed successfully.
188    Success(TxResult),
189    /// Transaction was invalid and should be skipped.
190    Invalid(String),
191}
192
193/// A successfully executed transaction's metadata.
194#[derive(Debug, Clone)]
195pub struct TxResult {
196    /// Gas used by this transaction (from header gas tracking).
197    pub gas_used: u64,
198    /// L1 poster data gas for this transaction.
199    pub data_gas: u64,
200    /// Whether the EVM execution itself succeeded (receipt status).
201    pub evm_success: bool,
202    /// Scheduled retryable redeems produced by this tx.
203    pub scheduled_txs: Vec<Vec<u8>>,
204    /// Whether the EVM reported an error (internal txs must not fail).
205    pub evm_error: Option<String>,
206}
207
208/// Per-tx decision made by the block production loop.
209#[derive(Debug)]
210pub enum TxAction {
211    /// Execute the internal start-block transaction.
212    ExecuteStartBlock,
213    /// Execute a retryable redeem.
214    ExecuteRedeem(Vec<u8>),
215    /// Execute a user/sequencer transaction.
216    ExecuteUserTx(Vec<u8>),
217    /// Block is complete.
218    Done,
219}
220
221/// Tracks block-level state during production.
222///
223/// The block executor creates a `BlockProductionState` at the start of the
224/// block, then calls `next_tx_action` in a loop to get transactions, and
225/// `record_tx_outcome` after executing each one. After the loop, call
226/// `finalize` for post-block checks.
227#[derive(Debug)]
228pub struct BlockProductionState {
229    /// Block gas remaining for rate-limiting.
230    pub block_gas_left: u64,
231    /// Pending retryable redeems scheduled by prior transactions.
232    redeems: Vec<Vec<u8>>,
233    /// Whether the internal start-block tx has been produced yet.
234    start_block_produced: bool,
235    /// Count of user transactions processed.
236    user_txs_processed: usize,
237    /// Expected balance delta from L1 deposits/withdrawals.
238    pub expected_balance_delta: i128,
239    /// The ArbOS version (may be updated after internal tx).
240    arbos_version: u64,
241    /// Block timestamp.
242    pub timestamp: u64,
243    /// Block base fee.
244    pub base_fee: U256,
245}
246
247impl BlockProductionState {
248    /// Create a new block production state.
249    pub fn new(
250        per_block_gas_limit: u64,
251        arbos_version: u64,
252        timestamp: u64,
253        base_fee: U256,
254    ) -> Self {
255        Self {
256            block_gas_left: per_block_gas_limit,
257            redeems: Vec::new(),
258            start_block_produced: false,
259            user_txs_processed: 0,
260            expected_balance_delta: 0,
261            arbos_version,
262            timestamp,
263            base_fee,
264        }
265    }
266
267    /// Get the next transaction action. The block executor calls this in a loop.
268    pub fn next_tx_action<H: SequencingHooks>(&mut self, hooks: &mut H) -> TxAction {
269        if !self.start_block_produced {
270            self.start_block_produced = true;
271            return TxAction::ExecuteStartBlock;
272        }
273
274        // Process queued redeems first (FIFO).
275        if !self.redeems.is_empty() {
276            let redeem = self.redeems.remove(0);
277            return TxAction::ExecuteRedeem(redeem);
278        }
279
280        // Ask the sequencer for the next transaction.
281        match hooks.next_tx_to_sequence() {
282            Some(tx_bytes) => {
283                // If the block has no gas left, skip user txs.
284                if self.block_gas_left < TX_GAS {
285                    hooks.insert_last_tx_error("block gas limit reached".to_string());
286                    return TxAction::Done;
287                }
288                TxAction::ExecuteUserTx(tx_bytes)
289            }
290            None => TxAction::Done,
291        }
292    }
293
294    /// Check whether a user tx can fit in the remaining block gas.
295    ///
296    /// In ArbOS < 50, user txs whose compute gas exceeds block_gas_left
297    /// are rejected (after the first tx). In ArbOS >= 50, per-tx gas limiting
298    /// is handled in the gas charging hook instead.
299    pub fn should_reject_for_block_gas(&self, compute_gas: u64, is_user_tx: bool) -> bool {
300        self.arbos_version < arb_ver::ARBOS_VERSION_50
301            && compute_gas > self.block_gas_left
302            && is_user_tx
303            && self.user_txs_processed > 0
304    }
305
306    /// Compute the poster data gas cost in L2 terms for block-level tracking.
307    pub fn compute_data_gas(poster_cost: U256, base_fee: U256, tx_gas: u64) -> u64 {
308        if base_fee.is_zero() {
309            return 0;
310        }
311
312        let poster_cost_in_l2_gas = poster_cost / base_fee;
313        let data_gas: u64 = poster_cost_in_l2_gas.try_into().unwrap_or(u64::MAX);
314
315        // Cap to tx gas limit.
316        data_gas.min(tx_gas)
317    }
318
319    /// Record the result of executing a transaction.
320    ///
321    /// Returns an error string if the internal start-block tx failed.
322    pub fn record_tx_outcome(
323        &mut self,
324        action: &TxAction,
325        outcome: TxOutcome,
326    ) -> Result<(), String> {
327        match outcome {
328            TxOutcome::Invalid(err) => {
329                // Invalid txs still consume a TX_GAS worth of block gas.
330                match action {
331                    TxAction::ExecuteUserTx(_) => {
332                        self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
333                        self.user_txs_processed += 1;
334                    }
335                    _ => {
336                        self.block_gas_left = self.block_gas_left.saturating_sub(TX_GAS);
337                    }
338                }
339                tracing::debug!(err, "tx invalid, skipped");
340                Ok(())
341            }
342            TxOutcome::Success(result) => {
343                // Internal start-block tx must not fail.
344                if matches!(action, TxAction::ExecuteStartBlock) {
345                    if let Some(ref err) = result.evm_error {
346                        return Err(format!("internal tx failed: {err}"));
347                    }
348                }
349
350                let tx_gas_used = result.gas_used;
351                let data_gas = result.data_gas;
352
353                // Subtract gas burned for scheduled redeems (ArbOS >= 4).
354                if self.arbos_version >= arb_ver::ARBOS_VERSION_3 {
355                    for scheduled in &result.scheduled_txs {
356                        // Each scheduled retryable has gas reserved.
357                        // The gas is embedded in the retryable tx encoding;
358                        // the executor should subtract it from tx_gas_used.
359                        let _ = scheduled; // gas deduction handled by executor
360                    }
361                }
362
363                // Queue any scheduled redeems.
364                self.redeems.extend(result.scheduled_txs);
365
366                // Compute used compute gas for block rate limiting.
367                let compute_used = if tx_gas_used >= data_gas {
368                    let c = tx_gas_used - data_gas;
369                    if c < TX_GAS {
370                        TX_GAS
371                    } else {
372                        c
373                    }
374                } else {
375                    tracing::error!(tx_gas_used, data_gas, "tx used less gas than expected");
376                    TX_GAS
377                };
378
379                self.block_gas_left = self.block_gas_left.saturating_sub(compute_used);
380
381                if matches!(action, TxAction::ExecuteUserTx(_)) {
382                    self.user_txs_processed += 1;
383                }
384
385                Ok(())
386            }
387        }
388    }
389
390    /// Track deposit balance delta for post-block verification.
391    pub fn track_deposit(&mut self, value: U256) {
392        let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
393        self.expected_balance_delta = self.expected_balance_delta.saturating_add(value_i128);
394    }
395
396    /// Track withdrawal balance delta from L2->L1 tx events.
397    pub fn track_withdrawal(&mut self, value: U256) {
398        let value_i128: i128 = value.try_into().unwrap_or(i128::MAX);
399        self.expected_balance_delta = self.expected_balance_delta.saturating_sub(value_i128);
400    }
401
402    /// Update ArbOS version (called after internal tx execution may upgrade).
403    pub fn set_arbos_version(&mut self, version: u64) {
404        self.arbos_version = version;
405    }
406
407    /// Verify the post-block balance delta matches expected deposits/withdrawals.
408    pub fn verify_balance_delta(
409        &self,
410        actual_balance_delta: i128,
411        debug_mode: bool,
412    ) -> Result<(), String> {
413        if actual_balance_delta == self.expected_balance_delta {
414            return Ok(());
415        }
416
417        if actual_balance_delta > self.expected_balance_delta || debug_mode {
418            return Err(format!(
419                "unexpected balance delta {} (expected {})",
420                actual_balance_delta, self.expected_balance_delta,
421            ));
422        }
423
424        // Funds were burnt (not minted), only log an error.
425        tracing::error!(
426            actual = actual_balance_delta,
427            expected = self.expected_balance_delta,
428            "unexpected balance delta (funds burnt)"
429        );
430        Ok(())
431    }
432
433    /// Total user transactions processed.
434    pub fn user_txs_processed(&self) -> usize {
435        self.user_txs_processed
436    }
437
438    /// Current ArbOS version.
439    pub fn arbos_version(&self) -> u64 {
440        self.arbos_version
441    }
442}