Skip to main content

ethrex_blockchain/
blockchain.rs

1//! # ethrex Blockchain
2//!
3//! Core blockchain logic for the ethrex Ethereum client.
4//!
5//! ## Overview
6//!
7//! This module implements the blockchain layer, which is responsible for:
8//! - Block validation and execution
9//! - State management and transitions
10//! - Fork choice rule implementation
11//! - Transaction mempool management
12//! - Payload building for block production
13//!
14//! ## Key Components
15//!
16//! - [`Blockchain`]: Main interface for blockchain operations
17//! - [`Mempool`]: Transaction pool for pending transactions
18//! - [`fork_choice`]: Fork choice rule implementation
19//! - [`payload`]: Block payload building for consensus
20//!
21//! ## Block Execution Flow
22//!
23//! ```text
24//! 1. Receive block from consensus/P2P
25//! 2. Validate block header (parent, timestamp, gas limit, etc.)
26//! 3. Execute transactions in EVM
27//! 4. Verify state root matches header
28//! 5. Store block and update canonical chain
29//! ```
30//!
31//! ## Usage
32//!
33//! ```ignore
34//! use ethrex_blockchain::Blockchain;
35//!
36//! let blockchain = Blockchain::new(store, BlockchainOptions::default());
37//!
38//! // Add a block
39//! blockchain.add_block(&block)?;
40//!
41//! // Add transaction to mempool
42//! blockchain.add_transaction_to_mempool(tx).await?;
43//! ```
44
45pub mod constants;
46pub mod error;
47pub mod fork_choice;
48pub mod mempool;
49pub mod payload;
50pub mod tracing;
51pub mod vm;
52
53use ::tracing::{debug, error, info, instrument, warn};
54use constants::{AMSTERDAM_MAX_INITCODE_SIZE, MAX_INITCODE_SIZE, POST_OSAKA_GAS_LIMIT_CAP};
55use error::MempoolError;
56use error::{ChainError, InvalidBlockError};
57use ethrex_common::constants::{EMPTY_KECCAK_HASH, EMPTY_TRIE_HASH, MIN_BASE_FEE_PER_BLOB_GAS};
58
59use crossbeam::channel::{self as cb, TryRecvError, select};
60// Re-export stateless validation functions for backwards compatibility
61#[cfg(feature = "c-kzg")]
62use ethrex_common::types::EIP4844Transaction;
63#[cfg(feature = "c-kzg")]
64use ethrex_common::types::MAX_BLOB_TX_SIZE;
65use ethrex_common::types::MAX_TX_SIZE;
66use ethrex_common::types::block_access_list::BlockAccessList;
67use ethrex_common::types::block_execution_witness::ExecutionWitness;
68use ethrex_common::types::fee_config::FeeConfig;
69use ethrex_common::types::{
70    AccountInfo, AccountState, AccountUpdate, BalSynthesisItem, Block, BlockHash, BlockHeader,
71    BlockNumber, ChainConfig, Code, Receipt, Transaction, WrappedEIP4844Transaction,
72    synthesize_bal_updates, validate_block_body,
73};
74use ethrex_common::types::{EIP7702_DELEGATED_CODE_LEN, is_eip7702_delegation};
75use ethrex_common::types::{ELASTICITY_MULTIPLIER, P2PTransaction};
76use ethrex_common::types::{Fork, MempoolTransaction};
77use ethrex_common::utils::keccak;
78use ethrex_common::{Address, H256, TrieLogger, U256};
79pub use ethrex_common::{
80    get_total_blob_gas, validate_block_access_list_hash, validate_block_pre_execution,
81    validate_gas_used, validate_receipts_root_and_logs_bloom, validate_requests_hash,
82};
83use ethrex_crypto::NativeCrypto;
84use ethrex_metrics::metrics;
85use ethrex_rlp::constants::RLP_NULL;
86use ethrex_rlp::decode::RLPDecode;
87use ethrex_rlp::encode::RLPEncode;
88use ethrex_storage::{
89    AccountUpdatesList, Store, UpdateBatch, error::StoreError, hash_address, hash_key,
90};
91use ethrex_trie::node::{BranchNode, ExtensionNode, LeafNode};
92use ethrex_trie::{Nibbles, Node, NodeRef, Trie, TrieError, TrieNode};
93use ethrex_vm::backends::CachingDatabase;
94#[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
95use ethrex_vm::backends::levm::LEVM;
96use ethrex_vm::backends::levm::db::DatabaseLogger;
97use ethrex_vm::{BlockExecutionResult, DynVmDatabase, Evm, EvmError};
98use mempool::Mempool;
99use payload::PayloadOrTask;
100use rustc_hash::{FxHashMap, FxHashSet};
101use std::collections::hash_map::Entry;
102use std::collections::{BTreeMap, HashMap, HashSet};
103use std::sync::LazyLock;
104use std::sync::mpsc::Sender;
105use std::sync::{
106    Arc, RwLock,
107    atomic::{AtomicBool, AtomicUsize, Ordering},
108    mpsc::{Receiver, channel},
109};
110use std::time::{Duration, Instant};
111use tokio::sync::Mutex as TokioMutex;
112use tokio_util::sync::CancellationToken;
113
114use vm::StoreVmDatabase;
115
116#[cfg(feature = "metrics")]
117use ethrex_metrics::bal::METRICS_BAL;
118#[cfg(feature = "metrics")]
119use ethrex_metrics::blocks::METRICS_BLOCKS;
120
121#[cfg(feature = "c-kzg")]
122use ethrex_common::types::BlobsBundle;
123
124const MAX_PAYLOADS: usize = 10;
125const MAX_MEMPOOL_SIZE_DEFAULT: usize = 10_000;
126
127/// Background thread for dropping large tree structures off the critical path.
128/// Accepts any `Send` value and drops it on a dedicated thread, avoiding
129/// recursive deallocation costs (~500us for state trie roots) on hot paths.
130static DROP_SENDER: LazyLock<Sender<Box<dyn Send>>> = LazyLock::new(|| {
131    let (tx, rx) = channel::<Box<dyn Send>>();
132    std::thread::Builder::new()
133        .name("drop_thread".to_string())
134        .spawn(move || for _ in rx {})
135        .expect("failed to spawn drop thread");
136    tx
137});
138
139// Result type for execute_block_pipeline
140type BlockExecutionPipelineResult = (
141    BlockExecutionResult,
142    AccountUpdatesList,
143    Option<Vec<AccountUpdate>>,
144    Option<BlockAccessList>, // produced BAL (Some on Amsterdam+ blocks)
145    usize,                   // max queue length
146    [Instant; 7],            // timing instants
147    Duration,                // warmer duration
148);
149
150type AddBlockPipelineInnerResult = (
151    Option<BlockAccessList>,
152    Option<ExecutionWitness>,
153    Result<(), ChainError>,
154);
155
156//TODO: Implement a struct Chain or BlockChain to encapsulate
157//functionality and canonical chain state and config
158
159/// Specifies whether the blockchain operates as L1 (mainnet/testnet) or L2 (rollup).
160#[derive(Debug, Clone, Default)]
161pub enum BlockchainType {
162    /// Standard Ethereum L1 blockchain.
163    #[default]
164    L1,
165    /// Layer 2 rollup with additional fee configuration.
166    L2(L2Config),
167}
168
169/// Configuration for L2 rollup operation.
170#[derive(Debug, Clone, Default)]
171pub struct L2Config {
172    /// Fee configuration for L2 transactions.
173    ///
174    /// Uses `RwLock` because the Watcher updates L1 fee config periodically.
175    pub fee_config: Arc<RwLock<FeeConfig>>,
176}
177
178/// Core blockchain implementation for block validation and execution.
179///
180/// The `Blockchain` struct is the main entry point for all blockchain operations:
181/// - Adding and validating blocks
182/// - Managing the transaction mempool
183/// - Building payloads for block production
184/// - Handling fork choice updates
185///
186/// # Thread Safety
187///
188/// `Blockchain` uses interior mutability for thread-safe access to shared state.
189/// The mempool and payload storage are protected by appropriate synchronization primitives.
190///
191/// # Example
192///
193/// ```ignore
194/// let blockchain = Blockchain::new(store, BlockchainOptions::default());
195///
196/// // Validate and add a block
197/// blockchain.add_block(&block)?;
198///
199/// // Check sync status
200/// if blockchain.is_synced() {
201///     // Process transactions from mempool
202/// }
203/// ```
204#[derive(Debug)]
205pub struct Blockchain {
206    /// Underlying storage for blocks and state.
207    storage: Store,
208    /// Transaction mempool for pending transactions.
209    pub mempool: Mempool,
210    /// Whether the node has completed initial sync.
211    ///
212    /// Set to true after initial sync completes, never reset to false.
213    /// Does not reflect whether an ongoing sync is in progress.
214    is_synced: AtomicBool,
215    /// Configuration options for blockchain behavior.
216    pub options: BlockchainOptions,
217    /// Cache of recently built payloads.
218    ///
219    /// Maps payload IDs to either completed payloads or in-progress build tasks.
220    /// Kept around in case consensus requests the same payload twice.
221    pub payloads: Arc<TokioMutex<Vec<(u64, PayloadOrTask)>>>,
222    /// Persistent thread pool for merkleization workers.
223    /// 17 threads: 16 shard workers + 1 watcher/coordination.
224    ///
225    /// `Arc` for sharing in test harnesses that build many `Blockchain`s; the
226    /// production path keeps the original semantics (one fresh pool per call
227    /// to `Blockchain::new` / `default_with_store`).
228    merkle_pool: Arc<rayon::ThreadPool>,
229}
230
231/// Configuration options for the blockchain.
232#[derive(Debug, Clone)]
233pub struct BlockchainOptions {
234    /// Maximum number of transactions in the mempool.
235    pub max_mempool_size: usize,
236    /// Whether to emit performance logging.
237    pub perf_logs_enabled: bool,
238    /// Blockchain type (L1 or L2).
239    pub r#type: BlockchainType,
240    /// EIP-7872: User-configured maximum blobs per block for local building.
241    /// If None, uses the protocol maximum for the current fork.
242    pub max_blobs_per_block: Option<u32>,
243    /// If true, computes execution witnesses upon receiving newPayload messages and stores them in local storage
244    pub precompute_witnesses: bool,
245    /// If true (default), per-block execution caches precompile results between the
246    /// warmer thread and the executor. Set to false (via `--no-precompile-cache`) to
247    /// disable the cache for benchmarking purposes.
248    pub precompile_cache_enabled: bool,
249    /// If true (default), Amsterdam+ validation runs transactions in parallel
250    /// using the header BAL to seed per-tx databases. Set to false (via
251    /// `--no-bal-parallel-exec`) to fall back to sequential execution.
252    pub bal_parallel_exec_enabled: bool,
253    /// If true (default), Amsterdam+ validation spawns a warmer thread that
254    /// prefetches accounts, storage slots, and codes listed in the header BAL.
255    /// Set to false (via `--no-bal-prefetch`) to skip prefetching on the BAL path.
256    pub bal_prefetch_enabled: bool,
257    /// If true (default), Amsterdam+ validation merkleizes optimistically from
258    /// `synthesize_bal_updates` in parallel with execution. Set to false (via
259    /// `--no-bal-parallel-trie`) to fall back to streaming `AccountUpdate`s from
260    /// the executor and merkleizing post-execution.
261    pub bal_parallel_trie_enabled: bool,
262}
263
264impl Default for BlockchainOptions {
265    fn default() -> Self {
266        Self {
267            max_mempool_size: MAX_MEMPOOL_SIZE_DEFAULT,
268            perf_logs_enabled: false,
269            r#type: BlockchainType::default(),
270            max_blobs_per_block: None,
271            precompute_witnesses: false,
272            precompile_cache_enabled: true,
273            bal_parallel_exec_enabled: true,
274            bal_prefetch_enabled: true,
275            bal_parallel_trie_enabled: true,
276        }
277    }
278}
279
280#[derive(Debug, Clone)]
281pub struct BatchBlockProcessingFailure {
282    pub last_valid_hash: H256,
283    pub failed_block_hash: H256,
284}
285
286fn log_batch_progress(batch_size: u32, current_block: u32) {
287    let progress_needed = batch_size > 10;
288    const PERCENT_MARKS: [u32; 4] = [20, 40, 60, 80];
289    if progress_needed {
290        PERCENT_MARKS.iter().for_each(|mark| {
291            if (batch_size * mark) / 100 == current_block {
292                info!("[SYNCING] {mark}% of batch processed");
293            }
294        });
295    }
296}
297
298enum WorkerRequest {
299    // From main thread (routed by account bucket)
300    ProcessAccount {
301        prefix: H256,
302        info: Option<AccountInfo>,
303        storage: FxHashMap<H256, U256>,
304        removed: bool,
305        removed_storage: bool,
306    },
307    // From main thread (broadcast to all workers)
308    FinishRouting,
309    MerklizeAccounts {
310        accounts: Vec<H256>,
311    },
312    CollectState {
313        tx: Sender<CollectedStateMsg>,
314    },
315    // Cross-worker storage messages (routed by storage key bucket)
316    MerklizeStorage {
317        prefix: H256,
318        key: H256,
319        value: U256,
320        storage_root: H256,
321    },
322    DeleteStorage(H256),
323    // Cross-worker: signals this worker finished routing all MerklizeStorage
324    RoutingDone {
325        from: u8,
326    },
327    // Cross-worker storage results (routed by account bucket)
328    StorageShard {
329        prefix: H256,
330        index: u8,
331        subroot: Box<BranchNode>,
332        nodes: Vec<TrieNode>,
333    },
334}
335
336struct CollectedStateMsg {
337    index: u8,
338    subroot: Box<BranchNode>,
339    state_nodes: Vec<TrieNode>,
340    storage_nodes: Vec<(H256, Vec<TrieNode>)>,
341}
342
343#[derive(Default)]
344struct PreMerkelizedAccountState {
345    storage_root: Option<Box<BranchNode>>,
346    nodes: Vec<TrieNode>,
347}
348
349/// Work item for BAL state trie shard workers.
350struct BalStateWorkItem {
351    hashed_address: H256,
352    nonce: Option<u64>,
353    balance: Option<U256>,
354    code_hash: Option<H256>,
355    /// Pre-computed storage root from Stage B, or None to keep existing.
356    storage_root: Option<H256>,
357}
358
359impl Blockchain {
360    /// Build a fresh 17-thread merkleization pool. Used by the default
361    /// constructors; tests that build many `Blockchain`s should share one pool
362    /// via `default_with_store_and_pool` to avoid spawning the pool repeatedly.
363    pub fn build_merkle_pool() -> Arc<rayon::ThreadPool> {
364        Arc::new(
365            rayon::ThreadPoolBuilder::new()
366                .num_threads(17)
367                .thread_name(|i| format!("merkle-worker-{i}"))
368                .build()
369                .expect("Failed to create merkle thread pool"),
370        )
371    }
372
373    pub fn new(store: Store, blockchain_opts: BlockchainOptions) -> Self {
374        Self {
375            storage: store,
376            mempool: Mempool::new(blockchain_opts.max_mempool_size),
377            is_synced: AtomicBool::new(false),
378            payloads: Arc::new(TokioMutex::new(Vec::new())),
379            options: blockchain_opts,
380            merkle_pool: Self::build_merkle_pool(),
381        }
382    }
383
384    /// Like `default_with_store`, but reuses an externally-owned merkleization
385    /// pool. Intended for test harnesses that build many short-lived
386    /// `Blockchain` instances; sharing the pool avoids spawning 17 fresh OS
387    /// threads per instance.
388    ///
389    /// SAFETY: the caller must ensure each pool has only one concurrent
390    /// `in_place_scope` user at a time. The internal merkle protocol requires
391    /// all 16 worker jobs to run concurrently (they cross-communicate via
392    /// channels); sharing a pool across simultaneous callers deadlocks.
393    pub fn default_with_store_and_pool(store: Store, pool: Arc<rayon::ThreadPool>) -> Self {
394        Self {
395            storage: store,
396            mempool: Mempool::new(MAX_MEMPOOL_SIZE_DEFAULT),
397            is_synced: AtomicBool::new(false),
398            payloads: Arc::new(TokioMutex::new(Vec::new())),
399            options: BlockchainOptions::default(),
400            merkle_pool: pool,
401        }
402    }
403
404    pub fn default_with_store(store: Store) -> Self {
405        Self {
406            storage: store,
407            mempool: Mempool::new(MAX_MEMPOOL_SIZE_DEFAULT),
408            is_synced: AtomicBool::new(false),
409            payloads: Arc::new(TokioMutex::new(Vec::new())),
410            options: BlockchainOptions::default(),
411            merkle_pool: Self::build_merkle_pool(),
412        }
413    }
414
415    /// L1 blocks must not contain L2-only transaction types (`FeeToken` 0x7d,
416    /// `Privileged` 0x7e). Both are L2-only types unknown to other L1 clients, so
417    /// accepting one on L1 diverges consensus. `Privileged` additionally takes its
418    /// sender from an unsigned, caller-chosen `from` (no signature recovery), so it
419    /// would also let a block forge a sender. On L2 these types are valid, so this
420    /// check only applies to L1.
421    fn validate_l1_transaction_types(&self, block: &Block) -> Result<(), ChainError> {
422        if !matches!(self.options.r#type, BlockchainType::L1) {
423            return Ok(());
424        }
425        for tx in &block.body.transactions {
426            if tx.tx_type().is_l2_only() {
427                return Err(ChainError::InvalidBlock(
428                    InvalidBlockError::UnsupportedTransactionType(tx.tx_type() as u8),
429                ));
430            }
431        }
432        Ok(())
433    }
434
435    /// Executes a block withing a new vm instance and state
436    fn execute_block(
437        &self,
438        block: &Block,
439    ) -> Result<(BlockExecutionResult, Vec<AccountUpdate>), ChainError> {
440        // Validate if it can be the new head and find the parent
441        let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else {
442            // If the parent is not present, we store it as pending.
443            self.storage.add_pending_block(block.clone())?;
444            return Err(ChainError::ParentNotFound);
445        };
446
447        let chain_config = self.storage.get_chain_config();
448
449        // Validate the block pre-execution
450        validate_block_pre_execution(block, &parent_header, &chain_config, ELASTICITY_MULTIPLIER)?;
451        self.validate_l1_transaction_types(block)?;
452
453        let vm_db = StoreVmDatabase::new(self.storage.clone(), parent_header)?;
454        let mut vm = self.new_evm(vm_db)?;
455
456        let (execution_result, bal) = vm.execute_block(block)?;
457        let account_updates = vm.get_state_transitions()?;
458
459        // Validate execution went alright
460        if let Err(e) = validate_gas_used(execution_result.block_gas_used, &block.header) {
461            ethrex_vm::log_gas_used_mismatch(
462                &execution_result.tx_gas_breakdowns,
463                block.header.number,
464                execution_result.block_gas_used,
465                block.header.gas_used,
466            );
467            return Err(e.into());
468        }
469        validate_receipts_root_and_logs_bloom(
470            &block.header,
471            &execution_result.receipts,
472            &NativeCrypto,
473        )?;
474        validate_requests_hash(&block.header, &chain_config, &execution_result.requests)?;
475        if let Some(bal) = &bal {
476            validate_block_access_list_hash(
477                &block.header,
478                &chain_config,
479                bal,
480                block.body.transactions.len(),
481                &NativeCrypto,
482            )?;
483        }
484
485        Ok((execution_result, account_updates))
486    }
487
488    /// Generates Block Access List by re-executing a block.
489    /// Returns None for pre-Amsterdam blocks.
490    /// This is used by engine_getPayloadBodiesByHashV2 and engine_getPayloadBodiesByRangeV2.
491    pub fn generate_bal_for_block(
492        &self,
493        block: &Block,
494    ) -> Result<Option<BlockAccessList>, ChainError> {
495        let chain_config = self.storage.get_chain_config();
496
497        // Pre-Amsterdam blocks don't have BAL
498        if !chain_config.is_amsterdam_activated(block.header.timestamp) {
499            return Ok(None);
500        }
501
502        // Find parent header
503        let parent_header = find_parent_header(&block.header, &self.storage)?;
504
505        // Create VM and execute block with BAL recording
506        let vm_db = StoreVmDatabase::new(self.storage.clone(), parent_header)?;
507        let mut vm = self.new_evm(vm_db)?;
508
509        let (_execution_result, bal) = vm.execute_block(block)?;
510
511        Ok(bal)
512    }
513
514    /// Executes a block withing a new vm instance and state
515    #[instrument(
516        level = "trace",
517        name = "Execute Block",
518        skip_all,
519        fields(namespace = "block_execution")
520    )]
521    fn execute_block_pipeline(
522        &self,
523        block: &Block,
524        parent_header: &BlockHeader,
525        vm: &mut Evm,
526        bal: Option<Arc<BlockAccessList>>,
527        collect_witness: bool,
528    ) -> Result<BlockExecutionPipelineResult, ChainError> {
529        let start_instant = Instant::now();
530
531        let chain_config = self.storage.get_chain_config();
532
533        // Validate the block pre-execution
534        validate_block_pre_execution(block, parent_header, &chain_config, ELASTICITY_MULTIPLIER)?;
535        self.validate_l1_transaction_types(block)?;
536        validate_block_body(&block.header, &block.body, &NativeCrypto)
537            .map_err(|e| ChainError::InvalidBlock(InvalidBlockError::InvalidBody(e)))?;
538        let block_validated_instant = Instant::now();
539
540        let exec_merkle_start = Instant::now();
541        let queue_length = AtomicUsize::new(0);
542        let queue_length_ref = &queue_length;
543        let mut max_queue_length = 0;
544
545        // Wrap the store with CachingDatabase so both warming and execution
546        // can benefit from shared caching of state lookups
547        let original_store = vm.db.store.clone();
548        let caching_store: Arc<dyn ethrex_vm::backends::LevmDatabase> = Arc::new(
549            CachingDatabase::new(original_store, self.options.precompile_cache_enabled),
550        );
551
552        // Replace the VM's store with the caching version
553        vm.db.store = caching_store.clone();
554
555        let cancelled = AtomicBool::new(false);
556        // Witness collection also forces sequential execution: parallel lanes
557        // re-read in-block-created state (e.g. a code deployed by an earlier
558        // tx) from the logged store, while sequential execution serves it from
559        // VM caches — recording accesses the canonical execution never makes.
560        let bal_parallel_exec_enabled = self.options.bal_parallel_exec_enabled && !collect_witness;
561
562        // Synthesize BAL updates pre-scope so the merkleizer thread can start
563        // trie work immediately, in parallel with execution.
564        // `--no-bal-parallel-trie` opts out: leave `optimistic_updates = None` so
565        // the merkleizer takes the streaming branch (fed by the EVM-side
566        // `bal_to_account_updates` send over the channel below).
567        // Witness collection forces the streaming branch too: the sequential
568        // executor (see `bal_parallel_exec_enabled` below) streams per-tx
569        // updates over the channel, which only the streaming merkleizer
570        // consumes — the synthesized path would leave the receiver dropped.
571        let optimistic_updates: Option<FxHashMap<Address, BalSynthesisItem>> =
572            if self.options.bal_parallel_trie_enabled && !collect_witness {
573                bal.as_deref().map(synthesize_bal_updates)
574            } else {
575                None
576            };
577
578        // Synchronously warm all BAL storage slots before the executor thread starts.
579        //
580        // The warmer and executor share one CachingDatabase; `prefetch_storage`
581        // populates the cache only after its whole parallel fetch completes, so when
582        // the warmer ran storage concurrently the executor raced it to the trie for
583        // SSTORE original values and lost (~22% of CPU on cold-cache import-bench).
584        // Doing the storage prefetch up front (parallel, on all cores) lets execution
585        // run fully warm and removes the warmer's CPU/lock contention with it.
586        //
587        // Measured on bal-devnet-7-mainnet-mix-460 (import-bench --with-bal, vs main):
588        //   - concurrent storage warming (any ordering/chunking): ~ -7% to -13%
589        //   - this synchronous full-storage prefetch:             ~ -24%
590        // DO NOT move storage back into the concurrent warmer; the race is the whole
591        // problem. DO NOT add account prefetch here too: that regressed (~ +150 ms),
592        // because account reads already overlap exec well and a synchronous pass both
593        // adds serial latency and double-fetches against the warmer's Phase 1. Slots
594        // are warmed in natural account order; an execution-order sort gave no benefit
595        // once every slot is warm before exec.
596        //
597        // Live-node tradeoff: this prefetch is on the critical path before exec, no
598        // longer overlapped with it. With a warm cache the reads hit and it is a
599        // no-op; on a genuinely cold block (first slot after restart, account-heavy
600        // block) it adds serial latency the old overlapped warmer would have hidden.
601        // The benchmarks above are cold-cache batch import, not single-block live
602        // tail latency; the tradeoff is deliberate and favors throughput.
603        //
604        // Gated by `--no-bal-prefetch`: when the operator disables BAL-driven
605        // prefetching, skip the synchronous storage warm too. The warmer thread
606        // below already honors the same toggle.
607        // Witness collection records every read that reaches the store-backed
608        // logger beneath the shared cache. The warmer's speculative reads would
609        // be recorded as state accesses the canonical execution never makes,
610        // polluting the witness (e.g. `engine_newPayloadWithWitnessV5`), so
611        // warming is skipped entirely when a witness is being collected.
612        #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
613        if self.options.bal_prefetch_enabled
614            && !collect_witness
615            && let Some(bal_ref) = bal.as_ref()
616        {
617            let slots = LEVM::bal_storage_slots(bal_ref);
618            if !slots.is_empty() {
619                let _ = caching_store.prefetch_storage(&slots);
620            }
621        }
622
623        // Each thread that captures `bal` needs its own Arc clone (cheap pointer bump).
624        #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
625        let bal_warmer = bal.clone();
626
627        let (execution_result, merkleization_result, warmer_duration) = std::thread::scope(
628            |s| -> Result<_, ChainError> {
629                #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
630                let vm_type = vm.vm_type;
631                let cancelled_ref = &cancelled;
632                #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
633                let bal_prefetch_enabled = self.options.bal_prefetch_enabled;
634                #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
635                let warm_handle = (!collect_witness)
636                    .then(|| {
637                        std::thread::Builder::new()
638                            .name("block_executor_warmer".to_string())
639                            .spawn_scoped(s, move || {
640                                // Warming uses the same caching store, sharing cached state with execution.
641                                // Precompile cache lives inside CachingDatabase, shared automatically.
642                                let start = Instant::now();
643                                if let Some(bal) = bal_warmer {
644                                    if bal_prefetch_enabled {
645                                        // Amsterdam+: BAL-based precise prefetching (no tx re-execution).
646                                        if let Err(e) = LEVM::warm_block_from_bal(
647                                            &bal,
648                                            caching_store,
649                                            cancelled_ref,
650                                        ) {
651                                            debug!("BAL warming failed (non-fatal): {e}");
652                                        }
653                                    } else if !bal_parallel_exec_enabled {
654                                        // --no-bal-prefetch combined with --no-bal-parallel-exec:
655                                        // mirror the pre-Amsterdam setup where a parallel speculative
656                                        // warmer races ahead of the serial executor. With parallel
657                                        // exec still on, we skip warming instead — two parallel passes
658                                        // over the same txs would just fight for cores.
659                                        if let Err(e) = LEVM::warm_block(
660                                            block,
661                                            caching_store,
662                                            vm_type,
663                                            &NativeCrypto,
664                                            cancelled_ref,
665                                        ) {
666                                            debug!("Block warming failed (non-fatal): {e}");
667                                        }
668                                    }
669                                } else {
670                                    // Pre-Amsterdam / P2P sync: speculative tx re-execution
671                                    if let Err(e) = LEVM::warm_block(
672                                        block,
673                                        caching_store,
674                                        vm_type,
675                                        &NativeCrypto,
676                                        cancelled_ref,
677                                    ) {
678                                        debug!("Block warming failed (non-fatal): {e}");
679                                    }
680                                }
681                                start.elapsed()
682                            })
683                            .map_err(|e| {
684                                ChainError::Custom(format!("Failed to spawn warmer thread: {e}"))
685                            })
686                    })
687                    .transpose()?;
688                let max_queue_length_ref = &mut max_queue_length;
689                // Channel is needed whenever the merkleizer takes the streaming
690                // branch OR LEVM falls into the sequential path:
691                // - sequential LEVM (`!bal_parallel_exec_enabled`) sends per-tx
692                //   updates via `send_state_transitions_tx`; errors if Sender is None.
693                // - streaming merkleizer (`!bal_parallel_trie_enabled` or no BAL)
694                //   reads updates from `rx`.
695                // Only the default `bal=Some && parallel_exec && parallel_trie` case
696                // can skip both: parallel LEVM doesn't stream when its Sender is None,
697                // and the merkleizer uses the synthesized optimistic map directly.
698                let (tx, rx_for_merkle) =
699                    if optimistic_updates.is_some() && bal_parallel_exec_enabled {
700                        // Paired with the merkleizer's `rx_for_merkle.expect()` below:
701                        // this is the only arm allowed to skip the channel. If a future
702                        // refactor lets another branch reach here with `rx == None`, the
703                        // merkleizer would panic instead of silently dropping updates.
704                        (None, None)
705                    } else {
706                        let (tx, rx) = channel();
707                        (Some(tx), Some(rx))
708                    };
709
710                let execution_handle = std::thread::Builder::new()
711                    .name("block_executor_execution".to_string())
712                    .spawn_scoped(s, move || -> Result<_, ChainError> {
713                        // Cheap Arc pointer bump: `execute_block_pipeline` takes
714                        // ownership, but the header-commitment check below still needs
715                        // the input BAL on the parallel path (produced_bal == None).
716                        let header_bal = bal.clone();
717                        let result = vm.execute_block_pipeline(
718                            block,
719                            tx,
720                            queue_length_ref,
721                            bal,
722                            bal_parallel_exec_enabled,
723                        );
724                        cancelled_ref.store(true, Ordering::Relaxed);
725                        let (execution_result, produced_bal) = result?;
726
727                        // Validate execution went alright
728                        if let Err(e) =
729                            validate_gas_used(execution_result.block_gas_used, &block.header)
730                        {
731                            ethrex_vm::log_gas_used_mismatch(
732                                &execution_result.tx_gas_breakdowns,
733                                block.header.number,
734                                execution_result.block_gas_used,
735                                block.header.gas_used,
736                            );
737                            return Err(e.into());
738                        }
739                        validate_receipts_root_and_logs_bloom(
740                            &block.header,
741                            &execution_result.receipts,
742                            &NativeCrypto,
743                        )?;
744                        validate_requests_hash(
745                            &block.header,
746                            &chain_config,
747                            &execution_result.requests,
748                        )?;
749                        // EIP-7928 block_access_list_hash commitment check.
750                        //
751                        // Sequential Amsterdam path: rebuilds a BAL and returns
752                        // Some(produced_bal), so the full hash+index+size check runs here.
753                        //
754                        // Parallel Amsterdam path: uses the header BAL directly to drive
755                        // execution and returns produced_bal = None. The header BAL's
756                        // index/size are already validated inside execute_block_pipeline,
757                        // and content-equivalence (unread_storage_reads /
758                        // unaccessed_pure_accounts) plus the state_root comparison prove the
759                        // header BAL is the canonical one. The one thing those checks do NOT
760                        // bind is the header commitment itself, so we must compare
761                        // keccak(rlp(header_bal)) against header.block_access_list_hash here;
762                        // otherwise a block with a content-valid BAL but a forged commitment
763                        // is accepted on this path while every spec-conformant client (and
764                        // our own sequential/batch paths) rejects it. This is a pure hash
765                        // compare on a BAL already in memory; the parallel exec optimization
766                        // (no BAL rebuild) is preserved.
767                        //
768                        // Pre-Amsterdam blocks never record a BAL, so both arms are skipped.
769                        if let Some(bal) = &produced_bal {
770                            validate_block_access_list_hash(
771                                &block.header,
772                                &chain_config,
773                                bal,
774                                block.body.transactions.len(),
775                                &NativeCrypto,
776                            )?;
777                        } else if let Some(header_bal) = header_bal.as_deref()
778                            && chain_config.is_amsterdam_activated(block.header.timestamp)
779                            && !header_bal.matches_commitment(
780                                block.header.block_access_list_hash,
781                                &NativeCrypto,
782                            )
783                        {
784                            return Err(InvalidBlockError::BlockAccessListHashMismatch.into());
785                        }
786
787                        let exec_end_instant = Instant::now();
788                        Ok((execution_result, produced_bal, exec_end_instant))
789                    })
790                    .map_err(|e| {
791                        ChainError::Custom(format!("Failed to spawn execution thread: {e}"))
792                    })?;
793                let parent_header_ref = &parent_header; // Avoid moving to thread
794                // Merkleizer returns (list, streaming witness or None on BAL path, merkle_start, merkle_end).
795                type MerkleResult = Result<
796                    (
797                        AccountUpdatesList,
798                        Option<Vec<AccountUpdate>>,
799                        Instant,
800                        Instant,
801                    ),
802                    StoreError,
803                >;
804                let merkleize_handle = std::thread::Builder::new()
805                    .name("block_executor_merkleizer".to_string())
806                    .spawn_scoped(s, move || -> MerkleResult {
807                        let merkle_start_instant = Instant::now();
808                        // Merkleizer behavior MUST match the channel-creation decision above:
809                        // a channel is created (and execution streams per-tx updates into it via
810                        // `send_state_transitions_tx`) in every case except `bal=Some &&
811                        // parallel_exec`. So the optimistic synthesized-updates path is valid ONLY
812                        // when no channel exists (`rx_for_merkle` is None); whenever a channel was
813                        // created we must consume it. Taking the optimistic path while a channel is
814                        // live drops `rx` mid-execution and races execution's later sends (the
815                        // post-requests send especially), surfacing as "sending on a closed channel".
816                        let (account_updates_list, streaming_witness) = match rx_for_merkle {
817                            None => {
818                                let prepared = optimistic_updates.expect(
819                                    "optimistic updates are present when the streaming channel is absent",
820                                );
821                                let list = self.handle_merkleization_bal_from_updates(
822                                    prepared,
823                                    parent_header_ref,
824                                )?;
825                                // The merkleizer builds the trie from the BAL-synthesized
826                                // updates and ignores the streaming channel. But sequential
827                                // execution (`!bal_parallel_exec_enabled`) still streams per-tx
828                                // updates over `rx_for_merkle`; if we drop the receiver before
829                                // the executor's last send, that send fails with "sending on a
830                                // closed channel", racing the real validation error. Drain the
831                                // channel (the updates are redundant here — the BAL path is
832                                // authoritative) so the executor always completes cleanly.
833                                if let Some(rx) = rx_for_merkle {
834                                    for _ in rx {}
835                                }
836                                (list, None)
837                            }
838                            Some(rx) => self.handle_merkleization(
839                                rx,
840                                parent_header_ref,
841                                queue_length_ref,
842                                max_queue_length_ref,
843                                collect_witness,
844                            )?,
845                        };
846                        let merkle_end_instant = Instant::now();
847                        Ok((
848                            account_updates_list,
849                            streaming_witness,
850                            merkle_start_instant,
851                            merkle_end_instant,
852                        ))
853                    })
854                    .map_err(|e| {
855                        ChainError::Custom(format!("Failed to spawn merkleizer thread: {e}"))
856                    })?;
857                let execution_result = execution_handle.join().unwrap_or_else(|_| {
858                    Err(ChainError::Custom("execution thread panicked".to_string()))
859                });
860                let merkleization_result = merkleize_handle.join().unwrap_or_else(|_| {
861                    Err(StoreError::Custom(
862                        "merkleization thread panicked".to_string(),
863                    ))
864                });
865                #[cfg(all(feature = "rayon", not(feature = "eip-8025")))]
866                let warmer_duration = warm_handle
867                    .map(|handle| {
868                        handle
869                            .join()
870                            .inspect_err(|e| warn!("Warming thread error: {e:?}"))
871                            .ok()
872                            .unwrap_or(Duration::ZERO)
873                    })
874                    .unwrap_or(Duration::ZERO);
875                #[cfg(any(not(feature = "rayon"), feature = "eip-8025"))]
876                let warmer_duration = Duration::ZERO;
877                Ok((execution_result, merkleization_result, warmer_duration))
878            },
879        )?;
880        let (account_updates_list, streaming_witness, merkle_start_instant, merkle_end_instant) =
881            merkleization_result?;
882        let (execution_result, produced_bal, exec_end_instant) = execution_result?;
883
884        // Witness collection forces the streaming merkleizer (synthesized
885        // updates are disabled above), so the streaming witness is the only
886        // possible source of accumulated updates.
887        let accumulated_updates = streaming_witness;
888
889        let exec_merkle_end_instant = Instant::now();
890
891        Ok((
892            execution_result,
893            account_updates_list,
894            accumulated_updates,
895            produced_bal,
896            max_queue_length,
897            [
898                start_instant,
899                block_validated_instant,
900                exec_merkle_start,
901                merkle_start_instant,
902                exec_end_instant,
903                merkle_end_instant,
904                exec_merkle_end_instant,
905            ],
906            warmer_duration,
907        ))
908    }
909
910    #[instrument(
911        level = "trace",
912        name = "Trie update",
913        skip_all,
914        fields(namespace = "block_execution")
915    )]
916    fn handle_merkleization(
917        &self,
918        rx: Receiver<Vec<AccountUpdate>>,
919        parent_header: &BlockHeader,
920        queue_length: &AtomicUsize,
921        max_queue_length: &mut usize,
922        collect_witness: bool,
923    ) -> Result<(AccountUpdatesList, Option<Vec<AccountUpdate>>), StoreError> {
924        let parent_state_root = parent_header.state_root;
925
926        // Create 16 worker channels (crossbeam for select! support)
927        let mut workers_tx = Vec::with_capacity(16);
928        let mut workers_rx = Vec::with_capacity(16);
929        for _ in 0..16 {
930            let (tx, rx) = cb::unbounded();
931            workers_tx.push(tx);
932            workers_rx.push(rx);
933        }
934
935        // Shutdown channel: dropping shutdown_tx signals all workers to exit.
936        let (shutdown_tx, shutdown_rx) = cb::bounded::<()>(0);
937        // Done channel: workers report completion status.
938        let (done_tx, done_rx) = cb::unbounded::<Result<(), StoreError>>();
939
940        // Run workers + coordination on the persistent pool.
941        // Workers and watcher are spawned as pool tasks; the coordination logic
942        // (dispatching messages, collecting results) runs on the calling thread
943        // via in_place_scope, so it executes concurrently with the pool tasks.
944        let watcher_error: Arc<std::sync::Mutex<Option<StoreError>>> = Default::default();
945        let result = self.merkle_pool.in_place_scope(|s| {
946            // Spawn 16 unified workers (each gets clone of all 16 senders)
947            for (i, rx) in workers_rx.into_iter().enumerate() {
948                let all_senders = workers_tx.clone();
949                let storage_clone = self.storage.clone();
950                let shutdown_rx = shutdown_rx.clone();
951                let done_tx = done_tx.clone();
952                s.spawn(move |_| {
953                    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
954                        handle_subtrie(
955                            storage_clone,
956                            rx,
957                            parent_state_root,
958                            i as u8,
959                            all_senders,
960                            shutdown_rx,
961                        )
962                    }));
963                    let result = match result {
964                        Ok(r) => r,
965                        Err(_) => Err(StoreError::Custom(format!("shard worker {i} panicked"))),
966                    };
967                    if let Err(cb::SendError(Err(e))) = done_tx.send(result) {
968                        error!("Failed to send worker {i} error to watcher: {e}");
969                    }
970                });
971            }
972            drop(done_tx); // Only workers hold senders
973            drop(shutdown_rx); // Only workers hold receivers
974
975            // Watcher task: drops shutdown_tx on first worker error to signal
976            // all remaining workers, preventing deadlock on gatherer_rx.
977            let watcher_error = watcher_error.clone();
978            s.spawn(move |_| {
979                let _shutdown = shutdown_tx;
980                for result in done_rx {
981                    if let Err(e) = result {
982                        // Store error for the caller, then drop _shutdown to signal workers.
983                        *watcher_error.lock().expect("watcher mutex poisoned") = Some(e);
984                        return;
985                    }
986                }
987            });
988
989            // Coordination runs on the calling thread, concurrently with pool tasks.
990            let mut code_updates: Vec<(H256, Code)> = vec![];
991            let mut hashed_address_cache: FxHashMap<Address, H256> = Default::default();
992            let mut has_storage: FxHashSet<H256> = Default::default();
993
994            let mut accumulator: Option<FxHashMap<Address, AccountUpdate>> =
995                collect_witness.then(FxHashMap::default);
996
997            for updates in rx {
998                let current_length = queue_length.fetch_sub(1, Ordering::Acquire);
999                *max_queue_length = current_length.max(*max_queue_length);
1000                // Accumulate updates for witness generation if enabled
1001                if let Some(acc) = &mut accumulator {
1002                    for update in updates.clone() {
1003                        match acc.entry(update.address) {
1004                            Entry::Vacant(e) => {
1005                                e.insert(update);
1006                            }
1007                            Entry::Occupied(mut e) => {
1008                                e.get_mut().merge(update);
1009                            }
1010                        }
1011                    }
1012                }
1013
1014                for update in updates {
1015                    let hashed_address = *hashed_address_cache
1016                        .entry(update.address)
1017                        .or_insert_with(|| keccak(update.address));
1018
1019                    let (info, code, storage) = if update.removed {
1020                        (Some(Default::default()), None, Default::default())
1021                    } else {
1022                        (update.info, update.code, update.added_storage)
1023                    };
1024
1025                    // Extract code for dispatcher-local collection
1026                    if let Some(ref info) = info
1027                        && let Some(code) = code
1028                    {
1029                        code_updates.push((info.code_hash, code));
1030                    }
1031
1032                    if update.removed || update.removed_storage || !storage.is_empty() {
1033                        has_storage.insert(hashed_address);
1034                    }
1035
1036                    let bucket = hashed_address.as_fixed_bytes()[0] >> 4;
1037                    workers_tx[bucket as usize]
1038                        .send(WorkerRequest::ProcessAccount {
1039                            prefix: hashed_address,
1040                            info,
1041                            storage,
1042                            removed: update.removed,
1043                            removed_storage: update.removed_storage,
1044                        })
1045                        .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
1046                }
1047            }
1048
1049            // Send FinishRouting — workers self-synchronize via RoutingDone exchange.
1050            for tx in &workers_tx {
1051                tx.send(WorkerRequest::FinishRouting)
1052                    .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
1053            }
1054
1055            // Send MerklizeAccounts for no-storage accounts.
1056            let mut early_batches: [Vec<H256>; 16] = Default::default();
1057            for hashed_account in hashed_address_cache.values() {
1058                if !has_storage.contains(hashed_account) {
1059                    let bucket = hashed_account.as_fixed_bytes()[0] >> 4;
1060                    early_batches[bucket as usize].push(*hashed_account);
1061                }
1062            }
1063            for (i, batch) in early_batches.into_iter().enumerate() {
1064                if !batch.is_empty() {
1065                    workers_tx[i]
1066                        .send(WorkerRequest::MerklizeAccounts { accounts: batch })
1067                        .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
1068                }
1069            }
1070
1071            // Send CollectState immediately — workers defer until collection is done.
1072            let mut storage_updates: Vec<(H256, Vec<TrieNode>)> = Default::default();
1073            let (gatherer_tx, gatherer_rx) = channel();
1074            for tx in &workers_tx {
1075                tx.send(WorkerRequest::CollectState {
1076                    tx: gatherer_tx.clone(),
1077                })
1078                .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
1079            }
1080            drop(gatherer_tx);
1081            drop(workers_tx);
1082
1083            let mut root = BranchNode::default();
1084            let mut state_updates = Vec::new();
1085            for CollectedStateMsg {
1086                index,
1087                subroot,
1088                state_nodes,
1089                storage_nodes,
1090            } in gatherer_rx
1091            {
1092                storage_updates.extend(storage_nodes);
1093                state_updates.extend(state_nodes);
1094                root.choices[index as usize] = subroot.choices[index as usize].clone();
1095            }
1096
1097            let collapsed = self.collapse_root_node(parent_header, None, root)?;
1098            let state_trie_hash = if let Some(root) = collapsed {
1099                let mut root = NodeRef::from(root);
1100                let hash = root.commit(Nibbles::default(), &mut state_updates, &NativeCrypto);
1101                let _ = DROP_SENDER.send(Box::new(root));
1102                hash.finalize(&NativeCrypto)
1103            } else {
1104                state_updates.push((Nibbles::default(), vec![RLP_NULL]));
1105                *EMPTY_TRIE_HASH
1106            };
1107
1108            let accumulated_updates = accumulator.map(|acc| acc.into_values().collect());
1109
1110            Ok((
1111                AccountUpdatesList {
1112                    state_trie_hash,
1113                    state_updates,
1114                    storage_updates,
1115                    code_updates,
1116                },
1117                accumulated_updates,
1118            ))
1119        });
1120
1121        // Surface any worker errors captured by the watcher task.
1122        if let Some(err) = watcher_error.lock().expect("watcher mutex poisoned").take() {
1123            return Err(err);
1124        }
1125
1126        result
1127    }
1128
1129    /// Validation path synthesizes `BalSynthesisItem`s from the input BAL pre-execution and
1130    /// merkleizes optimistically in parallel with EVM execution. Two gates guard the result:
1131    /// (1) the EIP-7928 `block_access_list_hash` commitment check, and
1132    /// (2) the downstream `state_root` comparison against the block header. The parallel
1133    /// path returns `produced_bal = None` (the header BAL drives execution rather than being
1134    /// rebuilt), so gate (1) compares `keccak(rlp(header_bal))` against the header commitment
1135    /// directly in the execution thread; the sequential path runs the same gate against the
1136    /// rebuilt BAL. On any mismatch the optimistic merkle output is discarded via `?` on the
1137    /// execution thread's join result.
1138    #[instrument(
1139        level = "trace",
1140        name = "Trie update (BAL)",
1141        skip_all,
1142        fields(namespace = "block_execution")
1143    )]
1144    fn handle_merkleization_bal_from_updates(
1145        &self,
1146        prepared: FxHashMap<Address, BalSynthesisItem>,
1147        parent_header: &BlockHeader,
1148    ) -> Result<AccountUpdatesList, StoreError> {
1149        const NUM_WORKERS: usize = 16;
1150        // Accounts with at least this many storage slot updates are sharded
1151        // across 16 workers inside `compute_sharded_storage_root` instead of
1152        // being handled by Stage B. When hot_indices is empty the Stage B path
1153        // is unchanged.
1154        const STORAGE_SHARD_THRESHOLD: usize = 2048;
1155        let parent_state_root = parent_header.state_root;
1156
1157        // Build code updates and work items with pre-hashed addresses from the
1158        // pre-synthesized map. No Stage A drain needed: the synthesis happened
1159        // pre-scope at the call site.
1160        let mut code_updates: Vec<(H256, Code)> = Vec::new();
1161        let mut accounts: Vec<(H256, BalSynthesisItem)> = Vec::with_capacity(prepared.len());
1162        for (addr, item) in prepared {
1163            let hashed = keccak(addr);
1164            if let Some(ch) = item.code_hash
1165                && let Some(ref code) = item.code
1166            {
1167                code_updates.push((ch, code.clone()));
1168            }
1169            accounts.push((hashed, item));
1170        }
1171
1172        // === Stage B: Parallel per-account storage root computation ===
1173
1174        // Partition accounts: those with >= STORAGE_SHARD_THRESHOLD slots are
1175        // handled after Stage B via `compute_sharded_storage_root` (hot_indices).
1176        // The rest follow the normal Stage B greedy bin-packing path (normal_indices).
1177        // When hot_indices is empty the behavior is byte-identical to the previous code.
1178        let mut normal_indices: Vec<usize> = Vec::new();
1179        let mut hot_indices: Vec<usize> = Vec::new();
1180        for (i, (_, item)) in accounts.iter().enumerate() {
1181            if item.added_storage.len() >= STORAGE_SHARD_THRESHOLD {
1182                hot_indices.push(i);
1183            } else {
1184                normal_indices.push(i);
1185            }
1186        }
1187
1188        // Sort by storage weight (descending) for greedy bin packing.
1189        // Every item with real Stage B work MUST have weight >= 1: the greedy
1190        // algorithm does `bin_weights[min] += weight`, so weight-0 items never
1191        // change the bin weight and `min_by_key` keeps returning the same bin,
1192        // piling ALL of them into a single worker.
1193        // Synthesis never sets `removed`/`removed_storage`, so weight is purely
1194        // based on storage slot count.
1195        let mut work_indices: Vec<(usize, usize)> = normal_indices
1196            .iter()
1197            .map(|&i| {
1198                let item = &accounts[i].1;
1199                let weight = if !item.added_storage.is_empty() {
1200                    1.max(item.added_storage.len())
1201                } else {
1202                    0
1203                };
1204                (i, weight)
1205            })
1206            .collect();
1207        work_indices.sort_unstable_by(|a, b| b.1.cmp(&a.1));
1208
1209        // Greedy bin packing into NUM_WORKERS bins
1210        let mut bins: Vec<Vec<usize>> = (0..NUM_WORKERS).map(|_| Vec::new()).collect();
1211        let mut bin_weights: Vec<usize> = vec![0; NUM_WORKERS];
1212        for (idx, weight) in work_indices {
1213            let min_bin = bin_weights
1214                .iter()
1215                .enumerate()
1216                .min_by_key(|(_, w)| **w)
1217                .expect("bin_weights is non-empty")
1218                .0;
1219            bins[min_bin].push(idx);
1220            bin_weights[min_bin] += weight;
1221        }
1222
1223        // Compute storage roots in parallel
1224        let mut storage_roots: Vec<Option<H256>> = vec![None; accounts.len()];
1225        let mut storage_updates: Vec<(H256, Vec<TrieNode>)> = Vec::new();
1226
1227        std::thread::scope(|s| -> Result<(), StoreError> {
1228            let accounts_ref = &accounts;
1229            let handles: Vec<_> = bins
1230                .into_iter()
1231                .enumerate()
1232                .filter_map(|(worker_id, bin)| {
1233                    if bin.is_empty() {
1234                        return None;
1235                    }
1236                    Some(
1237                        std::thread::Builder::new()
1238                            .name(format!("bal_storage_worker_{worker_id}"))
1239                            .spawn_scoped(
1240                                s,
1241                                move || -> Result<Vec<(usize, H256, Vec<TrieNode>)>, StoreError> {
1242                                    let mut results: Vec<(usize, H256, Vec<TrieNode>)> = Vec::new();
1243                                    // Open one state trie per worker for storage root lookups
1244                                    let state_trie =
1245                                        self.storage.open_state_trie(parent_state_root)?;
1246                                    for idx in bin {
1247                                        let (hashed_address, item) = &accounts_ref[idx];
1248                                        if item.added_storage.is_empty() {
1249                                            continue;
1250                                        }
1251
1252                                        let storage_root = match state_trie
1253                                            .get(hashed_address.as_bytes())?
1254                                        {
1255                                            Some(rlp) => AccountState::decode(&rlp)?.storage_root,
1256                                            None => *EMPTY_TRIE_HASH,
1257                                        };
1258                                        let mut trie = self.storage.open_storage_trie(
1259                                            *hashed_address,
1260                                            parent_state_root,
1261                                            storage_root,
1262                                        )?;
1263
1264                                        // Pre-hash and sort by trie path so per-slot inserts
1265                                        // walk the node arena in order, improving cache locality.
1266                                        let mut hashed_storage: Vec<(H256, U256)> = item
1267                                            .added_storage
1268                                            .iter()
1269                                            .map(|(k, v)| (keccak(k), *v))
1270                                            .collect();
1271                                        hashed_storage.sort_unstable_by(|a, b| a.0.cmp(&b.0));
1272                                        for (hashed_key, value) in &hashed_storage {
1273                                            if value.is_zero() {
1274                                                trie.remove(hashed_key.as_bytes())?;
1275                                            } else {
1276                                                trie.insert(
1277                                                    hashed_key.as_bytes().to_vec(),
1278                                                    value.encode_to_vec(),
1279                                                )?;
1280                                            }
1281                                        }
1282
1283                                        let (root_hash, nodes) =
1284                                            trie.collect_changes_since_last_hash(&NativeCrypto);
1285                                        results.push((idx, root_hash, nodes));
1286                                    }
1287                                    Ok(results)
1288                                },
1289                            )
1290                            .map_err(|e| StoreError::Custom(format!("spawn failed: {e}"))),
1291                    )
1292                })
1293                .collect::<Result<Vec<_>, _>>()?;
1294
1295            for handle in handles {
1296                let results = handle
1297                    .join()
1298                    .map_err(|_| StoreError::Custom("storage worker panicked".to_string()))??;
1299                for (idx, root_hash, nodes) in results {
1300                    storage_roots[idx] = Some(root_hash);
1301                    storage_updates.push((accounts_ref[idx].0, nodes));
1302                }
1303            }
1304            Ok(())
1305        })?;
1306
1307        // === Stage B.5: Sharded storage root for hot accounts ===
1308        // Each call to compute_sharded_storage_root already saturates 16 cores,
1309        // so we process hot accounts sequentially. A parallel outer loop is a
1310        // future option if multi-hot-account blocks appear.
1311        // Skip the state-trie open entirely on the common path (no hot accounts).
1312        if !hot_indices.is_empty() {
1313            let state_trie = self.storage.open_state_trie(parent_state_root)?;
1314            for idx in &hot_indices {
1315                let idx = *idx;
1316                let (hashed_address, item) = &accounts[idx];
1317                let storage_root = match state_trie.get(hashed_address.as_bytes())? {
1318                    Some(rlp) => AccountState::decode(&rlp)?.storage_root,
1319                    None => *EMPTY_TRIE_HASH,
1320                };
1321                // Slots are sorted by hashed key inside compute_sharded_storage_root
1322                // to match Stage B's insert order (cache locality + identical node set).
1323                let hashed_storage: Vec<(H256, U256)> = item
1324                    .added_storage
1325                    .iter()
1326                    .map(|(k, v)| (keccak(k), *v))
1327                    .collect();
1328                let (root_hash, nodes) = compute_sharded_storage_root(
1329                    &self.storage,
1330                    parent_state_root,
1331                    *hashed_address,
1332                    storage_root,
1333                    &hashed_storage,
1334                )?;
1335                storage_roots[idx] = Some(root_hash);
1336                storage_updates.push((*hashed_address, nodes));
1337            }
1338        }
1339
1340        // === Stage C: State trie update via 16 shard workers ===
1341
1342        // Build per-shard work items
1343        let mut shards: Vec<Vec<BalStateWorkItem>> = (0..NUM_WORKERS).map(|_| Vec::new()).collect();
1344        for (idx, (hashed_address, item)) in accounts.iter().enumerate() {
1345            let bucket = (hashed_address.as_fixed_bytes()[0] >> 4) as usize;
1346            shards[bucket].push(BalStateWorkItem {
1347                hashed_address: *hashed_address,
1348                nonce: item.nonce,
1349                balance: item.balance,
1350                code_hash: item.code_hash,
1351                storage_root: storage_roots[idx],
1352            });
1353        }
1354
1355        let mut root = BranchNode::default();
1356        let mut state_updates = Vec::new();
1357
1358        // All 16 shard threads must run, even for empty shards: each worker
1359        // opens the parent state trie and returns its existing subtree so the
1360        // root can be correctly assembled via `collect_trie`. Skipping unchanged
1361        // shards (unlike Stage B's filter_map) would leave holes in the root.
1362        std::thread::scope(|s| -> Result<(), StoreError> {
1363            let handles: Vec<_> = shards
1364                .into_iter()
1365                .enumerate()
1366                .map(|(index, shard_items)| {
1367                    std::thread::Builder::new()
1368                        .name(format!("bal_state_shard_{index}"))
1369                        .spawn_scoped(
1370                            s,
1371                            move || -> Result<(Box<BranchNode>, Vec<TrieNode>), StoreError> {
1372                                let mut state_trie =
1373                                    self.storage.open_state_trie(parent_state_root)?;
1374
1375                                for item in &shard_items {
1376                                    let path = item.hashed_address.as_bytes();
1377
1378                                    // Load existing account state
1379                                    let mut account_state = match state_trie.get(path)? {
1380                                        Some(rlp) => {
1381                                            let state = AccountState::decode(&rlp)?;
1382                                            // Re-insert to materialize the trie path so
1383                                            // collect_changes_since_last_hash includes this
1384                                            // node in the diff (needed for both updates and
1385                                            // removals via collect_trie).
1386                                            state_trie.insert(path.to_vec(), rlp)?;
1387                                            state
1388                                        }
1389                                        None => AccountState::default(),
1390                                    };
1391
1392                                    if let Some(n) = item.nonce {
1393                                        account_state.nonce = n;
1394                                    }
1395                                    if let Some(b) = item.balance {
1396                                        account_state.balance = b;
1397                                    }
1398                                    if let Some(ch) = item.code_hash {
1399                                        account_state.code_hash = ch;
1400                                    }
1401                                    if let Some(storage_root) = item.storage_root {
1402                                        account_state.storage_root = storage_root;
1403                                    }
1404
1405                                    // EIP-161: remove empty accounts (zero nonce, zero balance,
1406                                    // empty code, empty storage) from the state trie.
1407                                    if account_state != AccountState::default() {
1408                                        state_trie
1409                                            .insert(path.to_vec(), account_state.encode_to_vec())?;
1410                                    } else {
1411                                        state_trie.remove(path)?;
1412                                    }
1413                                }
1414
1415                                collect_trie(index as u8, state_trie)
1416                                    .map_err(|e| StoreError::Custom(format!("{e}")))
1417                            },
1418                        )
1419                        .map_err(|e| StoreError::Custom(format!("spawn failed: {e}")))
1420                })
1421                .collect::<Result<Vec<_>, _>>()?;
1422
1423            for (i, handle) in handles.into_iter().enumerate() {
1424                let (subroot, state_nodes) = handle
1425                    .join()
1426                    .map_err(|_| StoreError::Custom("state shard worker panicked".to_string()))??;
1427                state_updates.extend(state_nodes);
1428                root.choices[i] = subroot.choices[i].clone();
1429            }
1430            Ok(())
1431        })?;
1432
1433        // === Stage D: Finalize root ===
1434        let state_trie_hash =
1435            if let Some(root) = self.collapse_root_node(parent_header, None, root)? {
1436                let mut root = NodeRef::from(root);
1437                let hash = root.commit(Nibbles::default(), &mut state_updates, &NativeCrypto);
1438                let _ = DROP_SENDER.send(Box::new(root));
1439                hash.finalize(&NativeCrypto)
1440            } else {
1441                state_updates.push((Nibbles::default(), vec![RLP_NULL]));
1442                *EMPTY_TRIE_HASH
1443            };
1444
1445        Ok(AccountUpdatesList {
1446            state_trie_hash,
1447            state_updates,
1448            storage_updates,
1449            code_updates,
1450        })
1451    }
1452
1453    fn collapse_root_node(
1454        &self,
1455        parent_header: &BlockHeader,
1456        prefix: Option<H256>,
1457        root: BranchNode,
1458    ) -> Result<Option<Node>, StoreError> {
1459        collapse_root_node(&self.storage, parent_header.state_root, prefix, root)
1460    }
1461
1462    /// Executes a block from a given vm instance an does not clear its state
1463    fn execute_block_from_state(
1464        &self,
1465        parent_header: &BlockHeader,
1466        block: &Block,
1467        chain_config: &ChainConfig,
1468        vm: &mut Evm,
1469    ) -> Result<BlockExecutionResult, ChainError> {
1470        // Validate the block pre-execution
1471        validate_block_pre_execution(block, parent_header, chain_config, ELASTICITY_MULTIPLIER)?;
1472        self.validate_l1_transaction_types(block)?;
1473        let (execution_result, bal) = vm.execute_block(block)?;
1474        // Validate execution went alright
1475        if let Err(e) = validate_gas_used(execution_result.block_gas_used, &block.header) {
1476            ethrex_vm::log_gas_used_mismatch(
1477                &execution_result.tx_gas_breakdowns,
1478                block.header.number,
1479                execution_result.block_gas_used,
1480                block.header.gas_used,
1481            );
1482            return Err(e.into());
1483        }
1484        validate_receipts_root_and_logs_bloom(
1485            &block.header,
1486            &execution_result.receipts,
1487            &NativeCrypto,
1488        )?;
1489        validate_requests_hash(&block.header, chain_config, &execution_result.requests)?;
1490        if let Some(bal) = &bal {
1491            validate_block_access_list_hash(
1492                &block.header,
1493                chain_config,
1494                bal,
1495                block.body.transactions.len(),
1496                &NativeCrypto,
1497            )?;
1498        }
1499
1500        Ok(execution_result)
1501    }
1502
1503    pub async fn generate_witness_for_blocks(
1504        &self,
1505        blocks: &[Block],
1506    ) -> Result<ExecutionWitness, ChainError> {
1507        self.generate_witness_for_blocks_with_fee_configs(blocks, None)
1508            .await
1509    }
1510
1511    pub async fn generate_witness_for_blocks_with_fee_configs(
1512        &self,
1513        blocks: &[Block],
1514        fee_configs: Option<&[FeeConfig]>,
1515    ) -> Result<ExecutionWitness, ChainError> {
1516        let first_block_header = &blocks
1517            .first()
1518            .ok_or(ChainError::WitnessGeneration(
1519                "Empty block batch".to_string(),
1520            ))?
1521            .header;
1522
1523        // Get state at previous block
1524        let trie = self
1525            .storage
1526            .state_trie(first_block_header.parent_hash)
1527            .map_err(|_| ChainError::ParentStateNotFound)?
1528            .ok_or(ChainError::ParentStateNotFound)?;
1529        let initial_state_root = trie.hash_no_commit(&NativeCrypto);
1530
1531        let (mut current_trie_witness, mut trie) = TrieLogger::open_trie(trie);
1532
1533        // For each block, a new TrieLogger will be opened, each containing the
1534        // witness accessed during the block execution. We need to accumulate
1535        // all the nodes accessed during the entire batch execution.
1536        let mut accumulated_state_trie_witness = current_trie_witness
1537            .lock()
1538            .map_err(|_| {
1539                ChainError::WitnessGeneration("Failed to lock state trie witness".to_string())
1540            })?
1541            .clone();
1542
1543        let mut touched_account_storage_slots = BTreeMap::new();
1544        // This will become the state trie + storage trie
1545        let mut used_trie_nodes = Vec::new();
1546
1547        // Store the root node in case the block is empty and the witness does not record any nodes
1548        let root_node = trie.root_node().map_err(|_| {
1549            ChainError::WitnessGeneration("Failed to get root state node".to_string())
1550        })?;
1551
1552        let mut blockhash_opcode_references = HashMap::new();
1553        let mut codes = Vec::new();
1554
1555        for (i, block) in blocks.iter().enumerate() {
1556            let parent_hash = block.header.parent_hash;
1557            let parent_header = self
1558                .storage
1559                .get_block_header_by_hash(parent_hash)
1560                .map_err(ChainError::StoreError)?
1561                .ok_or(ChainError::ParentNotFound)?;
1562
1563            // This assumes that the user has the necessary state stored already,
1564            // so if the user only has the state previous to the first block, it
1565            // will fail in the second iteration of this for loop. To ensure this,
1566            // doesn't fail, later in this function we store the new state after
1567            // re-execution.
1568            let vm_db: DynVmDatabase =
1569                Box::new(StoreVmDatabase::new(self.storage.clone(), parent_header)?);
1570
1571            let logger = Arc::new(DatabaseLogger::new(Arc::new(vm_db)));
1572
1573            let mut vm = match self.options.r#type {
1574                BlockchainType::L1 => {
1575                    Evm::new_from_db_for_l1(logger.clone(), Arc::new(NativeCrypto))
1576                }
1577                BlockchainType::L2(_) => {
1578                    let l2_config = match fee_configs {
1579                        Some(fee_configs) => {
1580                            fee_configs.get(i).ok_or(ChainError::WitnessGeneration(
1581                                "FeeConfig not found for witness generation".to_string(),
1582                            ))?
1583                        }
1584                        None => Err(ChainError::WitnessGeneration(
1585                            "L2Config not found for witness generation".to_string(),
1586                        ))?,
1587                    };
1588                    Evm::new_from_db_for_l2(logger.clone(), *l2_config, Arc::new(NativeCrypto))
1589                }
1590            };
1591
1592            // Re-execute block with logger
1593            let (execution_result, _bal) = vm.execute_block(block)?;
1594
1595            // Gather account updates
1596            let account_updates = vm.get_state_transitions()?;
1597
1598            let mut state_accessed = logger
1599                .state_accessed
1600                .lock()
1601                .map_err(|_e| {
1602                    ChainError::WitnessGeneration("Failed to execute with witness".to_string())
1603                })?
1604                .clone();
1605
1606            // Deduplicate storage keys while preserving access order
1607            for keys in state_accessed.values_mut() {
1608                let mut seen = HashSet::new();
1609                keys.retain(|k| seen.insert(*k));
1610            }
1611
1612            for (account, acc_keys) in state_accessed.iter() {
1613                let slots: &mut Vec<H256> =
1614                    touched_account_storage_slots.entry(*account).or_default();
1615                slots.extend(acc_keys.iter().copied());
1616            }
1617
1618            // Get the used block hashes from the logger
1619            let logger_block_hashes = logger
1620                .block_hashes_accessed
1621                .lock()
1622                .map_err(|_e| {
1623                    ChainError::WitnessGeneration("Failed to get block hashes".to_string())
1624                })?
1625                .clone();
1626
1627            blockhash_opcode_references.extend(logger_block_hashes);
1628
1629            // Access all the accounts needed for withdrawals
1630            if let Some(withdrawals) = block.body.withdrawals.as_ref() {
1631                for withdrawal in withdrawals {
1632                    trie.get(&hash_address(&withdrawal.address)).map_err(|_e| {
1633                        ChainError::Custom("Failed to access account from trie".to_string())
1634                    })?;
1635                }
1636            }
1637
1638            let mut used_storage_tries = HashMap::new();
1639
1640            // Access all the accounts from the initial trie
1641            // Record all the storage nodes for the initial state
1642            for (account, acc_keys) in state_accessed.iter() {
1643                // Access the account from the state trie to record the nodes used to access it
1644                trie.get(&hash_address(account)).map_err(|_e| {
1645                    ChainError::WitnessGeneration("Failed to access account from trie".to_string())
1646                })?;
1647                // Get storage trie at before updates
1648                if !acc_keys.is_empty()
1649                    && let Ok(Some(storage_trie)) = self.storage.storage_trie(parent_hash, *account)
1650                {
1651                    let (storage_trie_witness, storage_trie) = TrieLogger::open_trie(storage_trie);
1652                    // Access all the keys
1653                    for storage_key in acc_keys {
1654                        let hashed_key = hash_key(storage_key);
1655                        storage_trie.get(&hashed_key).map_err(|_e| {
1656                            ChainError::WitnessGeneration(
1657                                "Failed to access storage key".to_string(),
1658                            )
1659                        })?;
1660                    }
1661                    // Store the tries to reuse when applying account updates
1662                    used_storage_tries.insert(*account, (storage_trie_witness, storage_trie));
1663                }
1664            }
1665
1666            // Store all the accessed evm bytecodes
1667            for code_hash in logger
1668                .code_accessed
1669                .lock()
1670                .map_err(|_e| {
1671                    ChainError::WitnessGeneration("Failed to gather used bytecodes".to_string())
1672                })?
1673                .iter()
1674            {
1675                let code = self
1676                    .storage
1677                    .get_account_code(*code_hash)
1678                    .map_err(|_e| {
1679                        ChainError::WitnessGeneration("Failed to get account code".to_string())
1680                    })?
1681                    .ok_or(ChainError::WitnessGeneration(
1682                        "Failed to get account code".to_string(),
1683                    ))?;
1684                codes.push(code.code().to_vec());
1685            }
1686
1687            // Apply account updates to the trie recording all the necessary nodes to do so
1688            let (storage_tries_after_update, account_updates_list) =
1689                self.storage.apply_account_updates_from_trie_with_witness(
1690                    trie,
1691                    &account_updates,
1692                    used_storage_tries,
1693                )?;
1694
1695            // We cannot ensure that the users of this function have the necessary
1696            // state stored, so in order for it to not assume anything, we update
1697            // the storage with the new state after re-execution
1698            self.store_block(block.clone(), account_updates_list, execution_result)?;
1699
1700            for (address, (witness, _storage_trie)) in storage_tries_after_update {
1701                let mut witness = witness.lock().map_err(|_| {
1702                    ChainError::WitnessGeneration("Failed to lock storage trie witness".to_string())
1703                })?;
1704                let witness = std::mem::take(&mut *witness);
1705                let witness = witness.into_values().collect::<Vec<_>>();
1706                used_trie_nodes.extend_from_slice(&witness);
1707                touched_account_storage_slots.entry(address).or_default();
1708            }
1709
1710            let (new_state_trie_witness, updated_trie) = TrieLogger::open_trie(
1711                self.storage
1712                    .state_trie(block.header.hash())
1713                    .map_err(|_| ChainError::ParentStateNotFound)?
1714                    .ok_or(ChainError::ParentStateNotFound)?,
1715            );
1716
1717            // Use the updated state trie for the next block
1718            trie = updated_trie;
1719
1720            for state_trie_witness in current_trie_witness
1721                .lock()
1722                .map_err(|_| {
1723                    ChainError::WitnessGeneration("Failed to lock state trie witness".to_string())
1724                })?
1725                .iter()
1726            {
1727                accumulated_state_trie_witness
1728                    .insert(*state_trie_witness.0, state_trie_witness.1.clone());
1729            }
1730
1731            current_trie_witness = new_state_trie_witness;
1732        }
1733
1734        used_trie_nodes.extend_from_slice(&Vec::from_iter(
1735            accumulated_state_trie_witness.into_values(),
1736        ));
1737
1738        // If the witness is empty at least try to store the root
1739        if used_trie_nodes.is_empty()
1740            && let Some(root) = root_node
1741        {
1742            used_trie_nodes.push((*root).clone());
1743        }
1744
1745        // - We now need necessary block headers, these go from the first block referenced (via BLOCKHASH or just the first block to execute) up to the parent of the last block to execute.
1746        let mut block_headers_bytes = Vec::new();
1747
1748        let first_blockhash_opcode_number = blockhash_opcode_references.keys().min();
1749        let first_needed_block_hash = first_blockhash_opcode_number
1750            .and_then(|n| {
1751                (*n < first_block_header.number.saturating_sub(1))
1752                    .then(|| blockhash_opcode_references.get(n))?
1753                    .copied()
1754            })
1755            .unwrap_or(first_block_header.parent_hash);
1756
1757        // At the beginning this is the header of the last block to execute.
1758        let mut current_header = blocks
1759            .last()
1760            .ok_or_else(|| ChainError::WitnessGeneration("Empty batch".to_string()))?
1761            .header
1762            .clone();
1763
1764        // Headers from latest - 1 until we reach first block header we need.
1765        // We do it this way because we want to fetch headers by hash, not by number
1766        while current_header.hash() != first_needed_block_hash {
1767            let parent_hash = current_header.parent_hash;
1768            let current_number = current_header.number - 1;
1769
1770            current_header = self
1771                .storage
1772                .get_block_header_by_hash(parent_hash)?
1773                .ok_or_else(|| {
1774                    ChainError::WitnessGeneration(format!(
1775                        "Failed to get block {current_number} header"
1776                    ))
1777                })?;
1778
1779            block_headers_bytes.push(current_header.encode_to_vec());
1780        }
1781
1782        // Get initial state trie root and embed the rest of the trie into it
1783        let nodes: BTreeMap<H256, Node> = used_trie_nodes
1784            .into_iter()
1785            .map(|node| {
1786                (
1787                    node.compute_hash(&NativeCrypto).finalize(&NativeCrypto),
1788                    node,
1789                )
1790            })
1791            .collect();
1792        let state_trie_root = if let NodeRef::Node(state_trie_root, _) =
1793            Trie::get_embedded_root(&nodes, initial_state_root)?
1794        {
1795            Some((*state_trie_root).clone())
1796        } else {
1797            None
1798        };
1799
1800        // Get all initial storage trie roots and embed the rest of the trie into it
1801        let state_trie = if let Some(state_trie_root) = &state_trie_root {
1802            Trie::new_temp_with_root(state_trie_root.clone().into())
1803        } else {
1804            Trie::new_temp()
1805        };
1806        let mut storage_trie_roots = BTreeMap::new();
1807        for address in touched_account_storage_slots.keys() {
1808            let hashed_address = hash_address(address);
1809            let hashed_address_h256 = H256::from_slice(&hashed_address);
1810            let Some(encoded_account) = state_trie.get(&hashed_address)? else {
1811                continue; // empty account, doesn't have a storage trie
1812            };
1813            let storage_root_hash = AccountState::decode(&encoded_account)?.storage_root;
1814            if storage_root_hash == *EMPTY_TRIE_HASH {
1815                continue; // empty storage trie
1816            }
1817            if !nodes.contains_key(&storage_root_hash) {
1818                continue; // storage trie isn't relevant to this execution
1819            }
1820            let node = Trie::get_embedded_root(&nodes, storage_root_hash)?;
1821            let NodeRef::Node(node, _) = node else {
1822                return Err(ChainError::Custom(
1823                    "execution witness does not contain non-empty storage trie".to_string(),
1824                ));
1825            };
1826            storage_trie_roots.insert(hashed_address_h256, (*node).clone());
1827        }
1828
1829        Ok(ExecutionWitness {
1830            codes,
1831            block_headers_bytes,
1832            first_block_number: first_block_header.number,
1833            chain_config: self.storage.get_chain_config(),
1834            state_trie_root,
1835            storage_trie_roots,
1836        })
1837    }
1838
1839    pub fn generate_witness_from_account_updates(
1840        &self,
1841        account_updates: Vec<AccountUpdate>,
1842        block: &Block,
1843        parent_header: BlockHeader,
1844        logger: &DatabaseLogger,
1845    ) -> Result<ExecutionWitness, ChainError> {
1846        // Get state at previous block
1847        let trie = self
1848            .storage
1849            .state_trie(parent_header.hash())
1850            .map_err(|_| ChainError::ParentStateNotFound)?
1851            .ok_or(ChainError::ParentStateNotFound)?;
1852        let initial_state_root = trie.hash_no_commit(&NativeCrypto);
1853
1854        let (trie_witness, trie) = TrieLogger::open_trie(trie);
1855
1856        let mut touched_account_storage_slots = BTreeMap::new();
1857        // This will become the state trie + storage trie
1858        let mut used_trie_nodes = Vec::new();
1859
1860        // Store the root node in case the block is empty and the witness does not record any nodes
1861        let root_node = trie.root_node().map_err(|_| {
1862            ChainError::WitnessGeneration("Failed to get root state node".to_string())
1863        })?;
1864
1865        let mut codes = Vec::new();
1866
1867        for account_update in &account_updates {
1868            touched_account_storage_slots.insert(
1869                account_update.address,
1870                account_update
1871                    .added_storage
1872                    .keys()
1873                    .cloned()
1874                    .collect::<Vec<H256>>(),
1875            );
1876        }
1877
1878        // Get the used block hashes from the logger
1879        let blockhash_opcode_references = logger
1880            .block_hashes_accessed
1881            .lock()
1882            .map_err(|_e| ChainError::WitnessGeneration("Failed to get block hashes".to_string()))?
1883            .clone();
1884
1885        // Access all the accounts needed for withdrawals
1886        if let Some(withdrawals) = block.body.withdrawals.as_ref() {
1887            for withdrawal in withdrawals {
1888                trie.get(&hash_address(&withdrawal.address)).map_err(|_e| {
1889                    ChainError::Custom("Failed to access account from trie".to_string())
1890                })?;
1891            }
1892        }
1893
1894        let mut used_storage_tries = HashMap::new();
1895
1896        // Access all the accounts from the initial trie
1897        // Record all the storage nodes for the initial state
1898        for (account, acc_keys) in logger
1899            .state_accessed
1900            .lock()
1901            .map_err(|_e| {
1902                ChainError::WitnessGeneration("Failed to execute with witness".to_string())
1903            })?
1904            .iter()
1905        {
1906            // Access the account from the state trie to record the nodes used to access it
1907            trie.get(&hash_address(account)).map_err(|_e| {
1908                ChainError::WitnessGeneration("Failed to access account from trie".to_string())
1909            })?;
1910            // Get storage trie at before updates
1911            if !acc_keys.is_empty()
1912                && let Ok(Some(storage_trie)) =
1913                    self.storage.storage_trie(parent_header.hash(), *account)
1914            {
1915                let (storage_trie_witness, storage_trie) = TrieLogger::open_trie(storage_trie);
1916                // Access all the keys
1917                for storage_key in acc_keys {
1918                    let hashed_key = hash_key(storage_key);
1919                    storage_trie.get(&hashed_key).map_err(|_e| {
1920                        ChainError::WitnessGeneration("Failed to access storage key".to_string())
1921                    })?;
1922                }
1923                // Store the tries to reuse when applying account updates
1924                used_storage_tries.insert(*account, (storage_trie_witness, storage_trie));
1925            }
1926        }
1927
1928        // Store all the accessed evm bytecodes
1929        for code_hash in logger
1930            .code_accessed
1931            .lock()
1932            .map_err(|_e| {
1933                ChainError::WitnessGeneration("Failed to gather used bytecodes".to_string())
1934            })?
1935            .iter()
1936        {
1937            let code = self
1938                .storage
1939                .get_account_code(*code_hash)
1940                .map_err(|_e| {
1941                    ChainError::WitnessGeneration("Failed to get account code".to_string())
1942                })?
1943                .ok_or(ChainError::WitnessGeneration(
1944                    "Failed to get account code".to_string(),
1945                ))?;
1946            codes.push(code.code().to_vec());
1947        }
1948
1949        // Apply account updates to the trie recording all the necessary nodes to do so
1950        let (storage_tries_after_update, _account_updates_list) =
1951            self.storage.apply_account_updates_from_trie_with_witness(
1952                trie,
1953                &account_updates,
1954                used_storage_tries,
1955            )?;
1956
1957        for (address, (witness, _storage_trie)) in storage_tries_after_update {
1958            let mut witness = witness.lock().map_err(|_| {
1959                ChainError::WitnessGeneration("Failed to lock storage trie witness".to_string())
1960            })?;
1961            let witness = std::mem::take(&mut *witness);
1962            let witness = witness.into_values().collect::<Vec<_>>();
1963            used_trie_nodes.extend_from_slice(&witness);
1964            touched_account_storage_slots.entry(address).or_default();
1965        }
1966
1967        used_trie_nodes.extend_from_slice(&Vec::from_iter(
1968            trie_witness
1969                .lock()
1970                .map_err(|_| {
1971                    ChainError::WitnessGeneration("Failed to lock state trie witness".to_string())
1972                })?
1973                .clone()
1974                .into_values(),
1975        ));
1976
1977        // If the witness is empty at least try to store the root
1978        if used_trie_nodes.is_empty()
1979            && let Some(root) = root_node
1980        {
1981            used_trie_nodes.push((*root).clone());
1982        }
1983
1984        // - We now need necessary block headers, these go from the first block referenced (via BLOCKHASH or just the first block to execute) up to the parent of the last block to execute.
1985        let mut block_headers_bytes = Vec::new();
1986
1987        let first_blockhash_opcode_number = blockhash_opcode_references.keys().min();
1988        let first_needed_block_hash = first_blockhash_opcode_number
1989            .and_then(|n| {
1990                (*n < block.header.number.saturating_sub(1))
1991                    .then(|| blockhash_opcode_references.get(n))?
1992                    .copied()
1993            })
1994            .unwrap_or(block.header.parent_hash);
1995
1996        let mut current_header = block.header.clone();
1997
1998        // Headers from latest - 1 until we reach first block header we need.
1999        // We do it this way because we want to fetch headers by hash, not by number
2000        while current_header.hash() != first_needed_block_hash {
2001            let parent_hash = current_header.parent_hash;
2002            let current_number = current_header.number - 1;
2003
2004            current_header = self
2005                .storage
2006                .get_block_header_by_hash(parent_hash)?
2007                .ok_or_else(|| {
2008                    ChainError::WitnessGeneration(format!(
2009                        "Failed to get block {current_number} header"
2010                    ))
2011                })?;
2012
2013            block_headers_bytes.push(current_header.encode_to_vec());
2014        }
2015
2016        // Get initial state trie root and embed the rest of the trie into it
2017        let nodes: BTreeMap<H256, Node> = used_trie_nodes
2018            .into_iter()
2019            .map(|node| {
2020                (
2021                    node.compute_hash(&NativeCrypto).finalize(&NativeCrypto),
2022                    node,
2023                )
2024            })
2025            .collect();
2026        let state_trie_root = if let NodeRef::Node(state_trie_root, _) =
2027            Trie::get_embedded_root(&nodes, initial_state_root)?
2028        {
2029            Some((*state_trie_root).clone())
2030        } else {
2031            None
2032        };
2033
2034        // Get all initial storage trie roots and embed the rest of the trie into it
2035        let state_trie = if let Some(state_trie_root) = &state_trie_root {
2036            Trie::new_temp_with_root(state_trie_root.clone().into())
2037        } else {
2038            Trie::new_temp()
2039        };
2040        let mut storage_trie_roots = BTreeMap::new();
2041        for address in touched_account_storage_slots.keys() {
2042            let hashed_address = hash_address(address);
2043            let hashed_address_h256 = H256::from_slice(&hashed_address);
2044            let Some(encoded_account) = state_trie.get(&hashed_address)? else {
2045                continue; // empty account, doesn't have a storage trie
2046            };
2047            let storage_root_hash = AccountState::decode(&encoded_account)?.storage_root;
2048            if storage_root_hash == *EMPTY_TRIE_HASH {
2049                continue; // empty storage trie
2050            }
2051            if !nodes.contains_key(&storage_root_hash) {
2052                continue; // storage trie isn't relevant to this execution
2053            }
2054            let node = Trie::get_embedded_root(&nodes, storage_root_hash)?;
2055            let NodeRef::Node(node, _) = node else {
2056                return Err(ChainError::Custom(
2057                    "execution witness does not contain non-empty storage trie".to_string(),
2058                ));
2059            };
2060            storage_trie_roots.insert(hashed_address_h256, (*node).clone());
2061        }
2062
2063        Ok(ExecutionWitness {
2064            codes,
2065            block_headers_bytes,
2066            first_block_number: parent_header.number,
2067            chain_config: self.storage.get_chain_config(),
2068            state_trie_root,
2069            storage_trie_roots,
2070        })
2071    }
2072
2073    #[instrument(
2074        level = "trace",
2075        name = "Block DB update",
2076        skip_all,
2077        fields(namespace = "block_execution")
2078    )]
2079    pub fn store_block(
2080        &self,
2081        block: Block,
2082        account_updates_list: AccountUpdatesList,
2083        execution_result: BlockExecutionResult,
2084    ) -> Result<(), ChainError> {
2085        // Check state root matches the one in block header
2086        validate_state_root(&block.header, account_updates_list.state_trie_hash)?;
2087
2088        let update_batch = UpdateBatch {
2089            account_updates: account_updates_list.state_updates,
2090            storage_updates: account_updates_list.storage_updates,
2091            receipts: vec![(block.hash(), execution_result.receipts)],
2092            blocks: vec![block],
2093            code_updates: account_updates_list.code_updates,
2094            batch_mode: false,
2095        };
2096
2097        self.storage
2098            .store_block_updates(update_batch)
2099            .map_err(|e| e.into())
2100    }
2101
2102    pub fn add_block(&self, block: Block) -> Result<(), ChainError> {
2103        let since = Instant::now();
2104        let (res, updates) = self.execute_block(&block)?;
2105        let executed = Instant::now();
2106
2107        // Apply the account updates over the last block's state and compute the new state root
2108        let account_updates_list = self
2109            .storage
2110            .apply_account_updates_batch(block.header.parent_hash, &updates)?
2111            .ok_or(ChainError::ParentStateNotFound)?;
2112
2113        let (gas_used, gas_limit, block_number, transactions_count) = (
2114            block.header.gas_used,
2115            block.header.gas_limit,
2116            block.header.number,
2117            block.body.transactions.len(),
2118        );
2119
2120        let merkleized = Instant::now();
2121        let result = self.store_block(block, account_updates_list, res);
2122        let stored = Instant::now();
2123
2124        if self.options.perf_logs_enabled {
2125            Self::print_add_block_logs(
2126                gas_used,
2127                gas_limit,
2128                block_number,
2129                transactions_count,
2130                since,
2131                executed,
2132                merkleized,
2133                stored,
2134            );
2135        }
2136        result
2137    }
2138
2139    pub fn add_block_pipeline(
2140        &self,
2141        block: Block,
2142        bal: Option<Arc<BlockAccessList>>,
2143    ) -> Result<(), ChainError> {
2144        let (_, _, result) = self.add_block_pipeline_inner(block, bal, false)?;
2145        result
2146    }
2147
2148    /// Same as [`add_block_pipeline`] but also returns the BAL produced during execution.
2149    /// A BAL only exists from Amsterdam onward. On the parallel validation path the BAL
2150    /// comes from the header and drives execution rather than being rebuilt, so the
2151    /// returned value is `None`; the sequential path (block production or
2152    /// `--no-bal-parallel-exec`) rebuilds it and returns `Some(bal)`. Pre-Amsterdam blocks
2153    /// never record a BAL, so the returned value is always `None`.
2154    pub fn add_block_pipeline_bal(
2155        &self,
2156        block: Block,
2157        bal: Option<Arc<BlockAccessList>>,
2158    ) -> Result<Option<BlockAccessList>, ChainError> {
2159        let (produced_bal, _, result) = self.add_block_pipeline_inner(block, bal, false)?;
2160        result?;
2161        Ok(produced_bal)
2162    }
2163
2164    /// Same as [`add_block_pipeline`] but returns the execution witness produced
2165    /// while importing the block.
2166    pub fn add_block_pipeline_with_witness(
2167        &self,
2168        block: Block,
2169        bal: Option<Arc<BlockAccessList>>,
2170    ) -> Result<ExecutionWitness, ChainError> {
2171        let (_, witness, result) = self.add_block_pipeline_inner(block, bal, true)?;
2172        result?;
2173        witness.ok_or_else(|| {
2174            ChainError::WitnessGeneration(
2175                "forced witness collection completed without producing a witness".to_string(),
2176            )
2177        })
2178    }
2179
2180    /// Runs the full block pipeline (execute + merkleize + store).
2181    ///
2182    /// Returns a two-level Result:
2183    /// - Outer `Err`: pipeline couldn't start (e.g. parent header not found).
2184    /// - Inner `Result`: block storage outcome. The produced BAL is returned
2185    ///   even when storage fails, so callers like `add_block_pipeline_bal` can
2186    ///   retrieve it. Note: if *execution* itself fails (outer `Result`), the
2187    ///   BAL is not available.
2188    fn add_block_pipeline_inner(
2189        &self,
2190        block: Block,
2191        bal: Option<Arc<BlockAccessList>>,
2192        force_witness: bool,
2193    ) -> Result<AddBlockPipelineInnerResult, ChainError> {
2194        // Validate if it can be the new head and find the parent
2195        let Ok(parent_header) = find_parent_header(&block.header, &self.storage) else {
2196            // If the parent is not present, we store it as pending.
2197            self.storage.add_pending_block(block)?;
2198            return Err(ChainError::ParentNotFound);
2199        };
2200
2201        let should_store_witness = self.options.precompute_witnesses && self.is_synced();
2202        let collect_witness = should_store_witness || force_witness;
2203
2204        let (mut vm, logger) = if collect_witness {
2205            // If witness pre-generation is enabled, we wrap the db with a logger
2206            // to track state access (block hashes, storage keys, codes) during execution
2207            // avoiding the need to re-execute the block later.
2208            let vm_db: DynVmDatabase = Box::new(StoreVmDatabase::new(
2209                self.storage.clone(),
2210                parent_header.clone(),
2211            )?);
2212
2213            let logger = Arc::new(DatabaseLogger::new(Arc::new(vm_db)));
2214
2215            let vm = match self.options.r#type.clone() {
2216                BlockchainType::L1 => {
2217                    Evm::new_from_db_for_l1(logger.clone(), Arc::new(NativeCrypto))
2218                }
2219                BlockchainType::L2(l2_config) => Evm::new_from_db_for_l2(
2220                    logger.clone(),
2221                    *l2_config.fee_config.read().map_err(|_| {
2222                        EvmError::Custom("Fee config lock was poisoned".to_string())
2223                    })?,
2224                    Arc::new(NativeCrypto),
2225                ),
2226            };
2227            (vm, Some(logger))
2228        } else {
2229            let vm_db = StoreVmDatabase::new(self.storage.clone(), parent_header.clone())?;
2230            let vm = self.new_evm(vm_db)?;
2231            (vm, None)
2232        };
2233
2234        // Keep a copy of the input BAL Arc for the post-execution BAL store call below.
2235        // `execute_block_pipeline` takes ownership; this clone is a cheap pointer bump.
2236        let input_bal = bal.clone();
2237        let (
2238            res,
2239            account_updates_list,
2240            accumulated_updates,
2241            produced_bal,
2242            merkle_queue_length,
2243            instants,
2244            warmer_duration,
2245        ) = { self.execute_block_pipeline(&block, &parent_header, &mut vm, bal, collect_witness)? };
2246
2247        let (gas_used, gas_limit, block_number, transactions_count) = (
2248            block.header.gas_used,
2249            block.header.gas_limit,
2250            block.header.number,
2251            block.body.transactions.len(),
2252        );
2253        let block_hash = block.hash();
2254
2255        let mut witness = None;
2256        if let Some(logger) = logger
2257            && let Some(account_updates) = accumulated_updates
2258        {
2259            let block_hash = block.hash();
2260            let generated_witness = self.generate_witness_from_account_updates(
2261                account_updates,
2262                &block,
2263                parent_header,
2264                &logger,
2265            )?;
2266            match (should_store_witness, force_witness) {
2267                (true, true) => {
2268                    witness = Some(generated_witness.clone());
2269                    self.storage
2270                        .store_witness(block_hash, block_number, generated_witness)?;
2271                }
2272                (true, false) => {
2273                    self.storage
2274                        .store_witness(block_hash, block_number, generated_witness)?;
2275                }
2276                (false, true) => {
2277                    witness = Some(generated_witness);
2278                }
2279                (false, false) => {}
2280            }
2281        };
2282
2283        // Store the block's BAL so peers can request it later without re-execution.
2284        // On the parallel Amsterdam validation path the BAL is supplied via the header
2285        // and `produced_bal` is None, so fall back to the validated incoming `bal`.
2286        // Pre-Amsterdam blocks have no BAL on either source, so nothing is stored.
2287        if let Some(bal) = produced_bal.as_ref().or(input_bal.as_deref())
2288            && let Err(err) = self.storage.store_block_access_list(block_hash, bal)
2289        {
2290            warn!("Failed to store block access list for block {block_hash}: {err}");
2291        }
2292
2293        let result = self.store_block(block, account_updates_list, res);
2294
2295        let stored = Instant::now();
2296
2297        let instants = std::array::from_fn(move |i| {
2298            if i < instants.len() {
2299                instants[i]
2300            } else {
2301                stored
2302            }
2303        });
2304
2305        if self.options.perf_logs_enabled {
2306            Self::print_add_block_pipeline_logs(
2307                gas_used,
2308                gas_limit,
2309                block_number,
2310                block_hash,
2311                transactions_count,
2312                merkle_queue_length,
2313                warmer_duration,
2314                instants,
2315            );
2316        }
2317
2318        metrics!(
2319            if let Some(bal_ref) = produced_bal.as_ref().or(input_bal.as_deref()) {
2320                let account_count = bal_ref.accounts().len() as u64;
2321                let slot_count = bal_ref.item_count().saturating_sub(account_count);
2322                let size_bytes = bal_ref.length() as f64;
2323                METRICS_BAL.blocks_total.inc();
2324                METRICS_BAL.size_bytes.set(size_bytes);
2325                METRICS_BAL.size_bytes_histogram.observe(size_bytes);
2326                METRICS_BAL.account_count.set(account_count as i64);
2327                METRICS_BAL.slot_count.set(slot_count as i64);
2328            }
2329        );
2330
2331        Ok((produced_bal, witness, result))
2332    }
2333
2334    #[allow(clippy::too_many_arguments)]
2335    fn print_add_block_logs(
2336        gas_used: u64,
2337        gas_limit: u64,
2338        block_number: u64,
2339        transactions_count: usize,
2340        since: Instant,
2341        executed: Instant,
2342        merkleized: Instant,
2343        stored: Instant,
2344    ) {
2345        let interval = stored.duration_since(since).as_millis() as f64;
2346        if interval != 0f64 {
2347            let as_gigas = gas_used as f64 / 10_f64.powf(9_f64);
2348            let throughput = as_gigas / interval * 1000_f64;
2349
2350            metrics!(
2351                METRICS_BLOCKS.set_block_number(block_number);
2352                METRICS_BLOCKS.set_latest_gas_used(gas_used as f64);
2353                METRICS_BLOCKS.set_latest_block_gas_limit(gas_limit as f64);
2354                METRICS_BLOCKS.set_latest_gigagas(throughput);
2355                METRICS_BLOCKS.set_execution_ms(executed.duration_since(since).as_secs_f64() * 1000.0);
2356                METRICS_BLOCKS.set_merkle_ms(merkleized.duration_since(executed).as_secs_f64() * 1000.0);
2357                METRICS_BLOCKS.set_store_ms(stored.duration_since(merkleized).as_secs_f64() * 1000.0);
2358                METRICS_BLOCKS.set_transaction_count(transactions_count as i64);
2359            );
2360
2361            let base_log = format!(
2362                "[METRIC] BLOCK EXECUTION THROUGHPUT ({}): {:.3} Ggas/s TIME SPENT: {:.0} ms. Gas Used: {:.3} ({:.0}%), #Txs: {}.",
2363                block_number,
2364                throughput,
2365                interval,
2366                as_gigas,
2367                (gas_used as f64 / gas_limit as f64) * 100.0,
2368                transactions_count
2369            );
2370
2371            fn percentage(init: Instant, end: Instant, total: f64) -> f64 {
2372                (end.duration_since(init).as_millis() as f64 / total * 100.0).round()
2373            }
2374            let extra_log = if as_gigas > 0.0 {
2375                format!(
2376                    " exec: {}% merkle: {}% store: {}%",
2377                    percentage(since, executed, interval),
2378                    percentage(executed, merkleized, interval),
2379                    percentage(merkleized, stored, interval)
2380                )
2381            } else {
2382                "".to_string()
2383            };
2384            info!("{}{}", base_log, extra_log);
2385        }
2386    }
2387
2388    #[allow(clippy::too_many_arguments)]
2389    fn print_add_block_pipeline_logs(
2390        gas_used: u64,
2391        gas_limit: u64,
2392        block_number: u64,
2393        block_hash: H256,
2394        transactions_count: usize,
2395        merkle_queue_length: usize,
2396        warmer_duration: Duration,
2397        [
2398            start_instant,
2399            block_validated_instant,
2400            exec_merkle_start,
2401            merkle_start_instant,
2402            exec_end_instant,
2403            merkle_end_instant,
2404            exec_merkle_end_instant,
2405            stored_instant,
2406        ]: [Instant; 8],
2407    ) {
2408        let total_ms = stored_instant.duration_since(start_instant).as_secs_f64() * 1000.0;
2409        if total_ms == 0.0 {
2410            return;
2411        }
2412
2413        let as_mgas = gas_used as f64 / 1e6;
2414        let throughput = (gas_used as f64 / 1e9) / (total_ms / 1000.0);
2415
2416        // Calculate phase durations in ms
2417        let validate_ms = block_validated_instant
2418            .duration_since(start_instant)
2419            .as_secs_f64()
2420            * 1000.0;
2421        let exec_ms = exec_end_instant
2422            .duration_since(exec_merkle_start)
2423            .as_secs_f64()
2424            * 1000.0;
2425        let store_ms = stored_instant
2426            .duration_since(exec_merkle_end_instant)
2427            .as_secs_f64()
2428            * 1000.0;
2429        let warmer_ms = warmer_duration.as_secs_f64() * 1000.0;
2430
2431        // Calculate merkle breakdown
2432        // merkle_end_instant marks when merkle thread finished (may be before or after exec)
2433        // exec_merkle_end_instant marks when both exec and merkle are done
2434        let _merkle_total_ms = exec_merkle_end_instant
2435            .duration_since(exec_merkle_start)
2436            .as_secs_f64()
2437            * 1000.0;
2438
2439        // Concurrent merkle time: the portion of merkle that ran while exec was running
2440        let merkle_concurrent_ms = (merkle_end_instant
2441            .duration_since(exec_merkle_start)
2442            .as_secs_f64()
2443            * 1000.0)
2444            .min(exec_ms);
2445
2446        // Drain time: time spent finishing merkle after exec completed
2447        let merkle_drain_ms = exec_merkle_end_instant
2448            .saturating_duration_since(exec_end_instant)
2449            .as_secs_f64()
2450            * 1000.0;
2451
2452        // Overlap percentage: how much of merkle work was done concurrently
2453        let actual_merkle_ms = merkle_concurrent_ms + merkle_drain_ms;
2454        let overlap_pct = if actual_merkle_ms > 0.0 {
2455            (merkle_concurrent_ms / actual_merkle_ms) * 100.0
2456        } else {
2457            0.0
2458        };
2459
2460        // Calculate warmer effectiveness (positive = finished early)
2461        let warmer_early_ms = exec_ms - warmer_ms;
2462
2463        // Determine bottleneck (effective time for each phase)
2464        // For merkle, only count the drain time (concurrent time overlaps with exec)
2465        let phases = [
2466            ("validate", validate_ms),
2467            ("exec", exec_ms),
2468            ("merkle", merkle_drain_ms),
2469            ("store", store_ms),
2470        ];
2471        let bottleneck = phases
2472            .iter()
2473            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2474            .map(|(name, _)| *name)
2475            .unwrap_or("exec");
2476
2477        // Helper for percentage
2478        let pct = |ms: f64| (ms / total_ms * 100.0).round() as u64;
2479
2480        // Format output
2481        let header = format!(
2482            "[METRIC] BLOCK {} {:#x} | {:.3} Ggas/s | {:.2} ms | {} txs | {:.0} Mgas ({}%)",
2483            block_number,
2484            block_hash,
2485            throughput,
2486            total_ms,
2487            transactions_count,
2488            as_mgas,
2489            (gas_used as f64 / gas_limit as f64 * 100.0).round() as u64
2490        );
2491
2492        let bottleneck_marker = |name: &str| {
2493            if name == bottleneck {
2494                " << BOTTLENECK"
2495            } else {
2496                ""
2497            }
2498        };
2499
2500        let warmer_relation = if warmer_early_ms >= 0.0 {
2501            "before exec"
2502        } else {
2503            "after exec"
2504        };
2505
2506        let merkle_start_delay_ms = merkle_start_instant
2507            .duration_since(exec_merkle_start)
2508            .as_secs_f64()
2509            * 1000.0;
2510
2511        info!("{}", header);
2512        info!(
2513            "  |- validate: {:>7.2} ms  ({:>2}%){}",
2514            validate_ms,
2515            pct(validate_ms),
2516            bottleneck_marker("validate")
2517        );
2518        info!(
2519            "  |- exec:     {:>7.2} ms  ({:>2}%){}",
2520            exec_ms,
2521            pct(exec_ms),
2522            bottleneck_marker("exec")
2523        );
2524        info!(
2525            "  |- merkle:   {:>7.2} ms  ({:>2}%){}  [concurrent: {:.2} ms, drain: {:.2} ms, overlap: {:.0}%, queue: {}, start_delay: {:.2} ms]",
2526            merkle_drain_ms,
2527            pct(merkle_drain_ms),
2528            bottleneck_marker("merkle"),
2529            merkle_concurrent_ms,
2530            merkle_drain_ms,
2531            overlap_pct,
2532            merkle_queue_length,
2533            merkle_start_delay_ms,
2534        );
2535        info!(
2536            "  |- store:    {:>7.2} ms  ({:>2}%){}",
2537            store_ms,
2538            pct(store_ms),
2539            bottleneck_marker("store")
2540        );
2541        info!(
2542            "  `- warmer:   {:>7.2} ms         [finished: {:.2} ms {}]",
2543            warmer_ms,
2544            warmer_early_ms.abs(),
2545            warmer_relation,
2546        );
2547
2548        // Set prometheus metrics
2549        metrics!(
2550            METRICS_BLOCKS.set_block_number(block_number);
2551            METRICS_BLOCKS.set_latest_gas_used(gas_used as f64);
2552            METRICS_BLOCKS.set_latest_block_gas_limit(gas_limit as f64);
2553            METRICS_BLOCKS.set_latest_gigagas(throughput);
2554            METRICS_BLOCKS.set_transaction_count(transactions_count as i64);
2555            METRICS_BLOCKS.set_validate_ms(validate_ms);
2556            METRICS_BLOCKS.set_execution_ms(exec_ms);
2557            METRICS_BLOCKS.set_merkle_concurrent_ms(merkle_concurrent_ms);
2558            METRICS_BLOCKS.set_merkle_drain_ms(merkle_drain_ms);
2559            METRICS_BLOCKS.set_merkle_ms(_merkle_total_ms);
2560            METRICS_BLOCKS.set_merkle_overlap_pct(overlap_pct);
2561            METRICS_BLOCKS.set_store_ms(store_ms);
2562            METRICS_BLOCKS.set_warmer_ms(warmer_ms);
2563            METRICS_BLOCKS.set_warmer_early_ms(warmer_early_ms);
2564        );
2565    }
2566
2567    /// Adds multiple blocks in a batch.
2568    ///
2569    /// If an error occurs, returns a tuple containing:
2570    /// - The error type ([`ChainError`]).
2571    /// - [`BatchProcessingFailure`] (if the error was caused by block processing).
2572    ///
2573    /// Note: only the last block's state trie is stored in the db
2574    /// `bals` holds the per-block Block Access Lists fetched during sync, aligned
2575    /// by index with `blocks`. Pass an empty slice when no BALs are available
2576    /// (e.g. block import from RLP); the persistence step then stores none. Only
2577    /// BALs matching their block's header commitment are persisted.
2578    pub async fn add_blocks_in_batch(
2579        &self,
2580        blocks: Vec<Block>,
2581        bals: &[Option<BlockAccessList>],
2582        cancellation_token: CancellationToken,
2583    ) -> Result<(), (ChainError, Option<BatchBlockProcessingFailure>)> {
2584        let mut last_valid_hash = H256::default();
2585
2586        // `bals` is either empty (no BALs available) or index-aligned with `blocks`.
2587        // Guard the contract so a wrong-length slice can't silently drop/ignore BALs
2588        // via the `zip` in the persistence step below.
2589        debug_assert!(
2590            bals.is_empty() || bals.len() == blocks.len(),
2591            "bals must be empty or aligned with blocks (bals={}, blocks={})",
2592            bals.len(),
2593            blocks.len(),
2594        );
2595
2596        let Some(first_block_header) = blocks.first().map(|e| e.header.clone()) else {
2597            return Err((ChainError::Custom("First block not found".into()), None));
2598        };
2599
2600        let chain_config: ChainConfig = self.storage.get_chain_config();
2601
2602        // Cache block hashes for the full batch so we can access them during
2603        // execution without having to store the blocks beforehand.
2604        let mut block_hash_cache: BTreeMap<BlockNumber, BlockHash> =
2605            blocks.iter().map(|b| (b.header.number, b.hash())).collect();
2606
2607        let parent_header = self
2608            .storage
2609            .get_block_header_by_hash(first_block_header.parent_hash)
2610            .map_err(|e| (ChainError::StoreError(e), None))?
2611            .ok_or((ChainError::ParentNotFound, None))?;
2612
2613        // Walk the parent chain to cache the last 256 block hashes so that
2614        // BLOCKHASH can resolve references to blocks from previous batches
2615        // (they may not be canonical yet during import).
2616        block_hash_cache
2617            .entry(parent_header.number)
2618            .or_insert_with(|| parent_header.hash());
2619        let mut hash = parent_header.parent_hash;
2620        let mut number = parent_header.number.saturating_sub(1);
2621        let lookback = first_block_header.number.saturating_sub(256);
2622        while number > lookback {
2623            block_hash_cache.entry(number).or_insert(hash);
2624            match self.storage.get_block_header_by_hash(hash) {
2625                Ok(Some(header)) => {
2626                    hash = header.parent_hash;
2627                    number = number.saturating_sub(1);
2628                }
2629                Ok(None) => break,
2630                Err(e) => {
2631                    warn!("Failed to fetch block header by hash during BLOCKHASH cache walk: {e}");
2632                    break;
2633                }
2634            }
2635        }
2636        let vm_db = StoreVmDatabase::new_with_block_hash_cache(
2637            self.storage.clone(),
2638            parent_header,
2639            block_hash_cache,
2640        )
2641        .map_err(|e| (ChainError::EvmError(e), None))?;
2642        let mut vm = self.new_evm(vm_db).map_err(|e| (e.into(), None))?;
2643
2644        let blocks_len = blocks.len();
2645        let mut all_receipts: Vec<(BlockHash, Vec<Receipt>)> = Vec::with_capacity(blocks_len);
2646        let mut total_gas_used = 0;
2647        let mut transactions_count = 0;
2648
2649        let interval = Instant::now();
2650        for (i, block) in blocks.iter().enumerate() {
2651            if cancellation_token.is_cancelled() {
2652                info!("Received shutdown signal, aborting");
2653                return Err((ChainError::Custom(String::from("shutdown signal")), None));
2654            }
2655            // for the first block, we need to query the store
2656            let parent_header = if i == 0 {
2657                find_parent_header(&block.header, &self.storage).map_err(|err| {
2658                    (
2659                        err,
2660                        Some(BatchBlockProcessingFailure {
2661                            failed_block_hash: block.hash(),
2662                            last_valid_hash,
2663                        }),
2664                    )
2665                })?
2666            } else {
2667                // for the subsequent ones, the parent is the previous block
2668                blocks[i - 1].header.clone()
2669            };
2670
2671            let BlockExecutionResult { receipts, .. } = self
2672                .execute_block_from_state(&parent_header, block, &chain_config, &mut vm)
2673                .map_err(|err| {
2674                    (
2675                        err,
2676                        Some(BatchBlockProcessingFailure {
2677                            failed_block_hash: block.hash(),
2678                            last_valid_hash,
2679                        }),
2680                    )
2681                })?;
2682            debug!("Executed block with hash {}", block.hash());
2683            last_valid_hash = block.hash();
2684            total_gas_used += block.header.gas_used;
2685            transactions_count += block.body.transactions.len();
2686            all_receipts.push((block.hash(), receipts));
2687
2688            // Conversion is safe because EXECUTE_BATCH_SIZE=1024
2689            log_batch_progress(blocks_len as u32, i as u32);
2690            tokio::task::yield_now().await;
2691        }
2692
2693        let account_updates = vm
2694            .get_state_transitions()
2695            .map_err(|err| (ChainError::EvmError(err), None))?;
2696
2697        let last_block = blocks
2698            .last()
2699            .ok_or_else(|| (ChainError::Custom("Last block not found".into()), None))?;
2700
2701        let last_block_number = last_block.header.number;
2702        let last_block_gas_limit = last_block.header.gas_limit;
2703
2704        // Apply the account updates over all blocks and compute the new state root
2705        let account_updates_list = self
2706            .storage
2707            .apply_account_updates_batch(first_block_header.parent_hash, &account_updates)
2708            .map_err(|e| (e.into(), None))?
2709            .ok_or((ChainError::ParentStateNotFound, None))?;
2710
2711        let new_state_root = account_updates_list.state_trie_hash;
2712        let state_updates = account_updates_list.state_updates;
2713        let accounts_updates = account_updates_list.storage_updates;
2714        let code_updates = account_updates_list.code_updates;
2715
2716        // Check state root matches the one in block header
2717        validate_state_root(&last_block.header, new_state_root).map_err(|e| (e, None))?;
2718
2719        // EIP-8159: persist the per-block BAL fetched during sync so peers can
2720        // later request it over eth/71 without re-execution (the batch path
2721        // doesn't record BALs, so without this they'd fall back to regenerating
2722        // against possibly-pruned parent state). Only persist a BAL that matches
2723        // its header commitment; a wrong/empty peer BAL is dropped here, and the
2724        // serve path guards again. Captured before `blocks` is moved below.
2725        let bals_to_store: Vec<(BlockHash, BlockAccessList)> = blocks
2726            .iter()
2727            .zip(bals.iter())
2728            .filter_map(|(block, bal)| {
2729                let bal = bal.as_ref()?;
2730                bal.matches_commitment(block.header.block_access_list_hash, &NativeCrypto)
2731                    .then(|| (block.hash(), bal.clone()))
2732            })
2733            .collect();
2734
2735        let update_batch = UpdateBatch {
2736            account_updates: state_updates,
2737            storage_updates: accounts_updates,
2738            blocks,
2739            receipts: all_receipts,
2740            code_updates,
2741            batch_mode: true,
2742        };
2743
2744        self.storage
2745            .store_block_updates(update_batch)
2746            .map_err(|e| (e.into(), None))?;
2747
2748        for (block_hash, bal) in &bals_to_store {
2749            if let Err(err) = self.storage.store_block_access_list(*block_hash, bal) {
2750                warn!(
2751                    "Failed to persist block access list for {block_hash} during batch sync: {err}"
2752                );
2753            }
2754        }
2755
2756        let elapsed_seconds = interval.elapsed().as_secs_f64();
2757        let throughput = if elapsed_seconds > 0.0 && total_gas_used != 0 {
2758            let as_gigas = (total_gas_used as f64) / 1e9;
2759            as_gigas / elapsed_seconds
2760        } else {
2761            0.0
2762        };
2763
2764        metrics!(
2765            METRICS_BLOCKS.set_block_number(last_block_number);
2766            METRICS_BLOCKS.set_latest_block_gas_limit(last_block_gas_limit as f64);
2767            // Set the latest gas used as the average gas used per block in the batch
2768            METRICS_BLOCKS.set_latest_gas_used(total_gas_used as f64 / blocks_len as f64);
2769            METRICS_BLOCKS.set_latest_gigagas(throughput);
2770        );
2771
2772        if self.options.perf_logs_enabled {
2773            info!(
2774                "[METRICS] Executed and stored: Range: {}, Last block num: {}, Last block gas limit: {}, Total transactions: {}, Total Gas: {}, Throughput: {} Gigagas/s",
2775                blocks_len,
2776                last_block_number,
2777                last_block_gas_limit,
2778                transactions_count,
2779                total_gas_used,
2780                throughput
2781            );
2782        }
2783
2784        Ok(())
2785    }
2786
2787    /// Add a blob transaction and its blobs bundle to the mempool checking that the transaction is valid
2788    #[cfg(feature = "c-kzg")]
2789    pub async fn add_blob_transaction_to_pool(
2790        &self,
2791        transaction: EIP4844Transaction,
2792        blobs_bundle: BlobsBundle,
2793    ) -> Result<H256, MempoolError> {
2794        let fork = self.current_fork().await?;
2795
2796        let transaction = Transaction::EIP4844Transaction(transaction);
2797        let hash = transaction.hash(&NativeCrypto);
2798        if self.mempool.contains_tx(hash)? {
2799            return Ok(hash);
2800        }
2801
2802        // Wire-wrapper size cap for blob txs. Matches geth `txMaxSize = 1 MiB`
2803        // (blobpool) and nethermind `MaxBlobTxSize`, which both bound the
2804        // wire-wrapper form including the sidecar. ethrex stores the core tx
2805        // and the bundle in separate structs, so sum the two encoded sizes
2806        // (the ±few bytes of outer list framing are rounding error at this
2807        // scale).
2808        let wrapper_len = transaction.encode_canonical_len() + blobs_bundle.length();
2809        if wrapper_len > MAX_BLOB_TX_SIZE {
2810            return Err(MempoolError::TxSizeExceeded {
2811                actual: wrapper_len,
2812                limit: MAX_BLOB_TX_SIZE,
2813            });
2814        }
2815
2816        // Validate blobs bundle after checking if it's already added.
2817        if let Transaction::EIP4844Transaction(transaction) = &transaction {
2818            blobs_bundle.validate(transaction, fork)?;
2819        }
2820
2821        let sender = transaction.sender(&NativeCrypto)?;
2822
2823        // Validate transaction
2824        if let Some(tx_to_replace) = self.validate_transaction(&transaction, sender).await? {
2825            self.remove_transaction_from_pool(&tx_to_replace)?;
2826        }
2827
2828        // Add blobs bundle before the transaction so that when add_transaction
2829        // notifies payload builders the blob data is already available.
2830        self.mempool.add_blobs_bundle(hash, blobs_bundle)?;
2831        self.mempool
2832            .add_transaction(hash, sender, MempoolTransaction::new(transaction, sender))?;
2833        Ok(hash)
2834    }
2835
2836    /// Add a transaction to the mempool checking that the transaction is valid
2837    pub async fn add_transaction_to_pool(
2838        &self,
2839        transaction: Transaction,
2840    ) -> Result<H256, MempoolError> {
2841        // Blob transactions should be submitted via add_blob_transaction along with the corresponding blobs bundle
2842        if matches!(transaction, Transaction::EIP4844Transaction(_)) {
2843            return Err(MempoolError::BlobTxNoBlobsBundle);
2844        }
2845        // Wire size cap: run before sender recovery so oversized txs don't
2846        // force secp256k1 work. Matches geth's `txMaxSize` admission order
2847        // (size-checked at `ValidateTransaction` entry, well before any
2848        // crypto). The same check sits in `validate_transaction` so direct
2849        // callers (tests, L2 paths) keep the guarantee.
2850        let encoded_len = transaction.encode_canonical_len();
2851        if encoded_len > MAX_TX_SIZE {
2852            return Err(MempoolError::TxSizeExceeded {
2853                actual: encoded_len,
2854                limit: MAX_TX_SIZE,
2855            });
2856        }
2857        let hash = transaction.hash(&NativeCrypto);
2858        if self.mempool.contains_tx(hash)? {
2859            return Ok(hash);
2860        }
2861        let sender = transaction.sender(&NativeCrypto)?;
2862        // Validate transaction
2863        if let Some(tx_to_replace) = self.validate_transaction(&transaction, sender).await? {
2864            self.remove_transaction_from_pool(&tx_to_replace)?;
2865        }
2866
2867        // Add transaction to storage
2868        self.mempool
2869            .add_transaction(hash, sender, MempoolTransaction::new(transaction, sender))?;
2870
2871        Ok(hash)
2872    }
2873
2874    /// Remove a transaction from the mempool
2875    pub fn remove_transaction_from_pool(&self, hash: &H256) -> Result<(), StoreError> {
2876        self.mempool.remove_transaction(hash)
2877    }
2878
2879    /// Remove all transactions in the executed block from the pool (if we have them)
2880    pub fn remove_block_transactions_from_pool(&self, block: &Block) -> Result<(), StoreError> {
2881        for tx in &block.body.transactions {
2882            self.mempool.remove_transaction(&tx.hash(&NativeCrypto))?;
2883        }
2884        Ok(())
2885    }
2886
2887    /// Drop blob txs with nonce below the sender's on-chain nonce at `head_hash`.
2888    /// Per-block pruning only covers the head block, so stale blob txs from
2889    /// non-head canonical blocks leak in and are never evicted (value/nonce
2890    /// eviction pins low nonces). Resetting against on-chain nonces clears them.
2891    pub async fn remove_stale_blob_txs(&self, head_hash: BlockHash) -> Result<(), StoreError> {
2892        let blob_txs = self.mempool.blob_txs()?;
2893        if blob_txs.is_empty() {
2894            return Ok(());
2895        }
2896        // Cache on-chain nonce per sender to avoid repeated state reads.
2897        let mut nonce_by_sender: HashMap<Address, u64> = HashMap::new();
2898        for (hash, sender, tx_nonce) in blob_txs {
2899            let state_nonce = match nonce_by_sender.entry(sender) {
2900                Entry::Occupied(e) => *e.get(),
2901                Entry::Vacant(e) => {
2902                    let nonce = self
2903                        .storage
2904                        .get_account_info_by_hash(head_hash, sender)?
2905                        .map(|info| info.nonce)
2906                        .unwrap_or(0);
2907                    *e.insert(nonce)
2908                }
2909            };
2910            if tx_nonce < state_nonce {
2911                self.mempool.remove_transaction(&hash)?;
2912            }
2913        }
2914        Ok(())
2915    }
2916
2917    /*
2918
2919    SOME VALIDATIONS THAT WE COULD INCLUDE
2920    Stateless validations
2921    1. This transaction is valid on current mempool
2922        -> Depends on mempool transaction filtering logic
2923    2. Ensure the maxPriorityFeePerGas is high enough to cover the requirement of the calling pool (the minimum to be included in)
2924        -> Depends on mempool transaction filtering logic
2925    3. Transaction's encoded size is smaller than maximum allowed
2926        -> I think that this is not in the spec, but it may be a good idea
2927    4. Make sure the transaction is signed properly
2928    5. Ensure a Blob Transaction comes with its sidecar (Done! - All blob validations have been moved to `common/types/blobs_bundle.rs`):
2929      1. Validate number of BlobHashes is positive (Done!)
2930      2. Validate number of BlobHashes is less than the maximum allowed per block,
2931         which may be computed as `maxBlobGasPerBlock / blobTxBlobGasPerBlob`
2932      3. Ensure number of BlobHashes is equal to:
2933        - The number of blobs (Done!)
2934        - The number of commitments (Done!)
2935        - The number of proofs (Done!)
2936      4. Validate that the hashes matches with the commitments, performing a `kzg4844` hash. (Done!)
2937      5. Verify the blob proofs with the `kzg4844` (Done!)
2938    Stateful validations
2939    1. Ensure transaction nonce is higher than the `from` address stored nonce
2940    2. Certain pools do not allow for nonce gaps. Ensure a gap is not produced (that is, the transaction nonce is exactly the following of the stored one)
2941    3. Ensure the transactor has enough funds to cover transaction cost:
2942        - Transaction cost is calculated as `(gas * gasPrice) + (blobGas * blobGasPrice) + value`
2943    4. In case of transaction reorg, ensure the transactor has enough funds to cover for transaction replacements without overdrafts.
2944    - This is done by comparing the total spent gas of the transactor from all pooled transactions, and accounting for the necessary gas spenditure if any of those transactions is replaced.
2945    5. Ensure the transactor is able to add a new transaction. The number of transactions sent by an account may be limited by a certain configured value
2946
2947    */
2948    /// Returns the hash of the transaction to replace in case the nonce already exists
2949    pub async fn validate_transaction(
2950        &self,
2951        tx: &Transaction,
2952        sender: Address,
2953    ) -> Result<Option<H256>, MempoolError> {
2954        let nonce = tx.nonce();
2955
2956        if matches!(tx, &Transaction::PrivilegedL2Transaction(_)) {
2957            return Ok(None);
2958        }
2959
2960        let header_no = self.storage.get_latest_block_number().await?;
2961        let header = self
2962            .storage
2963            .get_block_header(header_no)?
2964            .ok_or(MempoolError::NoBlockHeaderError)?;
2965        let config = self.storage.get_chain_config();
2966
2967        // Wire size cap for non-blob txs: peer-policy default, not consensus.
2968        // Matches geth `txMaxSize` (legacypool), reth `DEFAULT_MAX_TX_INPUT_BYTES`,
2969        // nethermind `MaxTxSize`. Blob txs are bounded by their own
2970        // wire-wrapper cap (`MAX_BLOB_TX_SIZE`) in `add_blob_transaction_to_pool`,
2971        // which sums the core tx and the sidecar to match geth/nethermind/erigon
2972        // scope.
2973        if !matches!(tx, Transaction::EIP4844Transaction(_)) {
2974            let encoded_len = tx.encode_canonical_len();
2975            if encoded_len > MAX_TX_SIZE {
2976                return Err(MempoolError::TxSizeExceeded {
2977                    actual: encoded_len,
2978                    limit: MAX_TX_SIZE,
2979                });
2980            }
2981        }
2982
2983        // Check init code size
2984        // [EIP-7954] - Amsterdam increases the limit
2985        let max_initcode_size = if config.is_amsterdam_activated(header.timestamp) {
2986            AMSTERDAM_MAX_INITCODE_SIZE
2987        } else {
2988            MAX_INITCODE_SIZE
2989        };
2990        if config.is_shanghai_activated(header.timestamp)
2991            && tx.is_contract_creation()
2992            && tx.data().len() > max_initcode_size as usize
2993        {
2994            return Err(MempoolError::TxMaxInitCodeSizeError);
2995        }
2996
2997        if config.is_osaka_activated(header.timestamp)
2998            && !config.is_amsterdam_activated(header.timestamp)
2999            && tx.gas_limit() > POST_OSAKA_GAS_LIMIT_CAP
3000        {
3001            // https://eips.ethereum.org/EIPS/eip-7825
3002            return Err(MempoolError::TxMaxGasLimitExceededError(
3003                tx.hash(&NativeCrypto),
3004                tx.gas_limit(),
3005            ));
3006        }
3007
3008        // Check gas limit is less than header's gas limit
3009        if header.gas_limit < tx.gas_limit() {
3010            return Err(MempoolError::TxGasLimitExceededError);
3011        }
3012
3013        // Check priority fee is less or equal than gas fee gap
3014        if tx.max_priority_fee().unwrap_or(0) > tx.max_fee_per_gas().unwrap_or(0) {
3015            return Err(MempoolError::TxTipAboveFeeCapError);
3016        }
3017
3018        // EIP-7702 type-4 structural validation, mirroring LEVM's
3019        // `validate_type_4_tx` and ordered before the gas checks so the returned
3020        // error names the structural fault, not a downstream gas symptom. Reject
3021        // at admission so invalid type-4 txs never enter the pool.
3022        if let Transaction::EIP7702Transaction(eip7702) = tx {
3023            // Type-4 txs only exist from Prague onward.
3024            if !config.is_prague_activated(header.timestamp) {
3025                return Err(MempoolError::Eip7702TxPreFork);
3026            }
3027            // An empty authorization_list makes the tx invalid.
3028            if eip7702.authorization_list.is_empty() {
3029                return Err(MempoolError::EmptyAuthorizationList);
3030            }
3031        }
3032
3033        // Check that the gas limit covers the gas needs for transaction metadata.
3034        if tx.gas_limit() < mempool::transaction_intrinsic_gas(tx, sender, &header, &config)? {
3035            return Err(MempoolError::TxIntrinsicGasCostAboveLimitError);
3036        }
3037
3038        // Check that the specified blob gas fee is above the minimum value
3039        if let Some(fee) = tx.max_fee_per_blob_gas() {
3040            // Blob tx fee checks
3041            if fee < MIN_BASE_FEE_PER_BLOB_GAS.into() {
3042                return Err(MempoolError::TxBlobBaseFeeTooLowError);
3043            }
3044        };
3045
3046        let maybe_sender_acc_info = self.storage.get_account_info(header_no, sender).await?;
3047
3048        if let Some(sender_acc_info) = maybe_sender_acc_info {
3049            if nonce < sender_acc_info.nonce || nonce == u64::MAX {
3050                return Err(MempoolError::NonceTooLow);
3051            }
3052
3053            // EIP-3607: reject txs from senders with deployed code, unless
3054            // the code is an EIP-7702 delegation designation (the account is
3055            // still an EOA in spirit, just pointing at delegate code).
3056            //
3057            // Length-based fast path: any code whose length isn't exactly
3058            // `EIP7702_DELEGATED_CODE_LEN` (23) cannot be a delegation, so
3059            // we reject without loading the bytecode. Only when the metadata
3060            // length matches do we fetch + verify the prefix. This avoids
3061            // pulling potentially large contract bytecode on every contract
3062            // sender that hits admission.
3063            if sender_acc_info.code_hash != *EMPTY_KECCAK_HASH {
3064                let metadata_len = self
3065                    .storage
3066                    .get_code_metadata(sender_acc_info.code_hash)?
3067                    .map(|m| m.length);
3068                let is_delegation = if metadata_len == Some(EIP7702_DELEGATED_CODE_LEN as u64) {
3069                    // Metadata says the code is delegation-shaped; if the
3070                    // bytecode is then missing from the store, the DB is
3071                    // inconsistent — surface that as `StoreError` instead of
3072                    // silently treating the sender as a contract (which would
3073                    // wrongly reject a valid 7702-delegated EOA).
3074                    let code = self
3075                        .storage
3076                        .get_account_code(sender_acc_info.code_hash)?
3077                        .ok_or_else(|| {
3078                            StoreError::Custom(format!(
3079                                "code missing for hash {:?} despite present metadata",
3080                                sender_acc_info.code_hash
3081                            ))
3082                        })?;
3083                    is_eip7702_delegation(code.code())
3084                } else {
3085                    false
3086                };
3087                if !is_delegation {
3088                    return Err(MempoolError::SenderIsContract);
3089                }
3090            }
3091
3092            let tx_cost = tx
3093                .cost_without_base_fee()
3094                .ok_or(MempoolError::InvalidTxGasvalues)?;
3095
3096            if tx_cost > sender_acc_info.balance {
3097                return Err(MempoolError::NotEnoughBalance);
3098            }
3099        } else {
3100            // An account that is not in the database cannot possibly have enough balance to cover the transaction cost
3101            return Err(MempoolError::NotEnoughBalance);
3102        }
3103
3104        // Check the nonce of pendings TXs in the mempool from the same sender
3105        // If it exists check if the new tx has higher fees
3106        let tx_to_replace_hash = self.mempool.find_tx_to_replace(sender, nonce, tx)?;
3107
3108        if tx
3109            .chain_id()
3110            .is_some_and(|chain_id| chain_id != config.chain_id)
3111        {
3112            return Err(MempoolError::InvalidChainId(config.chain_id));
3113        }
3114
3115        Ok(tx_to_replace_hash)
3116    }
3117
3118    /// Marks the node's chain as up to date with the current chain
3119    /// Once the initial sync has taken place, the node will be considered as sync
3120    pub fn set_synced(&self) {
3121        self.is_synced.store(true, Ordering::Relaxed);
3122    }
3123
3124    /// Marks the node's chain as not up to date with the current chain.
3125    /// This will be used when the node is one batch or more behind the current chain.
3126    pub fn set_not_synced(&self) {
3127        self.is_synced.store(false, Ordering::Relaxed);
3128    }
3129
3130    /// Returns whether the node's chain is up to date with the current chain
3131    /// This will be true if the initial sync has already taken place and does not reflect whether there is an ongoing sync process
3132    /// The node should accept incoming p2p transactions if this method returns true
3133    pub fn is_synced(&self) -> bool {
3134        self.is_synced.load(Ordering::Relaxed)
3135    }
3136
3137    pub fn get_p2p_transaction_by_hash(&self, hash: &H256) -> Result<P2PTransaction, StoreError> {
3138        let Some(tx) = self.mempool.get_transaction_by_hash(*hash)? else {
3139            return Err(StoreError::Custom(format!(
3140                "Hash {hash} not found in the mempool",
3141            )));
3142        };
3143        let result = match tx {
3144            Transaction::LegacyTransaction(itx) => P2PTransaction::LegacyTransaction(itx),
3145            Transaction::EIP2930Transaction(itx) => P2PTransaction::EIP2930Transaction(itx),
3146            Transaction::EIP1559Transaction(itx) => P2PTransaction::EIP1559Transaction(itx),
3147            Transaction::EIP4844Transaction(itx) => {
3148                let Some(bundle) = self.mempool.get_blobs_bundle(*hash)? else {
3149                    return Err(StoreError::Custom(format!(
3150                        "Blob transaction present without its bundle: hash {hash}",
3151                    )));
3152                };
3153
3154                P2PTransaction::EIP4844TransactionWithBlobs(WrappedEIP4844Transaction {
3155                    tx: itx,
3156                    wrapper_version: (bundle.version != 0).then_some(bundle.version),
3157                    blobs_bundle: bundle,
3158                })
3159            }
3160            Transaction::EIP7702Transaction(itx) => P2PTransaction::EIP7702Transaction(itx),
3161            // Exclude privileged transactions as they are only created
3162            // by the lead sequencer. In the future, they might get gossiped
3163            // like the rest.
3164            Transaction::PrivilegedL2Transaction(_) => {
3165                return Err(StoreError::Custom(
3166                    "Privileged Transactions are not supported in P2P".to_string(),
3167                ));
3168            }
3169            Transaction::FeeTokenTransaction(itx) => P2PTransaction::FeeTokenTransaction(itx),
3170        };
3171
3172        Ok(result)
3173    }
3174
3175    pub fn new_evm(&self, vm_db: StoreVmDatabase) -> Result<Evm, EvmError> {
3176        new_evm(&self.options.r#type, vm_db)
3177    }
3178
3179    /// Get the current fork of the chain, based on the latest block's timestamp
3180    pub async fn current_fork(&self) -> Result<Fork, StoreError> {
3181        let chain_config = self.storage.get_chain_config();
3182        let latest_block_number = self.storage.get_latest_block_number().await?;
3183        let latest_block = self
3184            .storage
3185            .get_block_header(latest_block_number)?
3186            .ok_or(StoreError::Custom("Latest block not in DB".to_string()))?;
3187        Ok(chain_config.fork(latest_block.timestamp))
3188    }
3189}
3190
3191/// Open a state trie or storage trie depending on whether `prefix` is given.
3192fn load_trie(
3193    storage: &Store,
3194    parent_state_root: H256,
3195    prefix: Option<H256>,
3196) -> Result<Trie, StoreError> {
3197    Ok(match prefix {
3198        Some(account_hash) => {
3199            let state_trie = storage.open_state_trie(parent_state_root)?;
3200            let storage_root = match state_trie.get(account_hash.as_bytes())? {
3201                Some(rlp) => AccountState::decode(&rlp)?.storage_root,
3202                None => *EMPTY_TRIE_HASH,
3203            };
3204            storage.open_storage_trie(account_hash, parent_state_root, storage_root)?
3205        }
3206        None => storage.open_state_trie(parent_state_root)?,
3207    })
3208}
3209
3210/// Collapse a root branch node into an extension or leaf if it has only one valid child.
3211/// Returns `None` if there are no valid children.
3212fn collapse_root_node(
3213    storage: &Store,
3214    parent_state_root: H256,
3215    prefix: Option<H256>,
3216    root: BranchNode,
3217) -> Result<Option<Node>, StoreError> {
3218    let children: Vec<(usize, &NodeRef)> = root
3219        .choices
3220        .iter()
3221        .enumerate()
3222        .filter(|(_, choice)| choice.is_valid())
3223        .take(2)
3224        .collect();
3225    if children.len() > 1 {
3226        return Ok(Some(Node::Branch(Box::from(root))));
3227    }
3228    let Some((choice, only_child)) = children.first() else {
3229        return Ok(None);
3230    };
3231    let only_child = Arc::unwrap_or_clone(match only_child {
3232        NodeRef::Node(node, _) => node.clone(),
3233        noderef @ NodeRef::Hash(_) => {
3234            let trie = load_trie(storage, parent_state_root, prefix)?;
3235            let Some(node) = noderef.get_node(trie.db(), Nibbles::from_hex(vec![*choice as u8]))?
3236            else {
3237                return Ok(None);
3238            };
3239            node
3240        }
3241    });
3242    Ok(Some(match only_child {
3243        Node::Branch(_) => {
3244            ExtensionNode::new(Nibbles::from_hex(vec![*choice as u8]), only_child.into()).into()
3245        }
3246        Node::Extension(mut extension_node) => {
3247            extension_node.prefix.prepend(*choice as u8);
3248            extension_node.into()
3249        }
3250        Node::Leaf(mut leaf) => {
3251            leaf.partial.prepend(*choice as u8);
3252            leaf.into()
3253        }
3254    }))
3255}
3256
3257/// Collect the state trie shard, merge pre-collected nodes, and send results.
3258fn collect_and_send(
3259    index: u8,
3260    state_trie: &mut Trie,
3261    pre_collected_state: &mut Vec<TrieNode>,
3262    storage_nodes: &mut Vec<(H256, Vec<TrieNode>)>,
3263    tx: Sender<CollectedStateMsg>,
3264) -> Result<(), StoreError> {
3265    let (subroot, mut state_nodes) = collect_trie(index, std::mem::take(state_trie))?;
3266    if !pre_collected_state.is_empty() {
3267        let mut pre = std::mem::take(pre_collected_state);
3268        pre.extend(state_nodes);
3269        state_nodes = pre;
3270    }
3271    tx.send(CollectedStateMsg {
3272        index,
3273        subroot,
3274        state_nodes,
3275        storage_nodes: std::mem::take(storage_nodes),
3276    })
3277    .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
3278    Ok(())
3279}
3280
3281/// Open or get an existing storage trie for the given account prefix.
3282fn get_or_open_storage_trie<'a>(
3283    storage_tries: &'a mut FxHashMap<H256, Trie>,
3284    storage: &Store,
3285    parent_state_root: H256,
3286    prefix: H256,
3287    storage_root: H256,
3288) -> Result<&'a mut Trie, StoreError> {
3289    match storage_tries.entry(prefix) {
3290        Entry::Occupied(e) => Ok(e.into_mut()),
3291        Entry::Vacant(e) => {
3292            Ok(e.insert(storage.open_storage_trie(prefix, parent_state_root, storage_root)?))
3293        }
3294    }
3295}
3296
3297fn handle_subtrie(
3298    storage: Store,
3299    rx: cb::Receiver<WorkerRequest>,
3300    parent_state_root: H256,
3301    index: u8,
3302    worker_senders: Vec<cb::Sender<WorkerRequest>>,
3303    shutdown_rx: cb::Receiver<()>,
3304) -> Result<(), StoreError> {
3305    let mut state_trie = storage.open_state_trie(parent_state_root)?;
3306    let mut storage_nodes: Vec<(H256, Vec<TrieNode>)> = vec![];
3307    let mut accounts: FxHashMap<H256, AccountState> = Default::default();
3308    let mut expected_shards: FxHashMap<H256, u16> = Default::default();
3309    let mut storage_state: FxHashMap<H256, PreMerkelizedAccountState> = Default::default();
3310    let mut received_shards: FxHashMap<H256, u16> = Default::default();
3311    let mut pending_storage_accounts: usize = 0;
3312    let mut pending_collect_tx: Option<Sender<CollectedStateMsg>> = None;
3313    let mut pre_collected_state: Vec<TrieNode> = vec![];
3314    let mut storage_tries: FxHashMap<H256, Trie> = Default::default();
3315    let mut pre_collected_storage: FxHashMap<H256, Vec<TrieNode>> = Default::default();
3316
3317    // Held until collection finishes to keep cross-worker channels open.
3318    let mut worker_senders: Option<Vec<cb::Sender<WorkerRequest>>> = Some(worker_senders);
3319    let mut dirty = false;
3320    // When active, we finalize one storage trie per loop iteration,
3321    // interleaving with incoming StorageShard messages.
3322    let mut collecting_storages = false;
3323    let mut routing_complete = false;
3324    let mut routing_done_mask: u16 = 0;
3325    let mut storage_to_collect: Vec<(H256, Trie)> = vec![];
3326
3327    loop {
3328        // When collecting storages, finalize one trie per iteration so that
3329        // incoming StorageShard messages can be processed in between.
3330        if collecting_storages {
3331            if let Some((prefix, trie)) = storage_to_collect.pop() {
3332                let senders = worker_senders
3333                    .as_ref()
3334                    .expect("collecting after senders dropped");
3335                let (root, mut nodes) = collect_trie(index, trie)?;
3336                if let Some(mut pre_nodes) = pre_collected_storage.remove(&prefix) {
3337                    pre_nodes.extend(nodes);
3338                    nodes = pre_nodes;
3339                }
3340                let bucket = prefix.as_fixed_bytes()[0] >> 4;
3341                senders[bucket as usize]
3342                    .send(WorkerRequest::StorageShard {
3343                        prefix,
3344                        index,
3345                        subroot: root,
3346                        nodes,
3347                    })
3348                    .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
3349            } else {
3350                // All storage tries finalized
3351                worker_senders = None;
3352                collecting_storages = false;
3353                // Check if deferred collect can resolve now
3354                if pending_storage_accounts == 0
3355                    && let Some(tx) = pending_collect_tx.take()
3356                {
3357                    collect_and_send(
3358                        index,
3359                        &mut state_trie,
3360                        &mut pre_collected_state,
3361                        &mut storage_nodes,
3362                        tx,
3363                    )?;
3364                    break;
3365                }
3366            }
3367        }
3368
3369        // When collecting or dirty, poll non-blocking so we can interleave.
3370        // When clean and not collecting, just block.
3371        let msg = if collecting_storages || dirty {
3372            match rx.try_recv() {
3373                Ok(msg) => msg,
3374                Err(TryRecvError::Disconnected) => break,
3375                Err(TryRecvError::Empty) => {
3376                    // Check for shutdown signal from watcher
3377                    if matches!(shutdown_rx.try_recv(), Err(TryRecvError::Disconnected)) {
3378                        return Err(StoreError::Custom("shard worker shutdown".into()));
3379                    }
3380                    if dirty {
3381                        // Pre-collect state trie — safe during storage
3382                        // collection too, since StorageShard resolution only
3383                        // dirties specific paths that get re-committed later.
3384                        let mut nodes = state_trie.commit_without_storing(&NativeCrypto);
3385                        nodes.retain(|(nib, _)| nib.as_ref().first() == Some(&index));
3386                        pre_collected_state.extend(nodes);
3387                        if !collecting_storages {
3388                            // Pre-collect storage tries (only when not draining)
3389                            for (prefix, trie) in storage_tries.iter_mut() {
3390                                let mut nodes = trie.commit_without_storing(&NativeCrypto);
3391                                nodes.retain(|(nib, _)| nib.as_ref().first() == Some(&index));
3392                                if !nodes.is_empty() {
3393                                    pre_collected_storage
3394                                        .entry(*prefix)
3395                                        .or_default()
3396                                        .extend(nodes);
3397                                }
3398                            }
3399                        }
3400                        dirty = false;
3401                    }
3402                    continue;
3403                }
3404            }
3405        } else {
3406            select! {
3407                recv(rx) -> msg => match msg {
3408                    Ok(msg) => msg,
3409                    Err(_) => break,
3410                },
3411                recv(shutdown_rx) -> _ => {
3412                    return Err(StoreError::Custom("shard worker shutdown".into()));
3413                }
3414            }
3415        };
3416
3417        match msg {
3418            WorkerRequest::ProcessAccount {
3419                prefix,
3420                info,
3421                storage: account_storage,
3422                removed,
3423                removed_storage,
3424            } => {
3425                let senders = worker_senders
3426                    .as_ref()
3427                    .expect("ProcessAccount after collection started");
3428
3429                // Always load account to warm state trie during execution overlap
3430                match accounts.entry(prefix) {
3431                    Entry::Occupied(_) => {}
3432                    Entry::Vacant(vacant_entry) => {
3433                        let account_state = match state_trie.get(prefix.as_bytes())? {
3434                            Some(rlp) => {
3435                                let state = AccountState::decode(&rlp)?;
3436                                state_trie.insert(prefix.as_bytes().to_vec(), rlp)?;
3437                                state
3438                            }
3439                            None => AccountState::default(),
3440                        };
3441                        vacant_entry.insert(account_state);
3442                    }
3443                }
3444
3445                // Apply info immediately and insert into trie
3446                if let Some(info) = info {
3447                    let acct = accounts.get_mut(&prefix).expect("just loaded");
3448                    acct.nonce = info.nonce;
3449                    acct.balance = info.balance;
3450                    acct.code_hash = info.code_hash;
3451                    let path = prefix.as_bytes();
3452                    if *acct != AccountState::default() {
3453                        state_trie.insert(path.to_vec(), acct.encode_to_vec())?;
3454                    } else {
3455                        state_trie.remove(path)?;
3456                    }
3457                }
3458
3459                if removed || removed_storage {
3460                    // Delete locally + send DeleteStorage to other 15 workers
3461                    pre_collected_storage.remove(&prefix);
3462                    storage_tries.insert(prefix, Trie::new_temp());
3463                    for (i, tx) in senders.iter().enumerate() {
3464                        if i as u8 != index {
3465                            tx.send(WorkerRequest::DeleteStorage(prefix))
3466                                .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
3467                        }
3468                    }
3469                    accounts.get_mut(&prefix).expect("just loaded").storage_root = *EMPTY_TRIE_HASH;
3470                    if expected_shards.insert(prefix, 0xFFFF).is_none() {
3471                        pending_storage_accounts += 1;
3472                    }
3473                    if removed {
3474                        dirty = true;
3475                        continue;
3476                    }
3477                }
3478
3479                if !account_storage.is_empty() {
3480                    let storage_root = accounts
3481                        .get(&prefix)
3482                        .map(|a| a.storage_root)
3483                        .unwrap_or(*EMPTY_TRIE_HASH);
3484
3485                    let is_new = !expected_shards.contains_key(&prefix);
3486                    for (key, value) in account_storage {
3487                        let hashed_key = keccak(key);
3488                        let bucket = hashed_key.as_fixed_bytes()[0] >> 4;
3489                        *expected_shards.entry(prefix).or_insert(0u16) |= 1 << bucket;
3490                        if bucket == index {
3491                            // Local storage: insert directly
3492                            let trie = get_or_open_storage_trie(
3493                                &mut storage_tries,
3494                                &storage,
3495                                parent_state_root,
3496                                prefix,
3497                                storage_root,
3498                            )?;
3499                            if value.is_zero() {
3500                                trie.remove(hashed_key.as_bytes())?;
3501                            } else {
3502                                trie.insert(hashed_key.as_bytes().to_vec(), value.encode_to_vec())?;
3503                            }
3504                        } else {
3505                            senders[bucket as usize]
3506                                .send(WorkerRequest::MerklizeStorage {
3507                                    prefix,
3508                                    key: hashed_key,
3509                                    value,
3510                                    storage_root,
3511                                })
3512                                .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
3513                        }
3514                    }
3515                    if is_new {
3516                        pending_storage_accounts += 1;
3517                    }
3518                }
3519                dirty = true;
3520            }
3521            WorkerRequest::MerklizeStorage {
3522                prefix,
3523                key,
3524                value,
3525                storage_root,
3526            } => {
3527                let trie = get_or_open_storage_trie(
3528                    &mut storage_tries,
3529                    &storage,
3530                    parent_state_root,
3531                    prefix,
3532                    storage_root,
3533                )?;
3534                if value.is_zero() {
3535                    trie.remove(key.as_bytes())?;
3536                } else {
3537                    trie.insert(key.as_bytes().to_vec(), value.encode_to_vec())?;
3538                }
3539                dirty = true;
3540            }
3541            WorkerRequest::DeleteStorage(prefix) => {
3542                pre_collected_storage.remove(&prefix);
3543                storage_tries.insert(prefix, Trie::new_temp());
3544                dirty = true;
3545            }
3546            WorkerRequest::FinishRouting => {
3547                // Signal all workers that we're done routing MerklizeStorage.
3548                let senders = worker_senders
3549                    .as_ref()
3550                    .expect("FinishRouting after senders dropped");
3551                for i in 0..16u8 {
3552                    senders[i as usize]
3553                        .send(WorkerRequest::RoutingDone { from: index })
3554                        .map_err(|e| StoreError::Custom(format!("send error: {e}")))?;
3555                }
3556            }
3557            WorkerRequest::RoutingDone { from } => {
3558                routing_done_mask |= 1u16 << from;
3559                if routing_done_mask == 0xFFFF && !collecting_storages && !routing_complete {
3560                    collecting_storages = true;
3561                    routing_complete = true;
3562                    storage_to_collect = storage_tries.drain().collect();
3563                }
3564            }
3565            WorkerRequest::MerklizeAccounts { accounts: batch } => {
3566                // Info already applied in ProcessAccount — just record empty storage nodes
3567                for hashed_account in batch {
3568                    storage_nodes.push((hashed_account, vec![]));
3569                }
3570            }
3571            WorkerRequest::StorageShard {
3572                prefix,
3573                index: shard_index,
3574                mut subroot,
3575                nodes,
3576            } => {
3577                let state = storage_state.entry(prefix).or_default();
3578                match &mut state.storage_root {
3579                    Some(root) => {
3580                        root.choices[shard_index as usize] =
3581                            std::mem::take(&mut subroot.choices[shard_index as usize]);
3582                    }
3583                    rootptr => {
3584                        *rootptr = Some(subroot);
3585                    }
3586                }
3587                state.nodes.extend(nodes);
3588
3589                let received = received_shards.entry(prefix).or_insert(0u16);
3590                *received |= 1 << shard_index;
3591                if *received == expected_shards.get(&prefix).copied().unwrap_or(0) {
3592                    // All shards received — resolve storage root
3593                    let mut state = storage_state.remove(&prefix).expect("shard without state");
3594                    let new_storage_root = if let Some(mut root) = state.storage_root {
3595                        // Children from other shards need clear_hash to be re-committed.
3596                        root.choices.iter_mut().for_each(NodeRef::clear_hash);
3597                        let collapsed =
3598                            collapse_root_node(&storage, parent_state_root, Some(prefix), *root)?;
3599                        if let Some(root) = collapsed {
3600                            let mut root = NodeRef::from(root);
3601                            let hash =
3602                                root.commit(Nibbles::default(), &mut state.nodes, &NativeCrypto);
3603                            let _ = DROP_SENDER.send(Box::new(root));
3604                            hash.finalize(&NativeCrypto)
3605                        } else {
3606                            state.nodes.push((Nibbles::default(), vec![RLP_NULL]));
3607                            *EMPTY_TRIE_HASH
3608                        }
3609                    } else {
3610                        *EMPTY_TRIE_HASH
3611                    };
3612                    storage_nodes.push((prefix, state.nodes));
3613
3614                    // Update account's storage root and re-insert into state trie
3615                    let old_state = accounts.get_mut(&prefix).expect("loaded in ProcessAccount");
3616                    old_state.storage_root = new_storage_root;
3617                    let path = prefix.as_bytes();
3618                    if *old_state != AccountState::default() {
3619                        state_trie.insert(path.to_vec(), old_state.encode_to_vec())?;
3620                    } else {
3621                        state_trie.remove(path)?;
3622                    }
3623
3624                    dirty = true;
3625                    pending_storage_accounts -= 1;
3626                    if pending_storage_accounts == 0
3627                        && !collecting_storages
3628                        && routing_complete
3629                        && let Some(tx) = pending_collect_tx.take()
3630                    {
3631                        collect_and_send(
3632                            index,
3633                            &mut state_trie,
3634                            &mut pre_collected_state,
3635                            &mut storage_nodes,
3636                            tx,
3637                        )?;
3638                        break;
3639                    }
3640                }
3641            }
3642            WorkerRequest::CollectState { tx } => {
3643                if pending_storage_accounts == 0 && !collecting_storages && routing_complete {
3644                    collect_and_send(
3645                        index,
3646                        &mut state_trie,
3647                        &mut pre_collected_state,
3648                        &mut storage_nodes,
3649                        tx,
3650                    )?;
3651                    break;
3652                }
3653                // Defer until collection is done and all StorageShards resolved
3654                pending_collect_tx = Some(tx);
3655            }
3656        }
3657    }
3658    Ok(())
3659}
3660
3661pub fn new_evm(blockchain_type: &BlockchainType, vm_db: StoreVmDatabase) -> Result<Evm, EvmError> {
3662    let evm = match blockchain_type {
3663        BlockchainType::L1 => Evm::new_for_l1(vm_db, Arc::new(NativeCrypto)),
3664        BlockchainType::L2(l2_config) => {
3665            let fee_config = *l2_config
3666                .fee_config
3667                .read()
3668                .map_err(|_| EvmError::Custom("Fee config lock was poisoned".to_string()))?;
3669
3670            Evm::new_for_l2(vm_db, fee_config, Arc::new(NativeCrypto))?
3671        }
3672    };
3673    Ok(evm)
3674}
3675
3676/// Performs post-execution checks
3677pub fn validate_state_root(
3678    block_header: &BlockHeader,
3679    new_state_root: H256,
3680) -> Result<(), ChainError> {
3681    // Compare state root
3682    if new_state_root == block_header.state_root {
3683        Ok(())
3684    } else {
3685        Err(ChainError::InvalidBlock(
3686            InvalidBlockError::StateRootMismatch,
3687        ))
3688    }
3689}
3690
3691// Returns the hash of the head of the canonical chain (the latest valid hash).
3692pub async fn latest_canonical_block_hash(storage: &Store) -> Result<H256, ChainError> {
3693    let latest_block_number = storage.get_latest_block_number().await?;
3694    if let Some(latest_valid_header) = storage.get_block_header(latest_block_number)? {
3695        let latest_valid_hash = latest_valid_header.hash();
3696        return Ok(latest_valid_hash);
3697    }
3698    Err(ChainError::StoreError(StoreError::Custom(
3699        "Could not find latest valid hash".to_string(),
3700    )))
3701}
3702
3703/// Searchs the header of the parent block header. If the parent header is missing,
3704/// Returns a ChainError::ParentNotFound. If the storage has an error it propagates it
3705pub fn find_parent_header(
3706    block_header: &BlockHeader,
3707    storage: &Store,
3708) -> Result<BlockHeader, ChainError> {
3709    match storage.get_block_header_by_hash(block_header.parent_hash)? {
3710        Some(parent_header) => Ok(parent_header),
3711        None => Err(ChainError::ParentNotFound),
3712    }
3713}
3714
3715pub async fn is_canonical(
3716    store: &Store,
3717    block_number: BlockNumber,
3718    block_hash: BlockHash,
3719) -> Result<bool, StoreError> {
3720    match store.get_canonical_block_hash(block_number).await? {
3721        Some(hash) if hash == block_hash => Ok(true),
3722        _ => Ok(false),
3723    }
3724}
3725
3726fn branchify(node: Node) -> Box<BranchNode> {
3727    match node {
3728        Node::Branch(branch_node) => branch_node,
3729        Node::Extension(extension_node) => {
3730            let index = extension_node.prefix.as_ref()[0];
3731            let noderef = if extension_node.prefix.len() == 1 {
3732                extension_node.child
3733            } else {
3734                let prefix = extension_node.prefix.offset(1);
3735                let node = ExtensionNode::new(prefix, extension_node.child);
3736                NodeRef::from(Arc::new(node.into()))
3737            };
3738            let mut choices = BranchNode::EMPTY_CHOICES;
3739            choices[index as usize] = noderef;
3740            Box::new(BranchNode::new(choices))
3741        }
3742        Node::Leaf(leaf_node) => {
3743            let index = leaf_node.partial.as_ref()[0];
3744            let node = LeafNode::new(leaf_node.partial.offset(1), leaf_node.value);
3745            let mut choices = BranchNode::EMPTY_CHOICES;
3746            choices[index as usize] = NodeRef::from(Arc::new(node.into()));
3747            Box::new(BranchNode::new(choices))
3748        }
3749    }
3750}
3751
3752fn collect_trie(index: u8, mut trie: Trie) -> Result<(Box<BranchNode>, Vec<TrieNode>), TrieError> {
3753    let root = branchify(
3754        trie.root_node()?
3755            .map(Arc::unwrap_or_clone)
3756            .unwrap_or_else(|| Node::Branch(Box::default())),
3757    );
3758    trie.root = Node::Branch(root).into();
3759    let (_, mut nodes) = trie.collect_changes_since_last_hash(&NativeCrypto);
3760    nodes.retain(|(nib, _)| nib.as_ref().first() == Some(&index));
3761
3762    let Some(Node::Branch(root)) = trie.root_node()?.map(Arc::unwrap_or_clone) else {
3763        return Err(TrieError::InvalidInput);
3764    };
3765    Ok((root, nodes))
3766}
3767
3768/// Serial reference computation of an account's storage root + node diff,
3769/// identical to the per-account Stage B path. Used as a fallback for the
3770/// degenerate sharding cases (see `compute_sharded_storage_root`) where the
3771/// branchify/collapse reassembly, while producing the correct root, would emit
3772/// a different (extra/stale, always unreachable) node set than the canonical
3773/// serial path.
3774fn serial_storage_root(
3775    storage: &Store,
3776    parent_state_root: H256,
3777    hashed_address: H256,
3778    storage_root: H256,
3779    hashed_storage: &[(H256, U256)],
3780) -> Result<(H256, Vec<TrieNode>), StoreError> {
3781    let mut trie = storage.open_storage_trie(hashed_address, parent_state_root, storage_root)?;
3782    for (hashed_key, value) in hashed_storage {
3783        if value.is_zero() {
3784            trie.remove(hashed_key.as_bytes())?;
3785        } else {
3786            trie.insert(hashed_key.as_bytes().to_vec(), value.encode_to_vec())?;
3787        }
3788    }
3789    Ok(trie.collect_changes_since_last_hash(&NativeCrypto))
3790}
3791
3792/// Compute the storage root for a single account by sharding its slots across
3793/// 16 nibble-keyed workers. The returned `(H256, Vec<TrieNode>)` is
3794/// drop-in compatible with `trie.collect_changes_since_last_hash`.
3795///
3796/// `hashed_storage` order does not matter (sharding and trie inserts are
3797/// order-independent). All 16 shard threads spawn even for empty buckets: an
3798/// empty thread opens the trie, performs no insertions, and returns
3799/// `collect_trie(nibble, trie)` so the reassembled root has no holes.
3800///
3801/// Exposed (hidden) for the `ethrex-test` crate's equivalence tests; not part
3802/// of the stable public API.
3803///
3804/// Load-bearing invariant: this relies on a **path-keyed** node DB, where a node
3805/// is reachable only via its trie path. On inserts/updates and the degenerate
3806/// fallbacks below the persisted node set is bit-identical to the serial path,
3807/// but the parallel removal path may emit a few redundant nodes that are
3808/// unreachable by path and therefore harmless (never read back, never GC'd).
3809/// This is why the `occupied <= 1` fallback is sufficient and why we do *not*
3810/// also fall back on the broader "removal collapses the trie post-bucketization"
3811/// case. If the storage backend ever moves to a content-addressed / hash-keyed
3812/// node DB, or grows reachability-based GC, the parallel-removal path could leak
3813/// unreachable-but-persistent nodes and this assumption must be revisited.
3814#[doc(hidden)]
3815pub fn compute_sharded_storage_root(
3816    storage: &Store,
3817    parent_state_root: H256,
3818    hashed_address: H256,
3819    storage_root: H256,
3820    hashed_storage: &[(H256, U256)],
3821) -> Result<(H256, Vec<TrieNode>), StoreError> {
3822    // Sort by hashed key, matching the serial Stage B path (the per-account
3823    // worker). The storage root is content-addressed so order is irrelevant to
3824    // correctness, but inserting in key order walks the node arena sequentially
3825    // (cache locality) and keeps the persisted node set bit-identical to Stage B.
3826    // Applies to both the sharded path below and the serial fallbacks: bucketing
3827    // by first nibble preserves the sorted order within each bucket.
3828    // Stable sort: production inputs (a map) have unique keys, but a stable order
3829    // preserves last-write-wins for any duplicate keys, matching sequential apply.
3830    let mut sorted = hashed_storage.to_vec();
3831    sorted.sort_by(|a, b| a.0.cmp(&b.0));
3832    let hashed_storage = sorted.as_slice();
3833
3834    // Split slots into 16 buckets by the first nibble of the hashed key.
3835    let mut buckets: [Vec<(H256, U256)>; 16] = Default::default();
3836    for &(key, value) in hashed_storage {
3837        let nibble = (key.as_fixed_bytes()[0] >> 4) as usize;
3838        buckets[nibble].push((key, value));
3839    }
3840
3841    // A slot landing in the wrong nibble bucket would silently corrupt the
3842    // reassembled root (consensus failure), so guard the invariant in debug.
3843    #[cfg(debug_assertions)]
3844    for (nibble, bucket) in buckets.iter().enumerate() {
3845        for (key, _) in bucket {
3846            debug_assert_eq!(
3847                (key.as_fixed_bytes()[0] >> 4) as usize,
3848                nibble,
3849                "storage slot in wrong shard bucket"
3850            );
3851        }
3852    }
3853
3854    // Degenerate case: with <=1 occupied bucket there is no parallelism to gain,
3855    // and the branchify/collapse reassembly of a single subtree would emit an
3856    // extra (unreachable) node vs the canonical serial diff. Compute serially so
3857    // the persisted node set is bit-identical to the non-sharded path.
3858    let occupied = buckets.iter().filter(|b| !b.is_empty()).count();
3859    if occupied <= 1 {
3860        return serial_storage_root(
3861            storage,
3862            parent_state_root,
3863            hashed_address,
3864            storage_root,
3865            hashed_storage,
3866        );
3867    }
3868
3869    let mut root = BranchNode::default();
3870    let mut nodes: Vec<TrieNode> = Vec::new();
3871
3872    // All 16 shard threads spawn even for empty buckets (same rationale as
3873    // Stage C comment): each thread opens the storage trie and returns the
3874    // existing subtree at its nibble so root reassembly has no holes.
3875    std::thread::scope(|s| -> Result<(), StoreError> {
3876        let handles: Vec<_> = buckets
3877            .into_iter()
3878            .enumerate()
3879            .map(|(nibble, bucket)| {
3880                std::thread::Builder::new()
3881                    .name(format!("storage_shard_{nibble}"))
3882                    .spawn_scoped(
3883                        s,
3884                        move || -> Result<(Box<BranchNode>, Vec<TrieNode>), StoreError> {
3885                            let mut trie = storage.open_storage_trie(
3886                                hashed_address,
3887                                parent_state_root,
3888                                storage_root,
3889                            )?;
3890                            for (hashed_key, value) in &bucket {
3891                                if value.is_zero() {
3892                                    trie.remove(hashed_key.as_bytes())?;
3893                                } else {
3894                                    trie.insert(
3895                                        hashed_key.as_bytes().to_vec(),
3896                                        value.encode_to_vec(),
3897                                    )?;
3898                                }
3899                            }
3900                            collect_trie(nibble as u8, trie)
3901                                .map_err(|e| StoreError::Custom(format!("{e}")))
3902                        },
3903                    )
3904                    .map_err(|e| StoreError::Custom(format!("spawn failed: {e}")))
3905            })
3906            .collect::<Result<Vec<_>, _>>()?;
3907
3908        for (i, handle) in handles.into_iter().enumerate() {
3909            let (subroot, shard_nodes) = handle
3910                .join()
3911                .map_err(|_| StoreError::Custom("storage shard worker panicked".to_string()))??;
3912            nodes.extend(shard_nodes);
3913            root.choices[i] = subroot.choices[i].clone();
3914        }
3915        Ok(())
3916    })?;
3917
3918    // Finalize: collapse single-child branch, commit, return root hash.
3919    // prefix = Some(hashed_address) because this is a storage trie.
3920    let Some(root_node) =
3921        collapse_root_node(storage, parent_state_root, Some(hashed_address), root)?
3922    else {
3923        // The update emptied the storage trie. The sharded tombstone set differs
3924        // from the canonical serial diff (deletions of now-unreachable nodes), so
3925        // recompute serially for a bit-identical result. Rare (full storage clear).
3926        return serial_storage_root(
3927            storage,
3928            parent_state_root,
3929            hashed_address,
3930            storage_root,
3931            hashed_storage,
3932        );
3933    };
3934
3935    let mut root_ref = NodeRef::from(root_node);
3936    let root_hash = root_ref.commit(Nibbles::default(), &mut nodes, &NativeCrypto);
3937    let _ = DROP_SENDER.send(Box::new(root_ref));
3938
3939    Ok((root_hash.finalize(&NativeCrypto), nodes))
3940}