arb_node/
producer.rs

1//! Block producer implementation.
2//!
3//! Produces blocks from L1 incoming messages by parsing transactions,
4//! executing them against the current state, and persisting the results.
5
6use std::sync::{
7    Arc,
8    atomic::{AtomicBool, AtomicU64, Ordering},
9};
10
11use alloy_consensus::{
12    Block, BlockBody, BlockHeader, EMPTY_OMMER_ROOT_HASH, Header, TxReceipt, proofs,
13    transaction::{SignerRecoverable, TxHashRef},
14};
15use alloy_eips::eip2718::Decodable2718;
16use alloy_evm::{
17    EvmFactory,
18    block::{BlockExecutor, BlockExecutorFactory},
19};
20use alloy_primitives::{Address, B64, B256, Bytes, U256};
21use alloy_rpc_types_eth::BlockNumberOrTag;
22use arb_evm::config::{ArbEvmConfig, arbos_version_from_mix_hash, l1_block_number_from_mix_hash};
23use arb_primitives::{ArbPrimitives, signed_tx::ArbTransactionSigned, tx_types::ArbInternalTx};
24use arb_rpc::block_producer::{
25    BlockProducer, BlockProducerError, BlockProductionInput, ProducedBlock,
26};
27use arbos::{
28    arbos_types::parse_init_message,
29    header::{ArbHeaderInfo, derive_arb_header_info},
30    internal_tx,
31    parse_l2::{ParsedTransaction, parse_l2_transactions, parsed_tx_to_signed},
32};
33use parking_lot::Mutex;
34use reth_chain_state::{CanonicalInMemoryState, ExecutedBlock, NewCanonicalChain};
35use reth_chainspec::ChainSpec;
36use reth_evm::ConfigureEvm;
37use reth_metrics::{
38    Metrics,
39    metrics::{self, Counter, Gauge, Histogram},
40};
41use reth_primitives_traits::{NodePrimitives, SealedHeader, logs_bloom};
42use reth_provider::{BlockNumReader, BlockReaderIdExt, HeaderProvider, StateProviderFactory};
43use reth_revm::database::StateProviderDatabase;
44use reth_storage_api::{StateProvider, StateProviderBox};
45use reth_trie_common::{HashedPostState, TrieInputSorted};
46use revm::database::{BundleState, StateBuilder};
47use revm_database::states::bundle_state::BundleRetention;
48use tracing::{debug, info, warn};
49
50use crate::genesis;
51
52/// Trait to access the in-memory canonical state from a provider.
53///
54/// `BlockchainProvider` has `canonical_in_memory_state()` as an inherent method
55/// but it's not exposed via any reth trait. This trait bridges that gap so
56/// the block producer can receive the handle generically.
57pub trait InMemoryStateAccess {
58    type Primitives: NodePrimitives;
59    fn canonical_in_memory_state(&self) -> CanonicalInMemoryState<Self::Primitives>;
60}
61
62/// Implement `InMemoryStateAccess` for reth's `BlockchainProvider`.
63impl<N> InMemoryStateAccess for reth_provider::providers::BlockchainProvider<N>
64where
65    N: reth_provider::providers::ProviderNodeTypes,
66{
67    type Primitives = N::Primitives;
68    fn canonical_in_memory_state(&self) -> CanonicalInMemoryState<Self::Primitives> {
69        self.canonical_in_memory_state()
70    }
71}
72
73pub const DEFAULT_FLUSH_INTERVAL: u64 = 128;
74const DEFAULT_MAX_INFLIGHT: usize = 512;
75
76fn max_inflight() -> usize {
77    static MAX: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
78    *MAX.get_or_init(|| {
79        std::env::var("ARB_RETH_MAX_INFLIGHT")
80            .ok()
81            .and_then(|s| s.parse::<usize>().ok())
82            .filter(|n| *n > 0)
83            .unwrap_or(DEFAULT_MAX_INFLIGHT)
84    })
85}
86
87/// Fixed-interval flush scheduler with an EMA of commit latency tracked for
88/// observability. The interval is set at construction and does not change.
89pub struct FlushScheduler {
90    interval: u64,
91    ema_commit_latency_ms: u64,
92}
93
94impl FlushScheduler {
95    pub fn new(interval: u64) -> Self {
96        Self {
97            interval,
98            ema_commit_latency_ms: 0,
99        }
100    }
101
102    pub fn should_flush(&self, since_last: u64) -> bool {
103        since_last >= self.interval
104    }
105
106    pub fn observe(&mut self, commit_latency_ms: u64) {
107        self.ema_commit_latency_ms = (self.ema_commit_latency_ms * 7 + commit_latency_ms * 3) / 10;
108    }
109
110    pub fn current_interval(&self) -> u64 {
111        self.interval
112    }
113}
114
115#[cfg(target_os = "linux")]
116fn read_dirty_pages_mb() -> Option<u64> {
117    let content = std::fs::read_to_string("/proc/meminfo").ok()?;
118    for line in content.lines() {
119        if let Some(rest) = line.strip_prefix("Dirty:") {
120            let kb: u64 = rest.trim().trim_end_matches(" kB").trim().parse().ok()?;
121            return Some(kb / 1024);
122        }
123    }
124    None
125}
126
127#[cfg(not(target_os = "linux"))]
128fn read_dirty_pages_mb() -> Option<u64> {
129    None
130}
131
132/// Prometheus metrics for block production.
133#[derive(Metrics)]
134#[metrics(scope = "arb_block_producer")]
135struct ArbBlockProducerMetrics {
136    /// Number of the latest block produced.
137    head_block: Gauge,
138    /// Total number of blocks produced.
139    blocks_produced_total: Counter,
140    /// Total gas processed across all produced blocks.
141    gas_processed_total: Counter,
142    /// Total transactions included across all produced blocks.
143    transactions_processed_total: Counter,
144    /// Duration of each block flush to disk (save_blocks + commit).
145    flush_commit_duration_seconds: Histogram,
146    /// Seconds the producer stalled on backpressure, per occurrence.
147    backpressure_stall_seconds: Histogram,
148}
149
150/// Block producer using reth's save_blocks(Full) for persistence.
151pub struct ArbBlockProducer<Provider> {
152    provider: Provider,
153    chain_spec: Arc<ChainSpec>,
154    evm_config: ArbEvmConfig,
155    in_memory_state: CanonicalInMemoryState<ArbPrimitives>,
156    head_block_num: AtomicU64,
157    blocks_since_flush: AtomicU64,
158    scheduler: Mutex<FlushScheduler>,
159    accumulated_trie_input: Mutex<Arc<TrieInputSorted>>,
160    flushing_trie_input: Mutex<Option<Arc<TrieInputSorted>>>,
161    pending_flush: AtomicBool,
162    produce_lock: tokio::sync::Mutex<()>,
163    cached_init: Mutex<Option<arbos::arbos_types::ParsedInitMessage>>,
164    /// Finality markers propagated by `nitroexecution_setFinalityData`.
165    finality: Mutex<FinalityMarkers>,
166    /// External shared slot pushed to on every set_finality update so
167    /// the `arb_getValidatedBlock` RPC handler can read it without
168    /// holding a strong reference to the producer.
169    validated_watcher: Mutex<Option<Arc<parking_lot::RwLock<alloy_primitives::B256>>>>,
170    /// Cached coalesced storage overlay for the current in-memory chain.
171    /// Extended in place after each block produced; invalidated on flush
172    /// or rollback so a stale chain view never feeds an SLOAD.
173    cached_overlay: Mutex<Option<CachedOverlay>>,
174    cached_prestate: Mutex<Option<CachedPrestate>>,
175    metrics: ArbBlockProducerMetrics,
176}
177
178#[derive(Debug, Default, Clone)]
179struct FinalityMarkers {
180    safe: Option<alloy_primitives::B256>,
181    finalized: Option<alloy_primitives::B256>,
182    validated: Option<alloy_primitives::B256>,
183}
184
185struct CachedOverlay {
186    parent_hash: B256,
187    overlay: Arc<crate::coalesced_state::CoalescedOverlay>,
188}
189
190struct CachedPrestate {
191    parent_hash: B256,
192    contracts: Arc<alloy_primitives::map::HashMap<B256, revm::bytecode::Bytecode>>,
193}
194
195impl<Provider> ArbBlockProducer<Provider>
196where
197    Provider: BlockNumReader,
198{
199    pub fn new(
200        provider: Provider,
201        chain_spec: Arc<ChainSpec>,
202        evm_config: ArbEvmConfig,
203        in_memory_state: CanonicalInMemoryState<ArbPrimitives>,
204        flush_interval: u64,
205    ) -> Self {
206        let head = provider.last_block_number().unwrap_or(0);
207        Self {
208            provider,
209            chain_spec,
210            evm_config,
211            in_memory_state,
212            head_block_num: AtomicU64::new(head),
213            blocks_since_flush: AtomicU64::new(0),
214            scheduler: Mutex::new(FlushScheduler::new(flush_interval)),
215            accumulated_trie_input: Mutex::new(Arc::new(TrieInputSorted::default())),
216            flushing_trie_input: Mutex::new(None),
217            pending_flush: AtomicBool::new(false),
218            produce_lock: tokio::sync::Mutex::new(()),
219            cached_init: Mutex::new(None),
220            finality: Mutex::new(FinalityMarkers::default()),
221            validated_watcher: Mutex::new(None),
222            cached_overlay: Mutex::new(None),
223            cached_prestate: Mutex::new(None),
224            metrics: ArbBlockProducerMetrics::default(),
225        }
226    }
227
228    fn get_or_build_overlay(
229        &self,
230        parent_hash: B256,
231        head_state: &reth_chain_state::BlockState<ArbPrimitives>,
232    ) -> Arc<crate::coalesced_state::CoalescedOverlay> {
233        let mut cache = self.cached_overlay.lock();
234        if let Some(c) = cache.as_ref()
235            && c.parent_hash == parent_hash
236        {
237            return c.overlay.clone();
238        }
239        let overlay = Arc::new(crate::coalesced_state::CoalescedOverlay::from_chain(
240            head_state,
241        ));
242        *cache = Some(CachedOverlay {
243            parent_hash,
244            overlay: overlay.clone(),
245        });
246        overlay
247    }
248
249    fn extend_cached_overlay(&self, new_block_hash: B256, bundle: &BundleState) {
250        let mut cache = self.cached_overlay.lock();
251        let mut overlay = match cache.take() {
252            Some(c) => match Arc::try_unwrap(c.overlay) {
253                Ok(o) => o,
254                Err(arc) => (*arc).clone(),
255            },
256            None => crate::coalesced_state::CoalescedOverlay::default(),
257        };
258        overlay.extend_with_block(bundle);
259        *cache = Some(CachedOverlay {
260            parent_hash: new_block_hash,
261            overlay: Arc::new(overlay),
262        });
263    }
264
265    fn invalidate_cached_overlay(&self) {
266        *self.cached_overlay.lock() = None;
267    }
268
269    fn get_or_build_prestate(
270        &self,
271        parent_hash: B256,
272        head_state: Option<&reth_chain_state::BlockState<ArbPrimitives>>,
273    ) -> Arc<alloy_primitives::map::HashMap<B256, revm::bytecode::Bytecode>> {
274        let mut cache = self.cached_prestate.lock();
275        if let Some(c) = cache.as_ref()
276            && c.parent_hash == parent_hash
277        {
278            return c.contracts.clone();
279        }
280        let mut contracts: alloy_primitives::map::HashMap<B256, revm::bytecode::Bytecode> =
281            Default::default();
282        if let Some(head_state) = head_state {
283            for block_state in head_state.chain() {
284                let exec_output = &block_state.block().execution_output;
285                for (hash, code) in &exec_output.state.contracts {
286                    contracts.entry(*hash).or_insert_with(|| code.clone());
287                }
288            }
289        }
290        let arc = Arc::new(contracts);
291        *cache = Some(CachedPrestate {
292            parent_hash,
293            contracts: arc.clone(),
294        });
295        arc
296    }
297
298    fn extend_cached_prestate(&self, new_block_hash: B256, bundle: &BundleState) {
299        let mut cache = self.cached_prestate.lock();
300        let mut contracts = match cache.take() {
301            Some(c) => match Arc::try_unwrap(c.contracts) {
302                Ok(map) => map,
303                Err(arc) => (*arc).clone(),
304            },
305            None => Default::default(),
306        };
307        for (hash, code) in &bundle.contracts {
308            contracts.entry(*hash).or_insert_with(|| code.clone());
309        }
310        *cache = Some(CachedPrestate {
311            parent_hash: new_block_hash,
312            contracts: Arc::new(contracts),
313        });
314    }
315
316    fn invalidate_cached_prestate(&self) {
317        *self.cached_prestate.lock() = None;
318    }
319
320    /// Currently-tracked finality markers (for RPC / debugging use).
321    pub fn finality_markers(
322        &self,
323    ) -> (
324        Option<alloy_primitives::B256>,
325        Option<alloy_primitives::B256>,
326        Option<alloy_primitives::B256>,
327    ) {
328        let f = self.finality.lock();
329        (f.safe, f.finalized, f.validated)
330    }
331}
332
333impl<Provider> ArbBlockProducer<Provider>
334where
335    Provider: BlockNumReader
336        + BlockReaderIdExt
337        + HeaderProvider<Header = Header>
338        + StateProviderFactory
339        + Send
340        + Sync
341        + 'static,
342{
343    /// Get the current head block number (includes in-memory buffered blocks).
344    fn head_block_number(&self) -> Result<u64, BlockProducerError> {
345        let head = self.head_block_num.load(Ordering::SeqCst);
346        if head > 0 {
347            Ok(head)
348        } else {
349            self.provider
350                .last_block_number()
351                .map_err(|e| BlockProducerError::StateAccess(e.to_string()))
352        }
353    }
354
355    /// Get the parent sealed header for block production.
356    fn parent_header(&self, head_num: u64) -> Result<SealedHeader<Header>, BlockProducerError> {
357        self.provider
358            .sealed_header_by_number_or_tag(BlockNumberOrTag::Number(head_num))
359            .map_err(|e| BlockProducerError::StateAccess(e.to_string()))?
360            .ok_or_else(|| {
361                BlockProducerError::StateAccess(format!("Parent block {head_num} not found"))
362            })
363    }
364
365    fn drain_completed_flush(&self) -> bool {
366        if !self.pending_flush.load(Ordering::SeqCst) {
367            return false;
368        }
369        let Some(result) = crate::launcher::try_flush_result() else {
370            return false;
371        };
372        self.in_memory_state
373            .remove_persisted_blocks(result.last_num_hash);
374        *self.flushing_trie_input.lock() = None;
375        self.pending_flush.store(false, Ordering::SeqCst);
376        self.invalidate_cached_overlay();
377        self.invalidate_cached_prestate();
378        let commit_latency_ms = result.duration.as_millis() as u64;
379        self.metrics
380            .flush_commit_duration_seconds
381            .record(result.duration.as_secs_f64());
382        let flush_interval_current = {
383            let mut sched = self.scheduler.lock();
384            sched.observe(commit_latency_ms);
385            sched.current_interval()
386        };
387        let dirty_pages_mb = read_dirty_pages_mb().unwrap_or(0);
388        let chain_len_unflushed = self
389            .in_memory_state
390            .head_state()
391            .map(|s| s.chain().count())
392            .unwrap_or(0) as u64;
393        info!(
394            target: "block_producer",
395            flushed = result.count,
396            last_block = result.last_num_hash.number,
397            mdbx_commit_latency_ms = commit_latency_ms,
398            dirty_pages_mb,
399            flush_interval_current,
400            chain_len_unflushed,
401            "block flush"
402        );
403        true
404    }
405
406    async fn apply_backpressure(&self) {
407        let chain_len = self
408            .in_memory_state
409            .head_state()
410            .map(|s| s.chain().count())
411            .unwrap_or(0);
412        let limit = max_inflight();
413        if chain_len <= limit {
414            return;
415        }
416        if !self.pending_flush.load(Ordering::SeqCst) {
417            self.start_async_flush();
418        }
419        let start = std::time::Instant::now();
420        let notifier = crate::launcher::flush_notifier();
421        loop {
422            if let Some(n) = notifier.as_ref() {
423                // Register interest before checking, so notifications fired
424                // between the check and the await are not missed.
425                let notified = n.notified();
426                if self.drain_completed_flush() {
427                    break;
428                }
429                let waited = tokio::time::timeout(std::time::Duration::from_secs(30), notified)
430                    .await
431                    .is_ok();
432                if !waited {
433                    warn!(
434                        target: "block_producer",
435                        chain_len,
436                        waited_ms = start.elapsed().as_millis() as u64,
437                        "Backpressure: flush notification timed out, polling once"
438                    );
439                }
440            } else {
441                if self.drain_completed_flush() {
442                    break;
443                }
444                tokio::time::sleep(std::time::Duration::from_millis(20)).await;
445            }
446        }
447        self.metrics
448            .backpressure_stall_seconds
449            .record(start.elapsed().as_secs_f64());
450        warn!(
451            target: "block_producer",
452            chain_len,
453            limit,
454            waited_ms = start.elapsed().as_millis() as u64,
455            "Backpressure: drained pending flush"
456        );
457    }
458
459    fn produce_block_with_execution(
460        &self,
461        input: &BlockProductionInput,
462        parsed_txs: Vec<ParsedTransaction>,
463    ) -> Result<ProducedBlock, BlockProducerError> {
464        self.drain_completed_flush();
465
466        let head_num = self.head_block_number()?;
467        let l2_block_number = head_num + 1;
468        let parent_header = self.parent_header(head_num)?;
469
470        let timestamp = input.l1_timestamp.max(parent_header.timestamp());
471        let time_passed = timestamp.saturating_sub(parent_header.timestamp());
472
473        let parent_mix_hash = parent_header.mix_hash().unwrap_or_default();
474        let parent_arbos_version = arbos_version_from_mix_hash(&parent_mix_hash);
475
476        // The StartBlock tx carries the reported value verbatim; the EVM sees
477        // the monotonic one.
478        let l1_block_number = input.l1_block_number;
479        let block_l1_block_number = monotonic_l1_block_number(l1_block_number, &parent_mix_hash);
480        let arbos_version = parent_arbos_version; // May upgrade during StartBlock
481
482        // Construct a provisional mix_hash for the EVM environment.
483        let send_count = {
484            let mut buf = [0u8; 8];
485            buf.copy_from_slice(&parent_mix_hash.0[0..8]);
486            u64::from_be_bytes(buf)
487        };
488        let provisional_mix_hash =
489            compute_mix_hash(send_count, block_l1_block_number, arbos_version);
490
491        // Open state at parent block via block hash.
492        let raw_state_provider = self
493            .provider
494            .state_by_block_hash(parent_header.hash())
495            .map_err(|e| BlockProducerError::StateAccess(e.to_string()))?;
496
497        let state_provider: StateProviderBox = match self
498            .in_memory_state
499            .state_by_hash(parent_header.hash())
500        {
501            Some(head_state) => {
502                let overlay = self.get_or_build_overlay(parent_header.hash(), &head_state);
503                if overlay.is_empty() {
504                    raw_state_provider
505                } else {
506                    crate::coalesced_state::CoalescedStateProvider::new(raw_state_provider, overlay)
507                        .boxed()
508                }
509            }
510            _ => raw_state_provider,
511        };
512
513        // Read the L2 baseFee from the parent's committed state.
514        let l2_base_fee = {
515            let read_slot = |addr: Address, slot: B256| state_provider.storage(addr, slot);
516            arbos::header::read_l2_base_fee(&read_slot)
517                .map_err(|e| BlockProducerError::Storage(e.to_string()))?
518                .or(parent_header.base_fee_per_gas())
519        };
520
521        // Build a provisional header for the EVM config.
522        let provisional_header = Header {
523            parent_hash: parent_header.hash(),
524            ommers_hash: EMPTY_OMMER_ROOT_HASH,
525            beneficiary: input.sender,
526            state_root: B256::ZERO, // placeholder
527            transactions_root: B256::ZERO,
528            receipts_root: B256::ZERO,
529            withdrawals_root: None,
530            logs_bloom: Default::default(),
531            timestamp,
532            mix_hash: provisional_mix_hash,
533            nonce: B64::from(input.delayed_messages_read.to_be_bytes()),
534            base_fee_per_gas: l2_base_fee,
535            number: l2_block_number,
536            gas_limit: parent_header.gas_limit(),
537            difficulty: U256::from(1),
538            gas_used: 0,
539            extra_data: Default::default(),
540            parent_beacon_block_root: None,
541            blob_gas_used: None,
542            excess_blob_gas: None,
543            requests_hash: None,
544        };
545
546        let evm_env = self
547            .evm_config
548            .evm_env(&provisional_header)
549            .map_err(|_| BlockProducerError::Execution("evm_env construction failed".into()))?;
550
551        // Collect bytecodes from in-memory blocks that might not be flushed to DB yet.
552        // When a Stylus contract is deployed in a recent block and the flush hasn't
553        // persisted it yet, the DB's Bytecodes table won't have the code. The
554        // State<DB>'s `code_by_hash` with `use_preloaded_bundle` will check the
555        // bundle_state.contracts before falling back to the DB, ensuring all
556        // bytecodes from recent blocks are available during execution.
557        let prestate = {
558            let head_state_opt = self.in_memory_state.state_by_hash(parent_header.hash());
559            let contracts =
560                self.get_or_build_prestate(parent_header.hash(), head_state_opt.as_deref());
561            BundleState {
562                contracts: (*contracts).clone(),
563                ..Default::default()
564            }
565        };
566
567        let mut db = StateBuilder::new()
568            .with_database(StateProviderDatabase::new(state_provider.as_ref()))
569            .with_bundle_prestate(prestate)
570            .with_bundle_update()
571            .without_state_clear()
572            .build();
573
574        let chain_id = self.chain_spec.chain().id();
575
576        // Apply cached ArbOS Init during block 1.
577        // Two cases:
578        //   - ArbOS not yet initialized (no chainspec alloc): full init from message.
579        //   - ArbOS already initialized (chainspec did it with placeholder L1 base fee): override
580        //     the L1 price_per_unit slot with the value from the init message, since chainspec has
581        //     no way to know the real value.
582        if let Some(init_msg) = self.cached_init.lock().take() {
583            if !genesis::is_arbos_initialized(&mut db) {
584                // Honor the genesis-declared ArbOS version from the parent
585                // header's mix_hash so chain specs that target a higher
586                // initial version (e.g. v30 / v50 spec fixtures) get the
587                // matching hardfork-equivalent EVM activation rather than
588                // booting at the v10 default.
589                let initial_version = std::env::var("ARB_INITIAL_ARBOS_VERSION")
590                    .ok()
591                    .and_then(|v| v.parse::<u64>().ok())
592                    .unwrap_or({
593                        if parent_arbos_version > 0 {
594                            parent_arbos_version
595                        } else {
596                            genesis::INITIAL_ARBOS_VERSION
597                        }
598                    });
599                info!(
600                    target: "block_producer",
601                    initial_version,
602                    "Applying cached ArbOS Init during block {} execution",
603                    l2_block_number
604                );
605                genesis::initialize_arbos_state(
606                    &mut db,
607                    &init_msg,
608                    chain_id,
609                    initial_version,
610                    genesis::DEFAULT_CHAIN_OWNER,
611                    genesis::ArbOSInit::default(),
612                )
613                .map_err(|e| BlockProducerError::Execution(e.to_string()))?;
614            } else {
615                use arbos::{arbos_state::ArbosState, burn::SystemBurner};
616                info!(
617                    target: "block_producer",
618                    initial_l1_base_fee = %init_msg.initial_l1_base_fee,
619                    "ArbOS already initialized; overriding L1 price_per_unit from Init message"
620                );
621                // SAFETY: `state_ptr` points at the local `db` owned by
622                // this scope; reads through it are sequential and the
623                // `&mut *state_ptr` re-borrows are dropped at each call
624                // site before the next one, so the type-level aliasing
625                // does not overlap at runtime.
626                let state_ptr: *mut _ = &mut db;
627                let mut arb_state =
628                    ArbosState::open(unsafe { &mut *state_ptr }, SystemBurner::new(None, false))
629                        .map_err(|e| BlockProducerError::Execution(e.to_string()))?;
630                let _ = arb_state
631                    .l1_pricing_state
632                    .set_price_per_unit(unsafe { &mut *state_ptr }, init_msg.initial_l1_base_fee);
633                if let Ok(target) = std::env::var("ARB_INITIAL_ARBOS_VERSION")
634                    && let Ok(target_version) = target.parse::<u64>()
635                {
636                    let current = arb_state.arbos_version();
637                    if target_version > current {
638                        match arb_state.upgrade_arbos_version(
639                            unsafe { &mut *state_ptr },
640                            target_version,
641                            true,
642                        ) {
643                            Err(e) => {
644                                info!(target: "block_producer", err = ?e, target_version, "ArbOS upgrade via env var failed");
645                            }
646                            _ => {
647                                info!(
648                                    target: "block_producer",
649                                    from = current,
650                                    to = target_version,
651                                    "ArbOS upgraded via ARB_INITIAL_ARBOS_VERSION"
652                                );
653                            }
654                        }
655                    }
656                }
657            }
658        }
659
660        let parent_extra = parent_header.extra_data().to_vec();
661        let mut exec_extra = parent_extra.clone();
662        exec_extra.resize(32, 0);
663        exec_extra.extend_from_slice(&input.delayed_messages_read.to_be_bytes());
664
665        let exec_ctx = alloy_evm::eth::EthBlockExecutionCtx {
666            tx_count_hint: Some(parsed_txs.len() + 2), // +2 for internal txs
667            parent_hash: parent_header.hash(),
668            parent_beacon_block_root: None,
669            ommers: &[],
670            withdrawals: None,
671            extra_data: exec_extra.into(),
672        };
673
674        // Create the block executor via the factory. A multi-gas inspector is
675        // installed so the v60 multi-dimensional pricing backlog is driven by
676        // per-opcode resource attribution; it publishes each tx's multi-gas to
677        // the shared sink the executor reads.
678        let multi_gas_sink = arb_evm::multi_gas::MultiGasSink::default();
679        let evm = self
680            .evm_config
681            .block_executor_factory()
682            .evm_factory()
683            .create_evm_with_inspector(
684                &mut db,
685                evm_env.clone(),
686                arb_evm::multi_gas::MultiGasInspector::with_sink(multi_gas_sink.clone()),
687            );
688        let mut executor = self
689            .evm_config
690            .block_executor_factory()
691            .create_arb_executor(evm, exec_ctx, chain_id);
692        executor.set_multi_gas_sink(multi_gas_sink);
693        executor.arb_ctx.l2_block_number = l2_block_number;
694        executor.arb_ctx.l1_block_number = block_l1_block_number;
695
696        // 256-ancestor populate only fires on a cold cache.
697        let l2_hash_entries = {
698            let mut entries = Vec::new();
699            let parent_num = l2_block_number.saturating_sub(1);
700            entries.push((parent_num, parent_header.hash()));
701            let cache_cold = parent_num > 1
702                && self
703                    .evm_config
704                    .executor_factory
705                    .arb_evm_factory()
706                    .chain_caches()
707                    .l2_block_hashes
708                    .lock()
709                    .get(&parent_num.saturating_sub(1))
710                    .is_none();
711            if cache_cold {
712                let mut hash = parent_header.parent_hash();
713                for i in 2..=256u64 {
714                    let Some(n) = l2_block_number.checked_sub(i) else {
715                        break;
716                    };
717                    entries.push((n, hash));
718                    match self
719                        .provider
720                        .sealed_header_by_number_or_tag(BlockNumberOrTag::Number(n))
721                    {
722                        Ok(Some(h)) => hash = h.parent_hash(),
723                        _ => break,
724                    }
725                }
726            }
727            entries
728        };
729
730        // Apply pre-execution changes (loads ArbOS state, fee accounts, block hashes).
731        executor
732            .apply_pre_execution_changes()
733            .map_err(|e| BlockProducerError::Execution(format!("pre-exec: {e}")))?;
734
735        for (l2_num, hash) in l2_hash_entries {
736            executor
737                .precompile_ctx
738                .block
739                .cache_l2_block_hash(l2_num, hash);
740        }
741
742        let mut all_txs: Vec<ArbTransactionSigned> = Vec::new();
743
744        // 1. Generate and execute the StartBlock internal tx (always first).
745        let l1_base_fee = input.l1_base_fee.unwrap_or(U256::ZERO);
746        let start_block_data = internal_tx::encode_start_block(
747            l1_base_fee,
748            l1_block_number,
749            l2_block_number,
750            time_passed,
751        );
752
753        let start_block_tx = create_internal_tx(chain_id, &start_block_data);
754        execute_and_commit_tx(&mut executor, &start_block_tx, "StartBlock")?;
755        all_txs.push(start_block_tx);
756
757        // Warm sender caches in parallel; kinds with an embedded `from` are skipped.
758        let pre_recovered: Vec<Option<ArbTransactionSigned>> = {
759            use rayon::prelude::*;
760            parsed_txs
761                .par_iter()
762                .map(|parsed| match parsed {
763                    ParsedTransaction::InternalStartBlock { .. }
764                    | ParsedTransaction::BatchPostingReport { .. } => None,
765                    other => {
766                        let signed = parsed_tx_to_signed(other, chain_id)?;
767                        let _ = signed.recover_signer();
768                        Some(signed)
769                    }
770                })
771                .collect()
772        };
773
774        // 2. Execute parsed user transactions.
775        for (idx, parsed) in parsed_txs.iter().enumerate() {
776            match parsed {
777                ParsedTransaction::InternalStartBlock { .. } => {
778                    // StartBlock is handled above, skip.
779                    continue;
780                }
781                ParsedTransaction::BatchPostingReport {
782                    batch_timestamp,
783                    batch_poster,
784                    batch_number,
785                    l1_base_fee_estimate,
786                    extra_gas,
787                    ..
788                } => {
789                    // Delayed message kind=13 contains a batch posting report.
790                    // Encode as V1 or V2 based on parent ArbOS version.
791                    let report_data =
792                        if parent_arbos_version >= arb_chainspec::arbos_version::ARBOS_VERSION_50 {
793                            // V2: pass raw batch data stats + extra_gas.
794                            let (length, non_zeros) = input.batch_data_stats.unwrap_or((0, 0));
795                            internal_tx::encode_batch_posting_report_v2(
796                                *batch_timestamp,
797                                *batch_poster,
798                                *batch_number,
799                                length,
800                                non_zeros,
801                                *extra_gas,
802                                *l1_base_fee_estimate,
803                            )
804                        } else {
805                            // V1: combine legacy gas cost + extra_gas into single field.
806                            let legacy_gas = input.batch_gas_cost.unwrap_or(0);
807                            let batch_data_gas = legacy_gas.saturating_add(*extra_gas);
808                            internal_tx::encode_batch_posting_report(
809                                *batch_timestamp,
810                                *batch_poster,
811                                *batch_number,
812                                batch_data_gas,
813                                *l1_base_fee_estimate,
814                            )
815                        };
816                    let report_tx = create_internal_tx(chain_id, &report_data);
817                    execute_and_commit_tx(&mut executor, &report_tx, "BatchPostingReport")?;
818                    all_txs.push(report_tx);
819                    continue;
820                }
821                _ => {}
822            }
823
824            let signed_tx = match pre_recovered.get(idx).and_then(|s| s.clone()) {
825                Some(tx) => tx,
826                None => {
827                    debug!(target: "block_producer", ?parsed, "Skipping unparseable transaction");
828                    continue;
829                }
830            };
831
832            let recovered = match signed_tx.clone().try_into_recovered() {
833                Ok(r) => r,
834                Err(e) => {
835                    warn!(target: "block_producer", error = %e, "Failed to recover tx sender, skipping");
836                    continue;
837                }
838            };
839            let tx_hash = *signed_tx.tx_hash();
840            let (exec_outcome, hostio_records) = arb_rpc::stylus_tracer::with_trace_buffer(|| {
841                executor.execute_transaction_without_commit(recovered)
842            });
843            match exec_outcome {
844                Ok(result) => {
845                    match executor.commit_transaction(result) {
846                        Ok(_gas_used) => {
847                            all_txs.push(signed_tx);
848                            if !hostio_records.is_empty() {
849                                arb_rpc::stylus_tracer::cache_trace(tx_hash, hostio_records);
850                            }
851
852                            // Drain and execute any scheduled txs (auto-redeems).
853                            // After a SubmitRetryable or manual Redeem precompile call,
854                            // the executor queues retry txs that must execute in the
855                            // same block, immediately after the triggering tx.
856                            loop {
857                                let scheduled = executor.drain_scheduled_txs();
858                                debug!(
859                                    target: "block_producer",
860                                    count = scheduled.len(),
861                                    "Drained scheduled txs"
862                                );
863                                if scheduled.is_empty() {
864                                    break;
865                                }
866                                for encoded in scheduled {
867                                    let retry_tx: Option<ArbTransactionSigned> =
868                                        ArbTransactionSigned::decode_2718(&mut &encoded[..]).ok();
869                                    if let Some(retry_tx) = retry_tx {
870                                        let retry_signed = retry_tx.clone();
871                                        let retry_hash = *retry_signed.tx_hash();
872                                        match retry_tx.try_into_recovered() {
873                                            Ok(recovered_retry) => {
874                                                let (retry_outcome, retry_records) =
875                                                    arb_rpc::stylus_tracer::with_trace_buffer(
876                                                        || {
877                                                            executor
878                                                                .execute_transaction_without_commit(
879                                                                    recovered_retry,
880                                                                )
881                                                        },
882                                                    );
883                                                match retry_outcome {
884                                                    Ok(retry_result) => {
885                                                        match executor
886                                                            .commit_transaction(retry_result)
887                                                        {
888                                                            Ok(_) => {
889                                                                all_txs.push(retry_signed);
890                                                                if !retry_records.is_empty() {
891                                                                    arb_rpc::stylus_tracer::cache_trace(
892                                                                        retry_hash,
893                                                                        retry_records,
894                                                                    );
895                                                                }
896                                                            }
897                                                            Err(e) => {
898                                                                warn!(
899                                                                    target: "block_producer",
900                                                                    error = %e,
901                                                                    "Failed to commit auto-redeem tx"
902                                                                );
903                                                            }
904                                                        }
905                                                    }
906                                                    Err(e) => {
907                                                        warn!(
908                                                            target: "block_producer",
909                                                            error = %e,
910                                                            "Auto-redeem tx execution failed"
911                                                        );
912                                                    }
913                                                }
914                                            }
915                                            Err(e) => {
916                                                warn!(
917                                                    target: "block_producer",
918                                                    error = %e,
919                                                    "Failed to recover auto-redeem tx sender"
920                                                );
921                                            }
922                                        }
923                                    }
924                                }
925                            }
926                        }
927                        Err(e) => {
928                            warn!(target: "block_producer", error = %e, "Failed to commit transaction");
929                        }
930                    }
931                }
932                Err(ref e) if e.to_string().contains("block gas limit reached") => {
933                    break;
934                }
935                Err(e) => {
936                    warn!(target: "block_producer", error = %e, "Transaction execution failed, skipping");
937                }
938            }
939        }
940
941        let zombie_accounts = executor.zombie_accounts().clone();
942        let finalise_deleted = executor.finalise_deleted().clone();
943
944        let (_, exec_result) = executor
945            .finish()
946            .map_err(|e| BlockProducerError::Execution(format!("finish: {e}")))?;
947
948        let receipts: Vec<arb_primitives::ArbReceipt> = exec_result.receipts;
949
950        db.merge_transitions(BundleRetention::Reverts);
951        let mut bundle = db.take_bundle();
952
953        augment_bundle_from_cache(&mut bundle, &db.cache, &*state_provider)?;
954
955        // Mark per-tx finalise deletions, skipping zombie accounts.
956        let keccak_empty_hash = alloy_primitives::B256::from(alloy_primitives::keccak256([]));
957        for addr in &finalise_deleted {
958            if zombie_accounts.contains(addr) {
959                continue;
960            }
961            if bundle.state.contains_key(addr) {
962                let existed_before = state_provider.basic_account(addr).ok().flatten().is_some();
963                if existed_before {
964                    // Account was in the trie. Only mark as deleted if it's
965                    // still empty — it may have been re-created with non-zero
966                    // state (e.g., nonce=1) by a later tx in this block.
967                    let still_empty = bundle
968                        .state
969                        .get(addr)
970                        .and_then(|a| a.info.as_ref())
971                        .is_none_or(|info| {
972                            info.nonce == 0
973                                && info.balance.is_zero()
974                                && info.code_hash == keccak_empty_hash
975                        });
976                    if still_empty && let Some(bundle_acct) = bundle.state.get_mut(addr) {
977                        bundle_acct.info = None;
978                    }
979                } else {
980                    let still_empty = bundle
981                        .state
982                        .get(addr)
983                        .and_then(|a| a.info.as_ref())
984                        .is_none_or(|info| {
985                            info.nonce == 0
986                                && info.balance.is_zero()
987                                && info.code_hash == keccak_empty_hash
988                        });
989                    if still_empty {
990                        bundle.state.remove(addr);
991                    }
992                }
993                continue;
994            }
995            if let Ok(Some(acct)) = state_provider.basic_account(addr) {
996                let was_originally_empty = acct.balance.is_zero()
997                    && acct.nonce == 0
998                    && acct.bytecode_hash.is_none_or(|h| h == keccak_empty_hash);
999                if was_originally_empty {
1000                    continue;
1001                }
1002                bundle.state.insert(
1003                    *addr,
1004                    revm_database::BundleAccount {
1005                        info: None, // signals trie deletion
1006                        original_info: None,
1007                        storage: Default::default(),
1008                        status: revm_database::AccountStatus::Changed,
1009                    },
1010                );
1011            }
1012        }
1013
1014        filter_unchanged_storage(&mut bundle);
1015        delete_empty_accounts(&mut bundle, &zombie_accounts, &*state_provider);
1016
1017        let hashed_state =
1018            HashedPostState::from_bundle_state::<reth_trie_common::KeccakKeyHasher>(bundle.state());
1019
1020        let (state_root, trie_updates) = {
1021            let acc_arc = self.accumulated_trie_input.lock().clone();
1022            let flushing_arc = self.flushing_trie_input.lock().clone();
1023
1024            let block_state_sorted = hashed_state.clone().into_sorted();
1025            let prefix_sets = block_state_sorted.construct_prefix_sets().freeze();
1026
1027            let mut new_acc_state = (*acc_arc.state).clone();
1028            new_acc_state.extend_ref_and_sort(&block_state_sorted);
1029            let new_acc_state_arc = Arc::new(new_acc_state);
1030
1031            let (overlay_state_arc, overlay_nodes_arc) = if let Some(f) = &flushing_arc {
1032                let mut s = (*f.state).clone();
1033                s.extend_ref_and_sort(&new_acc_state_arc);
1034                let mut n = (*f.nodes).clone();
1035                n.extend_ref_and_sort(&acc_arc.nodes);
1036                (Arc::new(s), Arc::new(n))
1037            } else {
1038                (Arc::clone(&new_acc_state_arc), Arc::clone(&acc_arc.nodes))
1039            };
1040
1041            let overlay = Arc::new(TrieInputSorted::new(
1042                overlay_nodes_arc,
1043                overlay_state_arc,
1044                Default::default(),
1045            ));
1046
1047            let (root, updates) =
1048                crate::launcher::compute_parallel_state_root(overlay, prefix_sets)
1049                    .map_err(|e| BlockProducerError::Execution(format!("state root: {e}")))?;
1050
1051            let mut new_acc_nodes = (*acc_arc.nodes).clone();
1052            new_acc_nodes.extend_ref_and_sort(&updates.clone_into_sorted());
1053            *self.accumulated_trie_input.lock() = Arc::new(TrieInputSorted::new(
1054                Arc::new(new_acc_nodes),
1055                new_acc_state_arc,
1056                Default::default(),
1057            ));
1058
1059            (root, updates)
1060        };
1061
1062        // Derive header info (send_root, send_count, etc.) from post-execution state.
1063        let arb_info =
1064            derive_header_info_from_state(state_provider.as_ref(), &bundle, input.sender)?;
1065
1066        let final_mix_hash = arb_info
1067            .as_ref()
1068            .map(|info| info.compute_mix_hash())
1069            .unwrap_or(provisional_mix_hash);
1070
1071        let extra_data: Bytes = arb_info
1072            .as_ref()
1073            .map(|info| {
1074                let mut data = info.send_root.to_vec();
1075                data.resize(32, 0);
1076                data.into()
1077            })
1078            .unwrap_or_else(|| {
1079                let mut data = parent_extra.clone();
1080                data.resize(32, 0);
1081                data.into()
1082            });
1083
1084        let send_root = arb_info
1085            .as_ref()
1086            .map(|info| info.send_root)
1087            .unwrap_or_else(|| {
1088                if parent_extra.len() >= 32 {
1089                    B256::from_slice(&parent_extra[..32])
1090                } else {
1091                    B256::ZERO
1092                }
1093            });
1094
1095        // Compute receipt-derived fields.
1096        let gas_used = exec_result.gas_used;
1097        let logs_bloom_val = logs_bloom(receipts.iter().flat_map(|r| r.logs()));
1098
1099        let transactions_root =
1100            proofs::calculate_transaction_root::<ArbTransactionSigned>(&all_txs);
1101        let receipts_root = proofs::calculate_receipt_root(
1102            &receipts
1103                .iter()
1104                .map(|r| r.with_bloom_ref())
1105                .collect::<Vec<_>>(),
1106        );
1107
1108        let header = Header {
1109            parent_hash: parent_header.hash(),
1110            ommers_hash: EMPTY_OMMER_ROOT_HASH,
1111            beneficiary: input.sender,
1112            state_root,
1113            transactions_root,
1114            receipts_root,
1115            withdrawals_root: None,
1116            logs_bloom: logs_bloom_val,
1117            timestamp,
1118            mix_hash: final_mix_hash,
1119            nonce: B64::from(input.delayed_messages_read.to_be_bytes()),
1120            base_fee_per_gas: l2_base_fee,
1121            number: l2_block_number,
1122            gas_limit: parent_header.gas_limit(),
1123            difficulty: U256::from(1),
1124            gas_used,
1125            extra_data,
1126            parent_beacon_block_root: None,
1127            blob_gas_used: None,
1128            excess_blob_gas: None,
1129            requests_hash: None,
1130        };
1131
1132        let block = Block::<ArbTransactionSigned> {
1133            header,
1134            body: BlockBody {
1135                transactions: all_txs,
1136                ommers: Default::default(),
1137                withdrawals: None,
1138            },
1139        };
1140
1141        let sealed = reth_primitives_traits::SealedBlock::seal_slow(block);
1142        let block_hash = sealed.hash();
1143
1144        self.extend_cached_overlay(block_hash, &bundle);
1145        self.extend_cached_prestate(block_hash, &bundle);
1146
1147        // Buffer block in memory for batched persistence.
1148        {
1149            use alloy_evm::block::BlockExecutionResult;
1150            use reth_chain_state::ComputedTrieData;
1151            use reth_execution_types::BlockExecutionOutput;
1152            use reth_primitives_traits::RecoveredBlock;
1153
1154            let recovered = Arc::new(RecoveredBlock::new_sealed(sealed.clone(), vec![]));
1155            let exec_output = Arc::new(BlockExecutionOutput {
1156                state: bundle,
1157                result: BlockExecutionResult {
1158                    receipts,
1159                    requests: Default::default(),
1160                    gas_used,
1161                    blob_gas_used: 0,
1162                },
1163            });
1164            let computed = ComputedTrieData {
1165                hashed_state: Arc::new(hashed_state.into_sorted()),
1166                trie_updates: Arc::new(trie_updates.into_sorted()),
1167                anchored_trie_input: None,
1168            };
1169            let executed = ExecutedBlock::new(recovered, exec_output, computed);
1170
1171            self.in_memory_state
1172                .update_chain(NewCanonicalChain::Commit {
1173                    new: vec![executed],
1174                });
1175
1176            let sealed_header = SealedHeader::new(sealed.header().clone(), sealed.hash());
1177            self.in_memory_state.set_canonical_head(sealed_header);
1178        }
1179
1180        self.head_block_num.store(l2_block_number, Ordering::SeqCst);
1181
1182        let num_txs = sealed.body().transactions.len();
1183        // Update block producer metrics.
1184        {
1185            self.metrics.head_block.set(l2_block_number as f64);
1186            self.metrics.blocks_produced_total.increment(1);
1187            self.metrics.gas_processed_total.increment(gas_used);
1188            self.metrics
1189                .transactions_processed_total
1190                .increment(num_txs as u64);
1191        }
1192
1193        let since_flush = self.blocks_since_flush.fetch_add(1, Ordering::SeqCst) + 1;
1194        let should_flush = self.scheduler.lock().should_flush(since_flush);
1195        if should_flush && !self.pending_flush.load(Ordering::SeqCst) {
1196            self.start_async_flush();
1197        }
1198
1199        info!(
1200            target: "block_producer",
1201            block_num = l2_block_number,
1202            ?block_hash,
1203            ?send_root,
1204            ?state_root,
1205            num_txs,
1206            gas_used,
1207            "Produced block"
1208        );
1209
1210        Ok(ProducedBlock {
1211            block_hash,
1212            send_root,
1213        })
1214    }
1215
1216    /// Start an async (non-blocking) flush to the background persistence thread.
1217    fn start_async_flush(&self) {
1218        let mut blocks: Vec<ExecutedBlock<ArbPrimitives>> = Vec::new();
1219        if let Some(head_state) = self.in_memory_state.head_state() {
1220            for block_state in head_state.chain() {
1221                blocks.push(block_state.block().clone());
1222            }
1223        }
1224        blocks.reverse();
1225
1226        let Some(last) = blocks.last() else {
1227            return;
1228        };
1229        let last_num_hash = alloy_eips::BlockNumHash::new(
1230            last.recovered_block().number(),
1231            last.recovered_block().hash(),
1232        );
1233
1234        // Double-buffer: move current accumulator to flushing slot.
1235        let current = std::mem::take(&mut *self.accumulated_trie_input.lock());
1236        *self.flushing_trie_input.lock() = Some(current);
1237
1238        self.blocks_since_flush.store(0, Ordering::SeqCst);
1239        self.pending_flush.store(true, Ordering::SeqCst);
1240
1241        let count = blocks.len();
1242        crate::launcher::start_flush(crate::launcher::FlushRequest {
1243            blocks,
1244            last_num_hash,
1245        });
1246
1247        debug!(
1248            target: "block_producer",
1249            count,
1250            last_block = last_num_hash.number,
1251            "Started async flush"
1252        );
1253    }
1254}
1255
1256#[async_trait::async_trait]
1257impl<Provider> BlockProducer for ArbBlockProducer<Provider>
1258where
1259    Provider: BlockNumReader
1260        + BlockReaderIdExt
1261        + HeaderProvider<Header = Header>
1262        + StateProviderFactory
1263        + Send
1264        + Sync
1265        + 'static,
1266{
1267    fn cache_init_message(&self, l2_msg: &[u8]) -> Result<(), BlockProducerError> {
1268        let init_msg = parse_init_message(l2_msg)
1269            .map_err(|e| BlockProducerError::Parse(format!("init message: {e}")))?;
1270
1271        info!(
1272            target: "block_producer",
1273            chain_id = %init_msg.chain_id,
1274            initial_l1_base_fee = %init_msg.initial_l1_base_fee,
1275            "Cached Init message params"
1276        );
1277
1278        *self.cached_init.lock() = Some(init_msg);
1279        Ok(())
1280    }
1281
1282    async fn produce_block(
1283        &self,
1284        msg_idx: u64,
1285        input: BlockProductionInput,
1286    ) -> Result<ProducedBlock, BlockProducerError> {
1287        let _lock = self.produce_lock.lock().await;
1288
1289        // Validate that this message is the next expected one.
1290        let head_num = self.head_block_number()?;
1291        let expected_block = head_num + 1;
1292        let actual_block = msg_idx;
1293
1294        if expected_block != actual_block {
1295            return Err(BlockProducerError::Unexpected(format!(
1296                "Expected block {expected_block} but got msg_idx {msg_idx} (block {actual_block})"
1297            )));
1298        }
1299
1300        // Parse L2 transactions from the message.
1301        let chain_id = self.chain_spec.chain().id();
1302
1303        let parsed_txs = parse_l2_transactions(
1304            input.kind,
1305            input.sender,
1306            &input.l2_msg,
1307            input.request_id,
1308            input.l1_base_fee,
1309            chain_id,
1310        )
1311        .unwrap_or_else(|e| {
1312            warn!(target: "block_producer", error=%e, "Error parsing L2 message, treating as empty");
1313            vec![]
1314        });
1315
1316        debug!(
1317            target: "block_producer",
1318            msg_idx,
1319            kind = input.kind,
1320            num_txs = parsed_txs.len(),
1321            "Parsed L1 message"
1322        );
1323
1324        self.apply_backpressure().await;
1325        self.produce_block_with_execution(&input, parsed_txs)
1326    }
1327
1328    async fn reset_to_block(&self, target_block_number: u64) -> Result<(), BlockProducerError> {
1329        let _lock = self.produce_lock.lock().await;
1330        let current = self.head_block_num.load(Ordering::SeqCst);
1331        if target_block_number > current {
1332            return Err(BlockProducerError::Unexpected(format!(
1333                "reset target {target_block_number} > current head {current}"
1334            )));
1335        }
1336        if target_block_number == current {
1337            return Ok(());
1338        }
1339
1340        let header = self
1341            .provider
1342            .sealed_header_by_number_or_tag(BlockNumberOrTag::Number(target_block_number))
1343            .map_err(|e| BlockProducerError::StateAccess(e.to_string()))?
1344            .ok_or_else(|| {
1345                BlockProducerError::Unexpected(format!(
1346                    "reset target block {target_block_number} not found"
1347                ))
1348            })?;
1349
1350        // Drain any in-flight flush before unwinding so disk state is consistent.
1351        if self.pending_flush.load(Ordering::SeqCst)
1352            && let Some(result) = crate::launcher::try_flush_result()
1353        {
1354            self.in_memory_state
1355                .remove_persisted_blocks(result.last_num_hash);
1356            *self.flushing_trie_input.lock() = None;
1357            self.pending_flush.store(false, Ordering::SeqCst);
1358        }
1359
1360        // Walk blocks above target in the in-memory state and gather
1361        // them as "old" for a reorg. Without them, the canonical head
1362        // points at the truncated block but consumers still see the
1363        // stale blocks in memory.
1364        let mut old_blocks: Vec<reth_chain_state::ExecutedBlock<ArbPrimitives>> = Vec::new();
1365        for bn in (target_block_number + 1)..=current {
1366            if let Some(state) = self.in_memory_state.state_by_number(bn) {
1367                old_blocks.push(state.block());
1368            }
1369        }
1370
1371        // Reorg with no new blocks => pure rollback.
1372        if !old_blocks.is_empty() {
1373            self.in_memory_state
1374                .update_chain(reth_chain_state::NewCanonicalChain::Reorg {
1375                    new: Vec::new(),
1376                    old: old_blocks,
1377                });
1378        }
1379
1380        self.invalidate_cached_overlay();
1381        self.invalidate_cached_prestate();
1382
1383        // Anchor the canonical head at the rolled-back block so RPC
1384        // queries like eth_blockNumber return the correct value.
1385        self.in_memory_state.set_canonical_head(header.clone());
1386
1387        // Reset the block producer's counter so the next digestMessage
1388        // extends from the new head.
1389        self.head_block_num
1390            .store(target_block_number, Ordering::SeqCst);
1391
1392        // Also remove persisted blocks above target from disk. The worker
1393        // thread runs this serially with flushes to avoid races.
1394        if let Some(rx) = crate::launcher::start_unwind(target_block_number) {
1395            match rx.recv() {
1396                Ok(Ok(())) => {}
1397                Ok(Err(e)) => {
1398                    return Err(BlockProducerError::Storage(format!(
1399                        "unwind above {target_block_number}: {e}"
1400                    )));
1401                }
1402                Err(e) => {
1403                    return Err(BlockProducerError::Storage(format!(
1404                        "unwind channel closed: {e}"
1405                    )));
1406                }
1407            }
1408        }
1409
1410        // Invalidate any trie-input carrying the now-removed blocks.
1411        *self.accumulated_trie_input.lock() = Arc::new(TrieInputSorted::default());
1412
1413        info!(
1414            target: "block_producer",
1415            target = target_block_number,
1416            hash = %header.hash(),
1417            old_count = current - target_block_number,
1418            "reset head"
1419        );
1420        Ok(())
1421    }
1422
1423    fn set_finality(
1424        &self,
1425        safe: Option<alloy_primitives::B256>,
1426        finalized: Option<alloy_primitives::B256>,
1427        validated: Option<alloy_primitives::B256>,
1428    ) -> Result<(), BlockProducerError> {
1429        let mut f = self.finality.lock();
1430        if safe.is_some() {
1431            f.safe = safe;
1432        }
1433        if finalized.is_some() {
1434            f.finalized = finalized;
1435        }
1436        if validated.is_some() {
1437            f.validated = validated;
1438        }
1439        drop(f);
1440
1441        // Propagate to reth's canonical in-memory state so
1442        // eth_getBlockByNumber("safe" | "finalized") returns the
1443        // correct header.
1444        if let Some(h) = safe
1445            && let Ok(Some(sealed)) = self.provider.sealed_header_by_hash(h)
1446        {
1447            self.in_memory_state.set_safe(sealed);
1448        }
1449        if let Some(h) = finalized
1450            && let Ok(Some(sealed)) = self.provider.sealed_header_by_hash(h)
1451        {
1452            self.in_memory_state.set_finalized(sealed);
1453        }
1454        // `validated` is Arbitrum-specific — reth's canonical state
1455        // exposes only safe/finalized. Push to the external watcher
1456        // so `arb_getValidatedBlock` RPC returns the latest value.
1457        if let Some(h) = validated
1458            && let Some(w) = self.validated_watcher.lock().as_ref()
1459        {
1460            *w.write() = h;
1461        }
1462        Ok(())
1463    }
1464
1465    fn attach_validated_watcher(&self, watcher: Arc<parking_lot::RwLock<alloy_primitives::B256>>) {
1466        *self.validated_watcher.lock() = Some(watcher);
1467    }
1468}
1469
1470// ---------------------------------------------------------------------------
1471// Helper functions
1472// ---------------------------------------------------------------------------
1473
1474/// Create an internal transaction (type 0x6A).
1475fn create_internal_tx(chain_id: u64, data: &[u8]) -> ArbTransactionSigned {
1476    use arb_primitives::signed_tx::ArbTypedTransaction;
1477    let tx = ArbTypedTransaction::Internal(ArbInternalTx {
1478        chain_id: U256::from(chain_id),
1479        data: Bytes::copy_from_slice(data),
1480    });
1481    let sig = alloy_primitives::Signature::new(U256::ZERO, U256::ZERO, false);
1482    ArbTransactionSigned::new_unhashed(tx, sig)
1483}
1484
1485/// Execute and commit an internal transaction via the block executor.
1486fn execute_and_commit_tx<E>(
1487    executor: &mut E,
1488    tx: &ArbTransactionSigned,
1489    label: &str,
1490) -> Result<(), BlockProducerError>
1491where
1492    E: BlockExecutor<Transaction = ArbTransactionSigned>,
1493{
1494    let recovered = tx
1495        .clone()
1496        .try_into_recovered()
1497        .map_err(|e| BlockProducerError::Execution(format!("{label} recovery: {e}")))?;
1498
1499    let result = executor
1500        .execute_transaction_without_commit(recovered)
1501        .map_err(|e| BlockProducerError::Execution(format!("{label} execution: {e}")))?;
1502
1503    executor
1504        .commit_transaction(result)
1505        .map_err(|e| BlockProducerError::Execution(format!("{label} commit: {e}")))?;
1506
1507    Ok(())
1508}
1509
1510fn compute_mix_hash(send_count: u64, l1_block_number: u64, arbos_version: u64) -> B256 {
1511    arbos::header::compute_arbos_mixhash(send_count, l1_block_number, arbos_version, false)
1512}
1513
1514/// L1 block number for the `NUMBER` opcode: monotonic, so a reported value
1515/// below the parent's (recovered from its mix_hash) is clamped up to it.
1516fn monotonic_l1_block_number(reported: u64, parent_mix_hash: &B256) -> u64 {
1517    reported.max(l1_block_number_from_mix_hash(parent_mix_hash))
1518}
1519
1520/// EIP-161: mark empty non-zombie accounts for trie deletion.
1521fn delete_empty_accounts(
1522    bundle: &mut BundleState,
1523    zombie_accounts: &rustc_hash::FxHashSet<Address>,
1524    state_provider: &dyn StateProvider,
1525) {
1526    let keccak_empty = alloy_primitives::B256::from(alloy_primitives::keccak256([]));
1527    let mut to_remove = Vec::new();
1528    for (addr, account) in bundle.state.iter_mut() {
1529        if let Some(ref info) = account.info {
1530            let is_empty =
1531                info.nonce == 0 && info.balance.is_zero() && info.code_hash == keccak_empty;
1532            if is_empty && !zombie_accounts.contains(addr) {
1533                let existed_before = state_provider.basic_account(addr).ok().flatten().is_some();
1534                if existed_before {
1535                    debug!(
1536                        target: "block_producer",
1537                        addr = ?addr,
1538                        "EIP-161: deleting empty account from state"
1539                    );
1540                    account.info = None;
1541                } else {
1542                    to_remove.push(*addr);
1543                }
1544            }
1545        }
1546    }
1547    for addr in to_remove {
1548        bundle.state.remove(&addr);
1549    }
1550}
1551
1552/// Remove unchanged storage slots from the bundle.
1553fn filter_unchanged_storage(bundle: &mut BundleState) {
1554    for (_addr, account) in bundle.state.iter_mut() {
1555        account
1556            .storage
1557            .retain(|_key, slot| slot.present_value != slot.previous_or_original_value);
1558    }
1559}
1560
1561/// Derive ArbHeaderInfo from post-execution state.
1562fn derive_header_info_from_state(
1563    state_provider: &dyn StateProvider,
1564    bundle_state: &BundleState,
1565    coinbase: Address,
1566) -> Result<Option<ArbHeaderInfo>, BlockProducerError> {
1567    let read_slot = |addr: Address, slot: B256| {
1568        if let Some(account) = bundle_state.state.get(&addr) {
1569            let slot_u256 = U256::from_be_bytes(slot.0);
1570            if let Some(storage_slot) = account.storage.get(&slot_u256) {
1571                return Ok(Some(storage_slot.present_value));
1572            }
1573        }
1574        state_provider.storage(addr, slot)
1575    };
1576
1577    derive_arb_header_info(&read_slot, coinbase)
1578        .map_err(|e| BlockProducerError::Storage(e.to_string()))
1579}
1580
1581/// Augment the bundle with direct cache modifications not captured by EVM transitions.
1582fn augment_bundle_from_cache(
1583    bundle: &mut BundleState,
1584    cache: &revm_database::CacheState,
1585    state_provider: &dyn StateProvider,
1586) -> Result<(), BlockProducerError> {
1587    use revm_database::states::plain_account::StorageSlot;
1588
1589    for (addr, cache_acct) in &cache.accounts {
1590        let current_info = cache_acct.account.as_ref().map(|a| a.info.clone());
1591        let current_storage = cache_acct
1592            .account
1593            .as_ref()
1594            .map(|a| &a.storage)
1595            .cloned()
1596            .unwrap_or_default();
1597
1598        if let Some(bundle_acct) = bundle.state.get_mut(addr) {
1599            // Update existing bundle entry from cache.
1600            bundle_acct.info = current_info;
1601
1602            for (key, value) in &current_storage {
1603                if let Some(slot) = bundle_acct.storage.get_mut(key) {
1604                    slot.present_value = *value;
1605                } else {
1606                    // Slot written via direct cache modification.
1607                    let original_value = state_provider
1608                        .storage(*addr, B256::from(*key))
1609                        .map_err(|e| BlockProducerError::Storage(e.to_string()))?
1610                        .unwrap_or(U256::ZERO);
1611                    if *value != original_value {
1612                        bundle_acct.storage.insert(
1613                            *key,
1614                            StorageSlot {
1615                                previous_or_original_value: original_value,
1616                                present_value: *value,
1617                            },
1618                        );
1619                    }
1620                }
1621            }
1622        } else {
1623            // Account not in bundle — check if modified from original.
1624            let original = state_provider
1625                .basic_account(addr)
1626                .map_err(|e| BlockProducerError::Storage(e.to_string()))?;
1627
1628            let info_changed = match (&original, &current_info) {
1629                (None, None) => false,
1630                (Some(_), None) | (None, Some(_)) => true,
1631                (Some(orig), Some(curr)) => {
1632                    orig.balance != curr.balance
1633                        || orig.nonce != curr.nonce
1634                        || orig
1635                            .bytecode_hash
1636                            .unwrap_or(alloy_primitives::KECCAK256_EMPTY)
1637                            != curr.code_hash
1638                }
1639            };
1640
1641            let mut storage_changes: alloy_primitives::map::HashMap<U256, StorageSlot> =
1642                alloy_primitives::map::HashMap::default();
1643            for (key, value) in &current_storage {
1644                let original_value = state_provider
1645                    .storage(*addr, B256::from(*key))
1646                    .map_err(|e| BlockProducerError::Storage(e.to_string()))?
1647                    .unwrap_or(U256::ZERO);
1648                if original_value != *value {
1649                    storage_changes.insert(
1650                        *key,
1651                        StorageSlot {
1652                            previous_or_original_value: original_value,
1653                            present_value: *value,
1654                        },
1655                    );
1656                }
1657            }
1658
1659            if info_changed || !storage_changes.is_empty() {
1660                let original_info = original.as_ref().map(|a| revm::state::AccountInfo {
1661                    balance: a.balance,
1662                    nonce: a.nonce,
1663                    code_hash: a.bytecode_hash.unwrap_or(alloy_primitives::KECCAK256_EMPTY),
1664                    code: None,
1665                    account_id: None,
1666                });
1667
1668                let status = if original.is_some() {
1669                    revm_database::AccountStatus::Changed
1670                } else {
1671                    revm_database::AccountStatus::InMemoryChange
1672                };
1673
1674                bundle.state.insert(
1675                    *addr,
1676                    revm_database::BundleAccount {
1677                        info: current_info,
1678                        original_info,
1679                        storage: storage_changes,
1680                        status,
1681                    },
1682                );
1683            }
1684        }
1685    }
1686    Ok(())
1687}
1688
1689#[cfg(test)]
1690mod tests {
1691    use arbos::header::compute_arbos_mixhash;
1692
1693    use super::*;
1694
1695    #[test]
1696    fn l1_block_number_clamps_to_parent() {
1697        let parent = compute_arbos_mixhash(0, 10_538_022, 51, false);
1698        // A lower sequencer-reported value is clamped up to the parent's.
1699        assert_eq!(monotonic_l1_block_number(10_537_967, &parent), 10_538_022);
1700        // A higher value advances normally.
1701        assert_eq!(monotonic_l1_block_number(10_538_099, &parent), 10_538_099);
1702        // Equal stays put.
1703        assert_eq!(monotonic_l1_block_number(10_538_022, &parent), 10_538_022);
1704    }
1705}