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