Skip to main content

reth_engine_tree/tree/
payload_validator.rs

1//! Types and traits for validating blocks and payloads.
2//!
3//! # Payload validation flow
4//!
5//! [`BasicEngineValidator::validate_block_with_state`] is the engine-side entry point for an
6//! inserted block or an `engine_newPayload` payload. It overlaps payload conversion, transaction
7//! preparation, cache prewarming, receipt-root computation, state-root computation, and deferred
8//! trie input construction wherever those tasks do not depend on each other.
9//!
10//! ## Validation phases
11//!
12//! 1. Fetch the parent header and spawn `payload-convert`, which converts payloads into sealed
13//!    blocks and runs header, parent-header, and pre-execution consensus validation.
14//! 2. Build the parent state provider, EVM environment, transaction iterator, lazy ancestor
15//!    overlay, and optional decoded EIP-7928 block access list (BAL).
16//! 3. Prepare the per-block state-root job. The default strategy picks a skipped, synchronous, or
17//!    sparse-trie job from [`TreeConfig`].
18//! 4. Spawn the payload processor. This always prepares transaction conversion and prewarming; a
19//!    streaming state-root job can provide a sink for prewarm and execution updates.
20//! 5. Execute the block. BAL payloads use the parallel BAL execute path only when state caching and
21//!    BAL parallel execution are enabled. Otherwise the regular executor still builds and validates
22//!    the BAL before post-execution consensus uses the decoded BAL hash.
23//! 6. Stop prewarming, terminate execution caching, spawn `hash-post-state`, await
24//!    `payload-convert` and `receipt-root`, then run post-execution consensus validation.
25//! 7. Resolve the state root by finishing the prepared job. The sparse-trie job falls back to
26//!    serial computation when the state-root task fails to produce a usable root.
27//! 8. Verify the header state root, spawn deferred trie input computation, and return the executed
28//!    block without waiting for that deferred trie task on the hot path.
29//!
30//! ## Spawned background work
31//!
32//! | Work | Spawned when | Role | Completion point |
33//! | --- | --- | --- | --- |
34//! | `payload-convert` | parent is known | convert payloads, validate header and body roots | after execution, unless the gas sanity check awaits it earlier |
35//! | `tx-iterator` | payload processor setup | convert transactions, using rayon for larger blocks | consumed by regular and BAL execution |
36//! | `prewarm` | payload processor setup | warm execution caches; in BAL mode, stream BAL-derived trie targets | stopped after execution, then caching is terminated |
37//! | proof workers | sparse-trie task setup | fetch trie proofs for sparse trie updates | consumed by the sparse trie task |
38//! | `sparse-trie` | sparse-trie task setup | apply execution or BAL updates and compute the state root | awaited by the state-root job |
39//! | `receipt-root` | execution start | compute receipt root and logs bloom incrementally | awaited before post-execution consensus |
40//! | `hash-post-state` | after execution | hash changed accounts and storage from `BundleState` | awaited by post-execution validation and root computation |
41//! | `serial-root` | sparse trie timeout fallback | race serial state-root computation against the sparse trie task | polled by the sparse-trie job |
42//! | deferred trie task | after root verification | sort trie data | not awaited by the validation hot path |
43//!
44//! ```mermaid
45//! sequenceDiagram
46//!     autonumber
47//!     participant Main as validate_block_with_state
48//!     participant Convert as payload-convert
49//!     participant Tx as tx-iterator
50//!     participant Prewarm as prewarm
51//!     participant Exec as EVM execution
52//!     participant Receipt as receipt-root
53//!     participant Trie as sparse trie and proofs
54//!     participant Hash as hash-post-state
55//!     participant Deferred as deferred trie task
56//!
57//!     Main->>Convert: spawn convert and pre-execution validation
58//!     Main->>Main: parent provider, EVM env, optional BAL decode
59//!     Main->>Tx: spawn transaction conversion
60//!     alt sparse-trie job
61//!         Main->>Trie: spawn proof workers and sparse trie
62//!     end
63//!     Main->>Prewarm: spawn transaction, BAL, or skipped prewarm
64//!     Main->>Receipt: spawn receipt root task
65//!     alt BAL path eligible
66//!         Main->>Exec: execute_block_bal
67//!         Prewarm->>Trie: BAL-derived sparse trie updates
68//!     else regular execution
69//!         Tx-->>Exec: recovered transactions in block order
70//!         Main->>Exec: execute_block
71//!         Exec->>Receipt: stream receipts
72//!         Exec->>Trie: stream state hook updates
73//!         Exec->>Exec: rebuild and validate BAL when present
74//!     end
75//!     Main->>Prewarm: stop prewarming and terminate cache
76//!     Main->>Hash: spawn changed-state hashing
77//!     Convert-->>Main: sealed block
78//!     Receipt-->>Main: receipt root and logs bloom
79//!     Main->>Main: post-execution consensus and BAL hash check
80//!     Hash-->>Main: hashed post state
81//!     alt sparse-trie job
82//!         Trie-->>Main: state root and trie updates
83//!     else synchronous or fallback
84//!         Main->>Main: compute serial StateRoot
85//!     end
86//!     Main->>Main: verify header state root
87//!     Main->>Deferred: spawn trie input sorting
88//!     Main-->>Main: return ValidationOutput
89//! ```
90//!
91//! ## Payload attributes validation
92//!
93//! During `engine_forkchoiceUpdated`,
94//! [`PayloadValidator::validate_payload_attributes_against_header`] checks payload attributes
95//! before a payload build job starts. On failure, the engine returns
96//! `INVALID_PAYLOAD_ATTRIBUTES` without rolling back the forkchoice update.
97
98use crate::tree::{
99    error::{
100        BlockAccessListDecodeError, InsertBlockError, InsertBlockErrorKind, InsertPayloadError,
101    },
102    instrumented_state::{InstrumentedStateProvider, StateProviderMetrics, StateProviderStats},
103    metrics::PrewarmReuseStats,
104    payload_processor::{
105        reuse::{PrewarmResults, ReuseMiss},
106        PayloadProcessor,
107    },
108    precompile_cache::{CachedPrecompile, CachedPrecompileMetrics, PrecompileCacheMap},
109    txpool_prewarm,
110    types::{InsertPayloadResult, ValidationOutput},
111    CacheWaitDurations, CachedStateProvider, EngineApiMetrics, EngineApiTreeState, ExecutionEnv,
112    PayloadHandle, StateProviderDatabase, TreeConfig, WaitForCaches,
113};
114use alloy_consensus::{transaction::Either, Transaction as _, TxReceipt as _};
115use alloy_eip7928::{
116    bal::{Bal, DecodedBal},
117    BlockAccessList,
118};
119use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal, NumHash};
120use alloy_evm::Evm;
121use alloy_primitives::{
122    map::{AddressMap, B256Set},
123    B256,
124};
125use reth_tasks::LazyHandle;
126
127use crate::tree::{
128    payload_processor::receipt_root_task::{IndexedReceipt, ReceiptRootTaskHandle},
129    state_root_strategy::{
130        DefaultStateRootStrategy, LazyHashedPostState, PayloadStateRootHandle,
131        PayloadStateRootJobContext, StateRootHintStream, StateRootJobContext, StateRootStrategy,
132        StateRootUpdateStream,
133    },
134};
135use alloy_primitives::Address;
136use reth_chain_state::{CanonicalInMemoryState, ExecutedBlock, ExecutionTimingStats};
137use reth_consensus::{ConsensusError, FullConsensus, ReceiptRootBloom};
138use reth_engine_primitives::{
139    ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator,
140};
141use reth_errors::{BlockExecutionError, BlockValidationError, ProviderResult};
142use reth_evm::{
143    block::{BlockExecutor, TxResult},
144    execute::ExecutableTxFor,
145    ConfigureEvm, EvmEnvFor, ExecutionCtxFor, HaltReasonFor, OnStateHook, SpecFor,
146    TxExecutionResultFor,
147};
148use reth_execution_cache::{CacheFillMode, CacheStats};
149use reth_execution_types::DecodedRevmBal;
150use reth_network_p2p::full_block::SealedBlockWithAccessList;
151use reth_payload_builder::{PayloadBuilderLease, PayloadBuilderResources};
152use reth_payload_primitives::{
153    BuiltPayload, BuiltPayloadExecutedBlock, InvalidPayloadAttributesError, NewPayloadError,
154    PayloadTypes,
155};
156use reth_primitives_traits::{
157    AlloyBlockHeader, BlockBody, BlockTy, FastInstant as Instant, GotExpected, NodePrimitives,
158    RecoveredBlock, SealedBlock, SealedHeader, SignerRecoverable, TxTy,
159};
160use reth_provider::{
161    BlockExecutionOutput, BlockReader, ChangeSetReader, DatabaseProviderFactory,
162    DatabaseProviderROFactory, EvmStateProvider, EvmStateProviderBox, HistoryReader, ProviderError,
163    PruneCheckpointReader, StageCheckpointReader, StateProvider, StateProviderFactory, StateReader,
164    StorageChangeSetReader, StorageSettingsCache,
165};
166use reth_revm::db::{states::bundle_state::BundleRetention, BundleAccount, State};
167use reth_storage_overlay::{OverlayManager, OverlayStateProviderFactory};
168use reth_trie::{updates::TrieUpdates, HashedPostState, KeccakKeyHasher, LazyTrieData};
169use revm::{
170    context::Block as _,
171    state::{bal::Bal as RevmBal, AccountInfo},
172};
173use std::{
174    sync::{
175        atomic::{AtomicUsize, Ordering},
176        Arc,
177    },
178    time::Duration,
179};
180use tracing::{debug, debug_span, error, info, instrument, trace, warn, Level, Span};
181
182pub use crate::tree::types::ValidationOutcome;
183
184/// Multiplier over the parent's gas limit beyond which a payload must pass pre-execution
185/// validation.
186const MAX_EXPECTED_GAS_LIMIT_MULTIPLIER: u64 = 2;
187
188/// Worker name for deferred trie data preparation.
189const DEFERRED_TRIE_WORKER_NAME: &str = "deferred-trie";
190
191type ReceiptRootSender<N> =
192    crossbeam_channel::Sender<IndexedReceipt<<N as NodePrimitives>::Receipt>>;
193type ReceiptRootReceiver = tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>;
194
195/// Context providing access to tree state during validation.
196///
197/// This context is provided to the [`EngineValidator`] and includes the state of the tree's
198/// internals
199pub struct TreeCtx<'a, N: NodePrimitives> {
200    /// The engine API tree state
201    state: &'a mut EngineApiTreeState<N>,
202    /// Reference to the canonical in-memory state
203    canonical_in_memory_state: &'a CanonicalInMemoryState<N>,
204}
205
206impl<'a, N: NodePrimitives> std::fmt::Debug for TreeCtx<'a, N> {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("TreeCtx")
209            .field("state", &"EngineApiTreeState")
210            .field("canonical_in_memory_state", &self.canonical_in_memory_state)
211            .finish()
212    }
213}
214
215impl<'a, N: NodePrimitives> TreeCtx<'a, N> {
216    /// Creates a new tree context
217    pub const fn new(
218        state: &'a mut EngineApiTreeState<N>,
219        canonical_in_memory_state: &'a CanonicalInMemoryState<N>,
220    ) -> Self {
221        Self { state, canonical_in_memory_state }
222    }
223}
224
225impl<'a, N: NodePrimitives> TreeCtx<'a, N> {
226    /// Returns a reference to the engine tree state
227    pub const fn state(&self) -> &EngineApiTreeState<N> {
228        &*self.state
229    }
230
231    /// Returns a mutable reference to the engine tree state
232    pub const fn state_mut(&mut self) -> &mut EngineApiTreeState<N> {
233        self.state
234    }
235
236    /// Returns a reference to the canonical in-memory state
237    pub const fn canonical_in_memory_state(&self) -> &'a CanonicalInMemoryState<N> {
238        self.canonical_in_memory_state
239    }
240}
241
242/// Pauses JIT helper execution while validating imported payloads.
243///
244/// Validation still queues JIT work and can use resident compiled code, but helper execution is
245/// paused during validation to minimize latency. Queued work resumes when validation exits, so JIT
246/// compilation is biased toward idle periods instead of competing with payload validation.
247struct JitPauseGuard<Evm: ConfigureEvm>(Evm);
248
249impl<Evm: ConfigureEvm> JitPauseGuard<Evm> {
250    fn new(evm_config: &Evm) -> Self {
251        if let Some(jit_backend) = evm_config.jit_backend() {
252            jit_backend.pause();
253        }
254        Self(evm_config.clone())
255    }
256}
257
258impl<Evm: ConfigureEvm> Drop for JitPauseGuard<Evm> {
259    fn drop(&mut self) {
260        if let Some(jit_backend) = self.0.jit_backend() {
261            jit_backend.resume();
262        }
263    }
264}
265
266/// A helper type that provides reusable payload validation logic for network-specific validators.
267///
268/// This type satisfies [`EngineValidator`] and is responsible for executing blocks/payloads.
269///
270/// This type contains common validation, execution, and state root computation logic that can be
271/// used by network-specific payload validators (e.g., Ethereum, Optimism). It is not meant to be
272/// used as a standalone component, but rather as a building block for concrete implementations.
273#[derive(derive_more::Debug)]
274pub struct BasicEngineValidator<P, Evm, V>
275where
276    Evm: ConfigureEvm,
277{
278    /// Provider for database access.
279    provider: P,
280    /// Consensus implementation for validation.
281    consensus: Arc<dyn FullConsensus<Evm::Primitives>>,
282    /// EVM configuration.
283    evm_config: Evm,
284    /// Configuration for the tree.
285    config: TreeConfig,
286    /// Payload processor for transaction conversion, prewarming, and execution caching.
287    payload_processor: PayloadProcessor<Evm>,
288    /// Precompile cache map.
289    precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
290    /// Precompile cache metrics.
291    precompile_cache_metrics: AddressMap<CachedPrecompileMetrics>,
292    /// Hook to call when invalid blocks are encountered.
293    #[debug(skip)]
294    invalid_block_hook: Box<dyn InvalidBlockHook<Evm::Primitives>>,
295    /// Metrics for the engine api.
296    metrics: EngineApiMetrics,
297    /// Validator for the payload.
298    validator: V,
299    /// Task runtime for spawning parallel work.
300    runtime: reth_tasks::Runtime,
301    /// Shared overlay manager.
302    overlay_manager: OverlayManager<Evm::Primitives>,
303    /// State-root strategy used to prepare per-block commitment tasks.
304    #[debug(skip)]
305    state_root_strategy: Arc<dyn StateRootStrategy<Evm::Primitives, P, Evm>>,
306    /// Persistent txpool prewarming worker and its latest immutable snapshot.
307    ///
308    /// None if txpool prewarming is disabled.
309    #[debug(skip)]
310    txpool_prewarm: Option<txpool_prewarm::Handle<Evm::Primitives, P, Evm>>,
311    /// Scratch buffer reused for BAL hash encoding across validated blocks.
312    bal_hash_buf: Vec<u8>,
313}
314
315impl<N, P, Evm, V> BasicEngineValidator<P, Evm, V>
316where
317    N: NodePrimitives,
318    P: DatabaseProviderFactory<
319            Provider: BlockReader
320                          + StageCheckpointReader
321                          + PruneCheckpointReader
322                          + ChangeSetReader
323                          + StorageChangeSetReader
324                          + StorageSettingsCache
325                          + HistoryReader
326                          + 'static,
327        > + BlockReader<Header = N::BlockHeader>
328        + ChangeSetReader
329        + StateProviderFactory
330        + StateReader
331        + Clone
332        + 'static,
333    Evm: ConfigureEvm<Primitives = N> + 'static,
334{
335    /// Creates a new `TreePayloadValidator`.
336    #[expect(clippy::too_many_arguments)]
337    pub fn new(
338        provider: P,
339        consensus: Arc<dyn FullConsensus<N>>,
340        evm_config: Evm,
341        validator: V,
342        config: TreeConfig,
343        invalid_block_hook: Box<dyn InvalidBlockHook<N>>,
344        overlay_manager: OverlayManager<N>,
345        runtime: reth_tasks::Runtime,
346    ) -> Self {
347        let precompile_cache_map = PrecompileCacheMap::default();
348        let payload_processor = PayloadProcessor::new(
349            runtime.clone(),
350            evm_config.clone(),
351            &config,
352            precompile_cache_map.clone(),
353        );
354        Self {
355            provider,
356            consensus,
357            evm_config,
358            payload_processor,
359            precompile_cache_map,
360            precompile_cache_metrics: AddressMap::default(),
361            config,
362            invalid_block_hook,
363            metrics: EngineApiMetrics::default(),
364            validator,
365            runtime,
366            overlay_manager,
367            state_root_strategy: Arc::new(DefaultStateRootStrategy::default()),
368            txpool_prewarm: None,
369            bal_hash_buf: Vec::new(),
370        }
371    }
372
373    /// Sets the state-root strategy used by payload validation.
374    pub fn with_state_root_strategy(
375        mut self,
376        state_root_strategy: Arc<dyn StateRootStrategy<N, P, Evm>>,
377    ) -> Self {
378        self.state_root_strategy = state_root_strategy;
379        self
380    }
381
382    /// Installs the txpool source and starts the persistent cache-prewarming worker.
383    pub fn with_txpool_prewarming(
384        mut self,
385        source: impl crate::tree::TxPoolPrewarmSource<N> + 'static,
386    ) -> Self {
387        self.txpool_prewarm = Some(txpool_prewarm::Handle::spawn(
388            &self.runtime,
389            Arc::new(source),
390            self.evm_config.clone(),
391            self.evm_config.reuses_prewarm_results() && self.config.prewarm_reuse(),
392        ));
393        self
394    }
395
396    /// Converts a [`BlockOrPayload`] to a recovered block.
397    #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
398    pub fn convert_to_block<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
399        &self,
400        input: BlockOrPayload<T>,
401    ) -> Result<SealedBlock<N::Block>, NewPayloadError>
402    where
403        V: PayloadValidator<T, Block = N::Block>,
404    {
405        match input {
406            BlockOrPayload::Payload(payload) => self.validator.convert_payload_to_block(payload),
407            BlockOrPayload::Block(block) => Ok(block.split().0),
408        }
409    }
410
411    /// Returns EVM environment for the given payload or block.
412    pub fn evm_env_for<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
413        &self,
414        input: &BlockOrPayload<T>,
415    ) -> Result<EvmEnvFor<Evm>, Evm::Error>
416    where
417        V: PayloadValidator<T, Block = N::Block>,
418        Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
419    {
420        match input {
421            BlockOrPayload::Payload(payload) => Ok(self.evm_config.evm_env_for_payload(payload)?),
422            BlockOrPayload::Block(block) => Ok(self.evm_config.evm_env(block.header())?),
423        }
424    }
425
426    /// Returns [`ExecutableTxIterator`] for the given payload or block.
427    pub fn tx_iterator_for<'a, T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
428        &'a self,
429        input: &'a BlockOrPayload<T>,
430    ) -> Result<impl ExecutableTxIterator<Evm>, NewPayloadError>
431    where
432        V: PayloadValidator<T, Block = N::Block>,
433        Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
434    {
435        Ok(match input {
436            BlockOrPayload::Payload(payload) => {
437                let iter = self
438                    .evm_config
439                    .tx_iterator_for_payload(payload)
440                    .map_err(NewPayloadError::other)?;
441                Either::Left(iter)
442            }
443            BlockOrPayload::Block(block) => {
444                let txs = block.body().clone_transactions();
445                let convert = |tx: N::SignedTx| tx.try_into_recovered();
446                Either::Right((txs, convert))
447            }
448        })
449    }
450
451    /// Returns a [`ExecutionCtxFor`] for the given payload or block.
452    pub fn execution_ctx_for<'a, T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
453        &self,
454        input: &'a BlockOrPayload<T>,
455    ) -> Result<ExecutionCtxFor<'a, Evm>, Evm::Error>
456    where
457        V: PayloadValidator<T, Block = N::Block>,
458        Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
459    {
460        match input {
461            BlockOrPayload::Payload(payload) => Ok(self.evm_config.context_for_payload(payload)?),
462            BlockOrPayload::Block(block) => Ok(self.evm_config.context_for_block(block)?),
463        }
464    }
465
466    /// Validates a block that has already been converted from a payload.
467    ///
468    /// This method performs:
469    /// - Consensus validation
470    /// - Block execution
471    /// - State root computation
472    /// - Fork detection
473    #[instrument(
474        level = "debug",
475        target = "engine::tree::payload_validator",
476        skip_all,
477        fields(
478            parent = ?input.parent_hash(),
479            type_name = ?input.type_name(),
480        )
481    )]
482    pub fn validate_block_with_state<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
483        &mut self,
484        input: BlockOrPayload<T>,
485        mut ctx: TreeCtx<'_, N>,
486    ) -> InsertPayloadResult<N>
487    where
488        V: PayloadValidator<T, Block = N::Block> + Clone,
489        Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
490    {
491        let parent_hash = input.parent_hash();
492        let _txpool_pause = self.txpool_prewarm.as_ref().map(txpool_prewarm::Handle::pause);
493        let txpool_snapshot =
494            self.txpool_prewarm.as_ref().and_then(|prewarmer| prewarmer.snapshot(parent_hash));
495        let _jit_pause = JitPauseGuard::new(&self.evm_config);
496
497        // Fetch parent block. This goes to memory most of the time unless the parent block is
498        // beyond the in-memory buffer.
499        let parent_block = match self.sealed_header_by_hash(parent_hash, ctx.state()) {
500            Ok(Some(parent_block)) => parent_block,
501            Ok(None) => {
502                return Err(InsertBlockError::new(
503                    self.convert_to_block(input)?,
504                    ProviderError::HeaderNotFound(parent_hash.into()).into(),
505                )
506                .into())
507            }
508            Err(e) => {
509                return Err(InsertBlockError::new(self.convert_to_block(input)?, e.into()).into())
510            }
511        };
512
513        // Spawn payload conversion and basic validation on a background thread so it runs
514        // concurrently with the rest of the function (setup + execution). For payloads this
515        // overlaps the cost of RLP decoding + header hashing.
516        let validated_block = self.spawn_convert_and_validate(&input, parent_block.clone());
517
518        /// A helper macro that returns the block in case there was an error
519        /// This macro is used for early returns before block conversion
520        macro_rules! ensure_ok {
521            ($expr:expr) => {
522                match $expr {
523                    Ok(val) => val,
524                    Err(e) => {
525                        let block = validated_block.try_into_inner().expect("sole handle")?;
526                        return Err(InsertBlockError::new(block, e.into()).into())
527                    }
528                }
529            };
530        }
531
532        /// A helper macro for handling errors after the input has been converted to a block
533        macro_rules! ensure_ok_post_block {
534            ($expr:expr, $block:expr) => {
535                match $expr {
536                    Ok(val) => val,
537                    Err(e) => {
538                        return Err(
539                            InsertBlockError::new($block.into_sealed_block(), e.into()).into()
540                        )
541                    }
542                }
543            };
544        }
545
546        // If the gas limit is multiple times higher than the parent's, be cautious and block on
547        // pre-execution checks of the block.
548        if input.gas_limit() >
549            parent_block.gas_limit().saturating_mul(MAX_EXPECTED_GAS_LIMIT_MULTIPLIER)
550        {
551            // Call `.get()` to await the pre-execution checks and exit early if they fail.
552            if validated_block.get().is_err() {
553                return Err(validated_block
554                    .try_into_inner()
555                    .expect("sole handle")
556                    .expect_err("Err result checked"))
557            }
558        }
559
560        trace!(target: "engine::tree::payload_validator", "Fetching block state provider");
561        let _enter =
562            debug_span!(target: "engine::tree::payload_validator", "state_provider").entered();
563        let Some(state_provider_factory) =
564            ensure_ok!(self.overlay_state_provider_factory(parent_hash, ctx.state()))
565        else {
566            // this is pre-validated in the tree
567            return Err(InsertBlockError::new(
568                validated_block.try_into_inner().expect("sole handle")?,
569                ProviderError::HeaderNotFound(parent_hash.into()).into(),
570            )
571            .into())
572        };
573        drop(_enter);
574
575        let evm_env = debug_span!(target: "engine::tree::payload_validator", "evm_env")
576            .in_scope(|| self.evm_env_for(&input))
577            .map_err(NewPayloadError::other)?;
578        let pool_seeds = self
579            .txpool_prewarm
580            .as_ref()
581            .and_then(|prewarmer| prewarmer.seeds(parent_hash, &evm_env));
582
583        // Extract the decoded BAL, if present. Undecodable block access list bytes invalidate the
584        // payload.
585        let decoded_bal =
586            ensure_ok!(input.try_decoded_access_list().map_err(BlockAccessListDecodeError::new))
587                .map(Arc::new);
588
589        if let Some(decoded_bal) = decoded_bal.as_deref() {
590            // Reject oversized BAL sidecars before executing the block.
591            ensure_ok!(decoded_bal
592                .as_bal()
593                .validate_gas_limit(input.gas_limit())
594                .map_err(ConsensusError::from));
595        }
596
597        let env = ExecutionEnv {
598            evm_env,
599            hash: input.hash(),
600            parent_hash: input.parent_hash(),
601            parent_state_root: parent_block.state_root(),
602            transaction_count: input.transaction_count(),
603            gas_used: input.gas_used(),
604            withdrawals: input.withdrawals().map(|w| w.to_vec()),
605            decoded_bal: decoded_bal.as_ref().map(Arc::clone),
606            txpool_snapshot: txpool_snapshot.clone(),
607            pool_seeds,
608        };
609
610        // Get an iterator over the transactions in the payload
611        let txs = self.tx_iterator_for(&input)?;
612
613        let parallel_bal_execution = ensure_ok!(self.bal_path_eligible(env.decoded_bal.as_deref()));
614
615        // Prepare the state-root job before execution so it can provide streaming hooks.
616        let mut state_root_job =
617            ensure_ok!(self.state_root_strategy.prepare(StateRootJobContext::new(
618                &self.runtime,
619                &self.overlay_manager,
620                &env,
621                &parent_block,
622                state_provider_factory.clone(),
623                &self.config,
624                parallel_bal_execution,
625                ctx.state_mut(),
626            )));
627        let state_root_job_name = state_root_job.name();
628
629        debug!(
630            target: "engine::tree::payload_validator",
631            strategy = state_root_job_name,
632            "Prepared state root job"
633        );
634
635        // The hook exists only when `prepare` installed it (serial path); on the parallel BAL
636        // path the authoritative capability went to the hashed update stream instead.
637        let execution_state_hook = state_root_job.take_execution_hook();
638        // The prewarm capabilities go to the code that produces their messages and are not
639        // retained anywhere else, so the task's update channel closes when producers finish.
640        let hint_stream = state_root_job.take_hint_stream();
641        let hashed_update_stream = state_root_job.take_hashed_update_stream();
642
643        // Spawn transaction conversion and prewarming.
644        let mut handle = ensure_ok!(self.spawn_payload_processor(
645            env.clone(),
646            txs,
647            state_provider_factory.clone(),
648            hint_stream,
649            hashed_update_stream,
650            parallel_bal_execution,
651        ));
652
653        // Create optional cache stats for detailed block logging
654        let slow_block_enabled = self.config.slow_block_threshold().is_some();
655        let cache_stats = slow_block_enabled.then(|| Arc::new(CacheStats::default()));
656        let instrument_state_provider = slow_block_enabled || self.config.state_provider_metrics();
657        let state_provider_metrics =
658            instrument_state_provider.then(|| StateProviderMetrics::with_source("engine"));
659        let state_provider_stats =
660            instrument_state_provider.then(|| Arc::new(StateProviderStats::default()));
661        let execution_cache = handle.caches().map(|caches| (caches, handle.cache_metrics()));
662
663        // This state provider factory is parametrized by:
664        //
665        // 1. fill_on_miss?
666        // 2. instrument_state_provider?
667        //
668        // `fill_on_miss` controls whether the loaded value after a cache miss will be inserted
669        // back into the cache. On a glance it seems to be always useful to do this. However,
670        // in practice, for the serial/non-BAL execution, it's not needed and is net negative:
671        //
672        // - It's not necessary because the revm machinery provides layer of caching itself. That
673        //   means a value for a miss will be recorded in revm's cache.
674        // - Inserting back into the cache is not free.
675        // - After execution, the execution post-state will be dumped into the execution cache as
676        //   whole anyway.
677        //
678        // Therefore, there `fill_on_miss` is going to be false for those paths.
679        //
680        // The second parameter `instrument_state_provider` controls whether we should
681        // instrument the state provider with metrics.
682        let make_state_provider = |fill_on_miss: bool| -> ProviderResult<EvmStateProviderBox> {
683            let provider = state_provider_factory.database_provider_ro()?.into_evm_state_provider();
684            let provider: EvmStateProviderBox =
685                if let Some((caches, cache_metrics)) = &execution_cache {
686                    let fill_mode = if fill_on_miss {
687                        CacheFillMode::FillOnMiss
688                    } else {
689                        CacheFillMode::LookupOnly
690                    };
691                    Box::new(
692                        CachedStateProvider::new_with_mode(
693                            provider,
694                            caches.clone(),
695                            fill_mode,
696                            cache_metrics.clone(),
697                            cache_stats.clone(),
698                        )
699                        .with_txpool_snapshot(txpool_snapshot.clone()),
700                    )
701                } else {
702                    Box::new(provider)
703                };
704
705            let provider: EvmStateProviderBox = if instrument_state_provider {
706                let stats = state_provider_stats
707                    .as_ref()
708                    .expect("instrumented state provider requires shared stats");
709                let metrics = state_provider_metrics
710                    .as_ref()
711                    .expect("instrumented state provider requires metrics");
712                Box::new(InstrumentedStateProvider::with_stats(
713                    provider,
714                    metrics.clone(),
715                    Arc::clone(stats),
716                ))
717            } else {
718                provider
719            };
720
721            Ok(provider)
722        };
723
724        // Execute the block and handle any execution errors.
725        // The receipt root task is spawned before execution and receives receipts incrementally
726        // as transactions complete, allowing parallel computation during execution.
727        let execute_block_start = Instant::now();
728        let execution_result = if parallel_bal_execution {
729            self.execute_block_bal(env, &input, &handle, &make_state_provider)
730        } else {
731            let state_provider = make_state_provider(false);
732            match state_provider {
733                Ok(state_provider) => self.execute_block(
734                    state_provider,
735                    env,
736                    &input,
737                    &mut handle,
738                    execution_state_hook,
739                ),
740                Err(err) => Err(err.into()),
741            }
742        };
743        let execution_duration = execute_block_start.elapsed();
744        if let (Some(metrics), Some(stats)) = (&state_provider_metrics, &state_provider_stats) {
745            metrics.record_totals(stats);
746        }
747        let (output, senders, receipt_root_rx, executed_bal) = ensure_ok!(execution_result);
748        let (built_bal, revm_bal) =
749            executed_bal.map(|ExecutedBal { alloy, revm }| (alloy, revm)).unzip();
750
751        // After executing the block we can stop prewarming transactions
752        handle.stop_prewarming_execution();
753
754        // Create ExecutionOutcome early so we can terminate caching before validation and state
755        // root computation. Using Arc allows sharing with both the caching task and the deferred
756        // trie task without cloning the expensive BundleState.
757        let output = Arc::new(output);
758
759        // Terminate caching task early since execution is complete and caching is no longer
760        // needed. This frees up resources while state root computation continues.
761        let valid_block_tx = handle.terminate_caching(Some(output.clone()));
762
763        // Spawn hashed post state computation in background so it runs concurrently with
764        // block conversion and receipt root computation. This is a pure CPU-bound task
765        // (keccak256 hashing of all changed addresses and storage slots).
766        let hashed_state_output = output.clone();
767        let mut hashed_state_rx = state_root_job.take_hashed_state_rx();
768        let parent_span = Span::current();
769        let mut hashed_state: LazyHashedPostState =
770            self.runtime.spawn_blocking_named("hash-post-state", move || {
771                let _span = debug_span!(
772                    target: "engine::tree::payload_validator",
773                    parent: parent_span,
774                    "hashed_post_state",
775                )
776                .entered();
777                if let Some(Ok(state)) = hashed_state_rx.as_mut().map(|rx| rx.recv()) {
778                    state
779                } else {
780                    Arc::new(HashedPostState::from_bundle_state::<KeccakKeyHasher>(
781                        hashed_state_output.state.state(),
782                    ))
783                }
784            });
785
786        let block = validated_block.try_into_inner().expect("sole handle")?;
787        let block = block.with_senders(senders);
788
789        // Wait for the receipt root computation to complete.
790        let receipt_root_bloom = {
791            let _enter = debug_span!(
792                target: "engine::tree::payload_validator",
793                "wait_receipt_root",
794            )
795            .entered();
796
797            receipt_root_rx
798                .blocking_recv()
799                .inspect_err(|_| {
800                    tracing::error!(
801                        target: "engine::tree::payload_validator",
802                        "Receipt root task dropped sender without result, receipt root calculation likely aborted"
803                    );
804                })
805                .ok()
806        };
807
808        ensure_ok_post_block!(
809            self.validate_post_execution(
810                &block,
811                &parent_block,
812                &output,
813                &mut ctx,
814                receipt_root_bloom,
815                built_bal
816            ),
817            block
818        );
819
820        let mut hashed_state_validate_result = debug_span!(
821            target: "engine::tree::payload_validator",
822            "validate_block_post_execution_with_hashed_state"
823        )
824        .in_scope(|| {
825            self.validator.validate_block_post_execution_with_hashed_state(
826                || hashed_state.get(),
827                &block,
828                &parent_block,
829                || {
830                    state_provider_factory
831                        .database_provider_ro()
832                        .map(|provider| Box::new(provider) as _)
833                },
834            )
835        });
836
837        let root_start = Instant::now();
838        let root_outcome = ensure_ok_post_block!(
839            state_root_job.finish(&block, output.clone(), &hashed_state),
840            block
841        );
842        let root_elapsed = root_start.elapsed();
843
844        info!(
845            target: "engine::tree::payload_validator",
846            strategy = state_root_job_name,
847            state_root = ?root_outcome.state_root,
848            elapsed = ?root_elapsed,
849            "State root job finished"
850        );
851
852        let state_root = root_outcome.state_root;
853        let trie_output = root_outcome.trie_updates;
854
855        // A fallback path recomputed the hashed post state. Replace the streaming-derived one
856        // and re-run hashed-state validation against it, since a failed state-root task may
857        // have produced an inconsistent byproduct.
858        if let Some(refreshed) = root_outcome.hashed_state {
859            hashed_state = LazyHandle::ready(refreshed);
860            hashed_state_validate_result = debug_span!(
861                target: "engine::tree::payload_validator",
862                "validate_block_post_execution_with_hashed_state"
863            )
864            .in_scope(|| {
865                self.validator.validate_block_post_execution_with_hashed_state(
866                    || hashed_state.get(),
867                    &block,
868                    &parent_block,
869                    || {
870                        state_provider_factory
871                            .database_provider_ro()
872                            .map(|provider| Box::new(provider) as _)
873                    },
874                )
875            });
876        }
877
878        if let Err(err) = hashed_state_validate_result {
879            if err.is_validation_error() {
880                self.on_invalid_block(&parent_block, &block, &output, None, ctx.state_mut());
881            }
882            return Err(InsertBlockError::new(block.into_sealed_block(), err).into())
883        }
884
885        self.metrics.block_validation.record_state_root(&trie_output, root_elapsed.as_secs_f64());
886        self.metrics
887            .record_state_root_gas_bucket(block.header().gas_used(), root_elapsed.as_secs_f64());
888        debug!(target: "engine::tree::payload_validator", ?root_elapsed, "Calculated state root");
889
890        // ensure state root matches
891        if state_root != block.header().state_root() {
892            // call post-block hook
893            self.on_invalid_block(
894                &parent_block,
895                &block,
896                &output,
897                Some((&trie_output, state_root)),
898                ctx.state_mut(),
899            );
900            let block_state_root = block.header().state_root();
901            return Err(InsertBlockError::new(
902                block.into_sealed_block(),
903                ConsensusError::BodyStateRootDiff(
904                    GotExpected { got: state_root, expected: block_state_root }.into(),
905                )
906                .into(),
907            )
908            .into())
909        }
910
911        let timing_stats = state_provider_stats.filter(|_| slow_block_enabled).map(|stats| {
912            self.calculate_timing_stats(
913                &block,
914                stats,
915                cache_stats,
916                &output,
917                execution_duration,
918                root_elapsed,
919            )
920        });
921
922        if let Some(valid_block_tx) = valid_block_tx {
923            let _ = valid_block_tx.send(());
924        }
925
926        // The payload's raw bytes are already bound to this header: payload-to-block conversion
927        // and downloaded-sidecar verification both check their hash against it, so pairing them
928        // with the BAL this execution produced is sound. The zip leaves the BAL unset for a
929        // downloaded block that carried only a BAL hash and no sidecar.
930        let bal = revm_bal.zip(decoded_bal).map(|(revm_bal, decoded_bal)| {
931            Arc::new(DecodedRevmBal::with_raw_bal(revm_bal, decoded_bal.as_raw_bal().clone()))
932        });
933        let executed_block = self
934            .spawn_deferred_trie_task(Arc::new(block), output, hashed_state, trie_output)
935            .with_bal(bal);
936        Ok(ValidationOutput::new(executed_block, timing_stats))
937    }
938
939    /// Spawns a background task to convert a [`BlockOrPayload`] into a [`SealedBlock`] and perform
940    /// basic consensus validations on it.
941    #[expect(clippy::type_complexity)]
942    pub fn spawn_convert_and_validate<T>(
943        &self,
944        input: &BlockOrPayload<T>,
945        parent: SealedHeader<N::BlockHeader>,
946    ) -> LazyHandle<Result<SealedBlock<N::Block>, InsertPayloadError<N::Block>>>
947    where
948        T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
949        V: PayloadValidator<T, Block = N::Block> + Clone,
950    {
951        let input = input.clone();
952        let validator = self.validator.clone();
953        let consensus = self.consensus.clone();
954        let parent_span = Span::current();
955        self.runtime.spawn_blocking_named("payload-convert", move || {
956            let _span = debug_span!(
957                target: "engine::tree::payload_validator",
958                parent: parent_span,
959                "convert_and_validate",
960            )
961            .entered();
962            let block = match input {
963                BlockOrPayload::Block(block) => block.split().0,
964                BlockOrPayload::Payload(payload) => {
965                    validator.convert_payload_to_block(payload)?
966                }
967            };
968
969            if let Err(e) = consensus.validate_header(block.sealed_header()) {
970                error!(target: "engine::tree::payload_validator", block = ?block.block_with_parent(), %e, "Failed to validate header");
971                return Err(InsertBlockError::consensus_error(e, block).into())
972            }
973
974            // now validate against the parent
975            let _enter = debug_span!(target: "engine::tree::payload_validator", "validate_header_against_parent").entered();
976            if let Err(e) = consensus.validate_header_against_parent(block.sealed_header(), &parent)
977            {
978                warn!(target: "engine::tree::payload_validator", block = ?block.block_with_parent(), %e, "Failed to validate header against parent");
979                return Err(InsertBlockError::consensus_error(e, block).into())
980            }
981            drop(_enter);
982
983            if let Err(e) =
984                consensus.validate_block_pre_execution_with_tx_root(&block, None)
985            {
986                error!(target: "engine::tree::payload_validator", block = ?block.block_with_parent(), %e, "Failed to validate block");
987                return Err(InsertBlockError::consensus_error(e, block).into())
988            }
989
990            Ok(block)
991        })
992    }
993
994    /// Return sealed block header from database or in-memory state by hash.
995    fn sealed_header_by_hash(
996        &self,
997        hash: B256,
998        state: &EngineApiTreeState<N>,
999    ) -> ProviderResult<Option<SealedHeader<N::BlockHeader>>> {
1000        // check memory first
1001        let header = state.tree_state.sealed_header_by_hash(&hash);
1002
1003        if header.is_some() {
1004            Ok(header)
1005        } else {
1006            self.provider.sealed_header_by_hash(hash)
1007        }
1008    }
1009
1010    /// Executes a block with the given state provider.
1011    ///
1012    /// This method orchestrates block execution:
1013    /// 1. Sets up the EVM with state database and precompile caching
1014    /// 2. Spawns a background task for incremental receipt root computation
1015    /// 3. Executes transactions with metrics collection via state hooks
1016    /// 4. Merges state transitions and records execution metrics
1017    #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
1018    #[expect(clippy::type_complexity)]
1019    fn execute_block<S, Err, T>(
1020        &mut self,
1021        state_provider: S,
1022        env: ExecutionEnv<Evm>,
1023        input: &BlockOrPayload<T>,
1024        handle: &mut PayloadHandle<impl ExecutableTxFor<Evm>, Err, N::Receipt, HaltReasonFor<Evm>>,
1025        state_hook: Option<Box<dyn OnStateHook + 'static>>,
1026    ) -> Result<
1027        (BlockExecutionOutput<N::Receipt>, Vec<Address>, ReceiptRootReceiver, Option<ExecutedBal>),
1028        InsertBlockErrorKind,
1029    >
1030    where
1031        S: EvmStateProvider + Send,
1032        Err: core::error::Error + Send + Sync + 'static,
1033        V: PayloadValidator<T, Block = N::Block>,
1034        T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
1035        Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
1036    {
1037        debug!(target: "engine::tree::payload_validator", "Executing block");
1038
1039        let has_bal = input.has_block_access_list();
1040        let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
1041            State::builder()
1042                .with_database(StateProviderDatabase::new(state_provider))
1043                .with_bundle_update()
1044                .with_bal_builder_if(has_bal)
1045                .build()
1046        });
1047
1048        let (spec_id, mut executor) = {
1049            let _span = debug_span!(target: "engine::tree", "create_evm").entered();
1050            let spec_id = *env.evm_env.spec_id();
1051            let evm_config = self.evm_config.clone().with_jit_support();
1052            let evm = evm_config.evm_with_env(&mut db, env.evm_env);
1053            let ctx = self
1054                .execution_ctx_for(input)
1055                .map_err(|e| InsertBlockErrorKind::Other(Box::new(e)))?;
1056            let executor = self.evm_config.create_executor(evm, ctx);
1057            (spec_id, executor)
1058        };
1059
1060        if !self.config.precompile_cache_disabled() {
1061            let _span = debug_span!(target: "engine::tree", "setup_precompile_cache").entered();
1062            executor.evm_mut().precompiles_mut().map_cacheable_precompiles(
1063                |address, precompile| {
1064                    let metrics = self
1065                        .precompile_cache_metrics
1066                        .entry(*address)
1067                        .or_insert_with(|| CachedPrecompileMetrics::new_with_address(*address))
1068                        .clone();
1069                    CachedPrecompile::wrap(
1070                        precompile,
1071                        self.precompile_cache_map.cache_for_address(*address),
1072                        spec_id,
1073                        Some(metrics),
1074                    )
1075                },
1076            );
1077        }
1078
1079        let transaction_count = input.transaction_count();
1080        let (receipt_tx, result_rx) = self.spawn_receipt_root_task(transaction_count);
1081        let executed_tx_index = Arc::clone(handle.executed_tx_index());
1082        // Reads made to validate prewarm results would be recorded in the built BAL.
1083        let prewarm_results = handle.prewarm_results().filter(|_| !has_bal);
1084        executor.evm_mut().db_mut().set_state_hook(state_hook);
1085
1086        let execution_start = Instant::now();
1087
1088        // Execute all transactions and finalize
1089        let (executor, senders) = self.execute_transactions(
1090            executor,
1091            transaction_count,
1092            handle.iter_transactions(),
1093            &receipt_tx,
1094            &executed_tx_index,
1095            has_bal,
1096            prewarm_results.as_deref(),
1097        )?;
1098        drop(receipt_tx);
1099        // Free stale results off the execution path.
1100        if let Some(prewarm_results) = prewarm_results {
1101            self.runtime.spawn_drop(prewarm_results);
1102        }
1103
1104        // Finish execution and get the result
1105        let post_exec_start = Instant::now();
1106        let (_evm, result) = debug_span!(target: "engine::tree", "BlockExecutor::finish")
1107            .in_scope(|| executor.finish())
1108            .map(|(evm, result)| (evm.into_db(), result))?;
1109        self.metrics.record_post_execution(post_exec_start.elapsed());
1110
1111        // Merge transitions into bundle state
1112        debug_span!(target: "engine::tree", "merge_transitions")
1113            .in_scope(|| db.merge_transitions(BundleRetention::Reverts));
1114
1115        // The builder only exists when the block declares a BAL. The revm form is the one shared
1116        // with the executed block, so it must survive here; the clone only feeds the
1117        // post-execution consensus checks, the sole consumer of the alloy form.
1118        let built_bal = db.take_built_bal().map(|revm_bal| ExecutedBal {
1119            alloy: revm_bal.clone().into_alloy_bal(),
1120            revm: Arc::new(revm_bal),
1121        });
1122        let output = BlockExecutionOutput { result, state: db.take_bundle() };
1123
1124        let execution_duration = execution_start.elapsed();
1125        self.metrics.record_block_execution(&output, execution_duration);
1126        self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
1127        debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
1128
1129        Ok((output, senders, result_rx, built_bal))
1130    }
1131
1132    /// Returns true when the BAL execute path should be used for this block.
1133    // TODO: extend with stronger gating before enabling on mainnet:
1134    //   - Fork check: `Amsterdam.active_at_timestamp(env.evm_env.timestamp)`. Today a BAL only
1135    //     exists post-Amsterdam, so the BAL-presence check is a sufficient proxy. It is a proxy,
1136    //     not a guarantee.
1137    //   - Tx-count threshold (`bal_execute_path_min_tx_count`): below the parallelism break-even
1138    //     point, provider setup and worker scheduling overhead can exceed the gain. Tune
1139    //     empirically once workers are parallel; meaningless while the commit loop is sequential.
1140    fn bal_path_eligible(&self, bal: Option<&DecodedBal>) -> Result<bool, InsertBlockErrorKind> {
1141        let has_bal = bal.is_some();
1142        let parallel_execution = has_bal && !self.config.disable_bal_parallel_execution();
1143        if parallel_execution && self.config.disable_bal_parallel_state_root() {
1144            return Err(InsertBlockErrorKind::Other(
1145                "disabling parallel state root is impossible when parallel execution is enabled"
1146                    .into(),
1147            ));
1148        }
1149
1150        Ok(parallel_execution)
1151    }
1152
1153    /// Executes the block on the BAL path. Mirrors the return shape of [`Self::execute_block`]
1154    /// so the dispatch site stays uniform.
1155    ///
1156    /// Inside, this:
1157    /// 1. Creates a shared parent-state cache handle for provider-backed workers.
1158    /// 2. Relies on BAL prewarm to stream state-root updates and optional state prefetches.
1159    /// 3. Spawns the receipt-root task.
1160    /// 4. Calls [`crate::tree::payload_processor::bal::execute_block`].
1161    /// 5. Returns the rebuilt BAL for post-execution consensus validation, paired with the revm
1162    ///    representation the workers consumed.
1163    #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
1164    #[expect(clippy::type_complexity)]
1165    fn execute_block_bal<Tx, Err, MakeStateProvider, T>(
1166        &self,
1167        env: ExecutionEnv<Evm>,
1168        input: &BlockOrPayload<T>,
1169        handle: &PayloadHandle<Tx, Err, N::Receipt, HaltReasonFor<Evm>>,
1170        make_state_provider: &MakeStateProvider,
1171    ) -> Result<
1172        (BlockExecutionOutput<N::Receipt>, Vec<Address>, ReceiptRootReceiver, Option<ExecutedBal>),
1173        InsertBlockErrorKind,
1174    >
1175    where
1176        Tx: ExecutableTxFor<Evm> + Send,
1177        Err: core::error::Error + Send + Sync + 'static,
1178        MakeStateProvider: Fn(bool) -> ProviderResult<EvmStateProviderBox> + Sync,
1179        Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
1180        T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
1181        V: PayloadValidator<T, Block = N::Block>,
1182    {
1183        debug!(target: "engine::tree::payload_validator", "Executing block via BAL path");
1184
1185        let (receipt_tx, result_rx) = self.spawn_receipt_root_task(env.transaction_count);
1186        let input_bal = env.decoded_bal.ok_or_else(|| {
1187            InsertBlockErrorKind::Other("BAL execute path: no decoded BAL available".into())
1188        })?;
1189
1190        let make_db = |fill_on_miss| {
1191            let provider = make_state_provider(fill_on_miss)
1192                .map_err(crate::tree::payload_processor::bal::BalExecutionError::Provider)?;
1193            Ok(StateProviderDatabase::new(provider))
1194        };
1195        let execution_start = Instant::now();
1196        let ctx =
1197            self.execution_ctx_for(input).map_err(|e| InsertBlockErrorKind::Other(Box::new(e)))?;
1198        let (output, senders, built_bal, received_bal_revm) =
1199            crate::tree::payload_processor::bal::execute_block(
1200                &self.runtime,
1201                &self.evm_config,
1202                &make_db,
1203                input_bal,
1204                env.evm_env,
1205                ctx,
1206                env.transaction_count,
1207                handle.clone_transaction_receiver(),
1208                receipt_tx,
1209            )?;
1210        let execution_duration = execution_start.elapsed();
1211
1212        self.metrics.record_block_execution(&output, execution_duration);
1213        self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
1214        debug!(
1215            target: "engine::tree::payload_validator",
1216            elapsed = ?execution_duration,
1217            "Executed block via BAL path",
1218        );
1219
1220        // The received BAL was already converted to the revm form to drive the workers. Once the
1221        // post-execution hash check against the rebuilt BAL passes, both have the same content, so
1222        // the converted one can be reused instead of converting the rebuilt BAL again.
1223        Ok((
1224            output,
1225            senders,
1226            result_rx,
1227            Some(ExecutedBal { alloy: built_bal, revm: received_bal_revm }),
1228        ))
1229    }
1230
1231    fn spawn_receipt_root_task(
1232        &self,
1233        receipts_len: usize,
1234    ) -> (ReceiptRootSender<N>, ReceiptRootReceiver) {
1235        // Unbounded channel is used since tx count bounds capacity anyway.
1236        let (receipt_tx, receipt_rx) = crossbeam_channel::unbounded();
1237        let (result_tx, result_rx) = tokio::sync::oneshot::channel();
1238        let task_handle = ReceiptRootTaskHandle::new(receipt_rx, result_tx);
1239        self.runtime.spawn_blocking_named("receipt-root", move || task_handle.run(receipts_len));
1240
1241        (receipt_tx, result_rx)
1242    }
1243
1244    /// Executes transactions and collects senders, streaming receipts to a background task.
1245    ///
1246    /// This method handles:
1247    /// - Applying pre-execution changes (e.g., beacon root updates)
1248    /// - Executing each transaction with timing metrics
1249    /// - Streaming receipts to the receipt root computation task
1250    /// - Collecting transaction senders for later use
1251    ///
1252    /// Returns the executor (for finalization) and the collected senders.
1253    #[expect(clippy::too_many_arguments)]
1254    fn execute_transactions<'a, E, Tx, Err, DB>(
1255        &self,
1256        mut executor: E,
1257        transaction_count: usize,
1258        transactions: impl Iterator<Item = Result<Tx, Err>>,
1259        receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
1260        executed_tx_index: &AtomicUsize,
1261        has_bal: bool,
1262        prewarm_results: Option<&PrewarmResults<HaltReasonFor<Evm>>>,
1263    ) -> Result<(E, Vec<Address>), BlockExecutionError>
1264    where
1265        E: BlockExecutor<
1266            Receipt = N::Receipt,
1267            Result = TxExecutionResultFor<Evm>,
1268            Evm: alloy_evm::Evm<DB = &'a mut State<DB>, HaltReason = HaltReasonFor<Evm>>,
1269        >,
1270        Tx: alloy_evm::block::ExecutableTx<E> + alloy_evm::RecoveredTx<TxTy<N>>,
1271        DB: revm::Database + 'a,
1272        Err: core::error::Error + Send + Sync + 'static,
1273    {
1274        let mut senders = Vec::with_capacity(transaction_count);
1275        let mut reuse_stats = PrewarmReuseStats::default();
1276
1277        // Apply pre-execution changes (e.g., beacon root update)
1278        let pre_exec_start = Instant::now();
1279        debug_span!(target: "engine::tree", "pre_execution")
1280            .in_scope(|| executor.apply_pre_execution_changes())?;
1281        self.metrics.record_pre_execution(pre_exec_start.elapsed());
1282
1283        // Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
1284        if has_bal {
1285            executor.evm_mut().db_mut().bump_bal_index();
1286        }
1287
1288        // Execute transactions
1289        let exec_span = debug_span!(target: "engine::tree", "execution").entered();
1290        let mut transactions = transactions.into_iter();
1291        // Some executors may execute transactions that do not append receipts during the
1292        // main loop (e.g., system transactions whose receipts are added during finalization).
1293        // In that case, invoking the callback on every transaction would resend the previous
1294        // receipt with the same index and can panic the ordered root builder.
1295        let mut last_sent_len = 0usize;
1296        loop {
1297            // Measure time spent waiting for next transaction from iterator
1298            // (e.g., parallel signature recovery)
1299            let wait_start = Instant::now();
1300            let Some(tx_result) = transactions.next() else { break };
1301            self.metrics.record_transaction_wait(wait_start.elapsed());
1302
1303            let tx = tx_result.map_err(BlockValidationError::other)?;
1304            let tx_signer = *<Tx as alloy_evm::RecoveredTx<TxTy<N>>>::signer(&tx);
1305
1306            senders.push(tx_signer);
1307
1308            let _enter = tracing::enabled!(target: "engine::tree", Level::TRACE).then(|| {
1309                tracing::trace_span!(
1310                    target: "engine::tree",
1311                    "execute tx",
1312                    tx_index = senders.len() - 1,
1313                )
1314                .entered()
1315            });
1316            if tracing::enabled!(target: "engine::tree", Level::TRACE) {
1317                trace!(target: "engine::tree", "Executing transaction");
1318            }
1319
1320            let tx_start = Instant::now();
1321            let index = senders.len() - 1;
1322            match prewarm_results {
1323                Some(results) => {
1324                    let outcome = self.commit_prewarm_result(
1325                        &mut executor,
1326                        results,
1327                        index,
1328                        <Tx as alloy_evm::RecoveredTx<TxTy<N>>>::tx(&tx),
1329                    )?;
1330                    if outcome.is_err() {
1331                        // Later prewarm results are refreshed against the actual writes.
1332                        executor.execute_transaction_with_result_closure(tx, |result| {
1333                            results.publish_executed(index, &result.result().state)
1334                        })?;
1335                    }
1336                    reuse_stats.record(outcome);
1337                }
1338                None => {
1339                    executor.execute_transaction(tx)?;
1340                }
1341            }
1342            self.metrics.record_transaction_execution(tx_start.elapsed());
1343
1344            // advance the shared counter so prewarm workers skip already-executed txs
1345            executed_tx_index.store(senders.len(), Ordering::Relaxed);
1346
1347            let current_len = executor.receipts().len();
1348            if current_len > last_sent_len {
1349                last_sent_len = current_len;
1350                // Send the latest receipt to the background task for incremental root computation.
1351                if let Some(receipt) = executor.receipts().last() {
1352                    let tx_index = current_len - 1;
1353                    let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
1354                }
1355            }
1356            // Bump BAL index after each transaction (EIP-7928)
1357            if has_bal {
1358                executor.evm_mut().db_mut().bump_bal_index();
1359            }
1360        }
1361
1362        drop(exec_span);
1363
1364        if prewarm_results.is_some() {
1365            self.metrics.prewarm_reuse.record(reuse_stats);
1366        }
1367
1368        Ok((executor, senders))
1369    }
1370
1371    /// Commits the prewarm result of the transaction at `index` instead of executing it, if the
1372    /// result is ready and its reads match the current state.
1373    fn commit_prewarm_result<'a, E, DB>(
1374        &self,
1375        executor: &mut E,
1376        results: &PrewarmResults<HaltReasonFor<Evm>>,
1377        index: usize,
1378        tx: &TxTy<N>,
1379    ) -> Result<Result<(), ReuseMiss>, BlockExecutionError>
1380    where
1381        E: BlockExecutor<
1382            Receipt = N::Receipt,
1383            Result = TxExecutionResultFor<Evm>,
1384            Evm: alloy_evm::Evm<DB = &'a mut State<DB>, HaltReason = HaltReasonFor<Evm>>,
1385        >,
1386        DB: revm::Database + 'a,
1387    {
1388        // Skipping execution bypasses the block executor's available gas check. Ignoring the
1389        // transaction gas limit cap makes this check stricter; normal execution handles the rest.
1390        let block = executor.evm().block();
1391        let block_gas_used = executor.receipts().last().map_or(0, |r| r.cumulative_gas_used());
1392        if tx.gas_limit() > block.gas_limit().saturating_sub(block_gas_used) {
1393            return Ok(Err(ReuseMiss::Stale))
1394        }
1395
1396        let result = match results
1397            .take_valid(index, executor.evm_mut().db_mut())
1398            .map_err(BlockExecutionError::other)?
1399        {
1400            Ok(result) => result,
1401            Err(miss) => return Ok(Err(miss)),
1402        };
1403        let Some(result) = self.evm_config.prewarm_tx_result(tx, result) else {
1404            return Ok(Err(ReuseMiss::Stale))
1405        };
1406        executor.commit_transaction(result);
1407        Ok(Ok(()))
1408    }
1409
1410    /// Validates the block after execution.
1411    ///
1412    /// This performs:
1413    /// - parent header validation
1414    /// - post-execution consensus validation
1415    /// - state-root based post-execution validation
1416    ///
1417    /// If `receipt_root_bloom` is provided, it will be used instead of computing the receipt root
1418    /// and logs bloom from the receipts.
1419    ///
1420    /// The `hashed_state` handle wraps the background hashed post state computation.
1421    #[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
1422    fn validate_post_execution<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
1423        &mut self,
1424        block: &RecoveredBlock<N::Block>,
1425        parent_block: &SealedHeader<N::BlockHeader>,
1426        output: &BlockExecutionOutput<N::Receipt>,
1427        ctx: &mut TreeCtx<'_, N>,
1428        receipt_root_bloom: Option<ReceiptRootBloom>,
1429        built_bal: Option<BlockAccessList>,
1430    ) -> Result<(), InsertBlockErrorKind>
1431    where
1432        V: PayloadValidator<T, Block = N::Block>,
1433    {
1434        let start = Instant::now();
1435
1436        trace!(target: "engine::tree::payload_validator", block=?block.num_hash(), "Validating block consensus");
1437
1438        // Validate block post-execution rules
1439        let _enter =
1440            debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution")
1441                .entered();
1442        let built_bal = built_bal.map(Bal::from);
1443        let block_access_list_hash =
1444            built_bal.as_ref().map(|bal| bal.compute_hash_with_buf(&mut self.bal_hash_buf));
1445
1446        let validation_result = built_bal
1447            .as_ref()
1448            .map(|bal| bal.validate_gas_limit(block.gas_limit()).map_err(ConsensusError::from))
1449            .transpose()
1450            .and_then(|_| {
1451                self.consensus.validate_block_post_execution(
1452                    block,
1453                    output,
1454                    receipt_root_bloom,
1455                    block_access_list_hash,
1456                )
1457            });
1458
1459        if let Err(err) = validation_result {
1460            // call post-block hook
1461            self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
1462            return Err(err.into())
1463        }
1464        drop(_enter);
1465
1466        // record post-execution validation duration
1467        self.metrics
1468            .block_validation
1469            .post_execution_validation_duration
1470            .record(start.elapsed().as_secs_f64());
1471
1472        Ok(())
1473    }
1474
1475    /// Spawns transaction conversion and cache prewarming for payload validation.
1476    ///
1477    /// State-root tasks are prepared before this method and can provide capabilities that
1478    /// prewarm uses for BAL-derived authoritative updates or transaction-derived hints.
1479    #[instrument(
1480        level = "debug",
1481        target = "engine::tree::payload_validator",
1482        skip_all,
1483        fields(
1484            has_hint_stream = hint_stream.is_some(),
1485            has_hashed_update_stream = hashed_update_stream.is_some(),
1486            parallel_bal_execution
1487        )
1488    )]
1489    fn spawn_payload_processor<T: ExecutableTxIterator<Evm>>(
1490        &self,
1491        env: ExecutionEnv<Evm>,
1492        txs: T,
1493        state_provider_factory: OverlayStateProviderFactory<P, N>,
1494        hint_stream: Option<StateRootHintStream>,
1495        hashed_update_stream: Option<StateRootUpdateStream>,
1496        parallel_bal_execution: bool,
1497    ) -> Result<
1498        PayloadHandle<
1499            impl ExecutableTxFor<Evm> + use<N, P, Evm, V, T>,
1500            impl core::error::Error + Send + Sync + 'static + use<N, P, Evm, V, T>,
1501            N::Receipt,
1502            HaltReasonFor<Evm>,
1503        >,
1504        InsertBlockErrorKind,
1505    > {
1506        let start = Instant::now();
1507        let handle = self.payload_processor.spawn_with_state_root_streams(
1508            env,
1509            txs,
1510            state_provider_factory,
1511            hint_stream,
1512            hashed_update_stream,
1513            parallel_bal_execution,
1514        );
1515
1516        self.metrics.block_validation.spawn_payload_processor.record(start.elapsed().as_secs_f64());
1517
1518        Ok(handle)
1519    }
1520
1521    /// Creates an overlay state provider factory for the given parent hash.
1522    ///
1523    /// Returns `None` when the parent is neither in memory nor persisted.
1524    fn overlay_state_provider_factory(
1525        &self,
1526        hash: B256,
1527        state: &EngineApiTreeState<N>,
1528    ) -> ProviderResult<Option<OverlayStateProviderFactory<P, N>>> {
1529        if !state.tree_state.contains_hash(&hash) && self.provider.header(hash)?.is_none() {
1530            debug!(target: "engine::tree::payload_validator", %hash, "no canonical state found for block");
1531            return Ok(None)
1532        }
1533
1534        Ok(Some(OverlayStateProviderFactory::new(
1535            self.provider.clone(),
1536            state.tree_state.overlay_manager.overlay_builder(hash),
1537        )))
1538    }
1539
1540    /// Called when an invalid block is encountered during validation.
1541    fn on_invalid_block(
1542        &self,
1543        parent_header: &SealedHeader<N::BlockHeader>,
1544        block: &RecoveredBlock<N::Block>,
1545        output: &BlockExecutionOutput<N::Receipt>,
1546        trie_updates: Option<(&TrieUpdates, B256)>,
1547        state: &mut EngineApiTreeState<N>,
1548    ) {
1549        if state.invalid_headers.get(&block.hash()).is_some() {
1550            // we already marked this block as invalid
1551            return
1552        }
1553        self.invalid_block_hook.on_invalid_block(parent_header, block, output, trie_updates);
1554    }
1555
1556    /// Prepares the optional payload-builder state-root handle through the installed
1557    /// [`StateRootStrategy`].
1558    fn payload_state_root_handle_for(
1559        &self,
1560        parent_hash: B256,
1561        parent_header: &N::BlockHeader,
1562        timestamp: u64,
1563        state: &mut EngineApiTreeState<N>,
1564    ) -> Option<PayloadStateRootHandle> {
1565        let state_provider_factory = match self.overlay_state_provider_factory(parent_hash, state) {
1566            Ok(Some(state_provider_factory)) => state_provider_factory,
1567            Ok(None) => return None,
1568            Err(err) => {
1569                warn!(
1570                    target: "engine::tree::payload_validator",
1571                    %err,
1572                    %parent_hash,
1573                    "failed to prepare payload-builder state-root provider"
1574                );
1575                return None
1576            }
1577        };
1578        match self.state_root_strategy.prepare_payload_builder(PayloadStateRootJobContext::new(
1579            &self.runtime,
1580            &self.overlay_manager,
1581            parent_hash,
1582            parent_header,
1583            timestamp,
1584            state,
1585            state_provider_factory,
1586            &self.config,
1587        )) {
1588            Ok(handle) => handle,
1589            Err(err) => {
1590                warn!(
1591                    target: "engine::tree::payload_validator",
1592                    %err,
1593                    %parent_hash,
1594                    "failed to prepare payload-builder state-root job"
1595                );
1596                None
1597            }
1598        }
1599    }
1600
1601    /// Spawns a background task to compute and sort trie data for the executed block.
1602    ///
1603    /// This function creates a [`LazyTrieData`] handle and spawns a blocking task that:
1604    /// 1. Sort the block's hashed state and trie updates
1605    /// 2. Publishes the result so subsequent calls return immediately
1606    ///
1607    /// If the background task hasn't completed when `trie_data()` is called, callers wait for the
1608    /// publishing task instead of computing synchronously.
1609    ///
1610    /// The validation hot path can return immediately after state root verification,
1611    /// while consumers (DB writes, overlay providers, proofs) get trie data from the completed
1612    /// task.
1613    fn spawn_deferred_trie_task(
1614        &self,
1615        block: Arc<RecoveredBlock<N::Block>>,
1616        execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
1617        hashed_state: LazyHashedPostState,
1618        trie_output: Arc<TrieUpdates>,
1619    ) -> ExecutedBlock<N> {
1620        // Create deferred handle and task that owns the unsorted inputs.
1621        // Resolve the lazy handle into Arc<HashedPostState>. By this point the hashed state has
1622        // already been computed and used for state root verification, so .get() returns instantly.
1623        let hashed_state = match hashed_state.try_into_inner() {
1624            Ok(state) => state,
1625            Err(handle) => handle.get().clone(),
1626        };
1627        let (deferred_trie_data, deferred_trie_task) =
1628            LazyTrieData::pending(hashed_state, trie_output);
1629        let block_validation_metrics = self.metrics.block_validation.clone();
1630
1631        // Capture block info for tracing.
1632        let block_number = block.number();
1633
1634        // Spawn background task to compute trie data.
1635        let compute_trie_input_task = move || {
1636            let _span = debug_span!(
1637                target: "engine::tree::payload_validator",
1638                "compute_trie_input_task",
1639                block_number
1640            )
1641            .entered();
1642
1643            let compute_start = Instant::now();
1644            let computed = deferred_trie_task.compute_and_publish();
1645            block_validation_metrics
1646                .deferred_trie_compute_duration
1647                .record(compute_start.elapsed().as_secs_f64());
1648
1649            // Record sizes of the computed trie data
1650            block_validation_metrics
1651                .hashed_post_state_size
1652                .record(computed.hashed_state.total_len() as f64);
1653            block_validation_metrics
1654                .trie_updates_sorted_size
1655                .record(computed.trie_updates.total_len() as f64);
1656        };
1657
1658        // Spawn task that computes trie data asynchronously.
1659        self.runtime.spawn_blocking_named(DEFERRED_TRIE_WORKER_NAME, compute_trie_input_task);
1660
1661        ExecutedBlock::with_deferred_trie_data(block, execution_outcome, deferred_trie_data)
1662    }
1663
1664    fn calculate_timing_stats(
1665        &self,
1666        block: &RecoveredBlock<N::Block>,
1667        provider_stats: Arc<StateProviderStats>,
1668        cache_stats: Option<Arc<CacheStats>>,
1669        output: &BlockExecutionOutput<N::Receipt>,
1670        execution_duration: Duration,
1671        state_hash_duration: Duration,
1672    ) -> Box<ExecutionTimingStats> {
1673        let accounts_read = provider_stats.total_account_fetches();
1674        let storage_read = provider_stats.total_storage_fetches();
1675        let code_read = provider_stats.total_code_fetches();
1676        let code_bytes_read = provider_stats.total_code_fetched_bytes();
1677
1678        // Write stats from BundleState (final state changes)
1679        let accounts_changed = output.state.len();
1680        let accounts_deleted =
1681            output.state.state.values().filter(|acc| acc.was_destroyed()).count();
1682        let storage_slots_changed =
1683            output.state.state.values().map(|account| account.storage.len()).sum::<usize>();
1684        let storage_slots_deleted = output
1685            .state
1686            .state
1687            .values()
1688            .flat_map(|account| account.storage.values())
1689            .filter(|slot| {
1690                slot.present_value.is_zero() && !slot.previous_or_original_value.is_zero()
1691            })
1692            .count();
1693
1694        // Helper: check if account represents a new contract deployment
1695        let is_new_deployment = |acc: &BundleAccount| -> bool {
1696            let has_code_now = acc.info.as_ref().is_some_and(|info| !info.is_empty_code_hash());
1697            let had_no_code_before =
1698                acc.original_info.as_ref().is_none_or(AccountInfo::is_empty_code_hash);
1699            has_code_now && had_no_code_before
1700        };
1701
1702        let bytecodes_changed =
1703            output.state.state.values().filter(|acc| is_new_deployment(acc)).count();
1704
1705        // Unique new code hashes to count actual bytes persisted (deduplicated)
1706        let unique_new_code_hashes: B256Set = output
1707            .state
1708            .state
1709            .values()
1710            .filter(|acc| is_new_deployment(acc))
1711            .filter_map(|acc| acc.info.as_ref().map(|info| info.code_hash))
1712            .collect();
1713        let code_bytes_written: usize = unique_new_code_hashes
1714            .iter()
1715            .filter_map(|hash| output.state.contracts.get(hash).map(|bytecode| bytecode.len()))
1716            .sum();
1717
1718        // Total time spent fetching state during execution
1719        let state_read_duration = provider_stats.total_account_fetch_latency() +
1720            provider_stats.total_storage_fetch_latency() +
1721            provider_stats.total_code_fetch_latency();
1722
1723        // EIP-7702 delegation tracking from bytecode changes
1724        // Count new EIP-7702 bytecodes as delegations set
1725        let eip7702_delegations_set =
1726            output.state.contracts.values().filter(|bytecode| bytecode.is_eip7702()).count();
1727        // Delegations cleared: accounts where bytecode changed FROM EIP-7702 TO empty
1728        // This detects when an EIP-7702 delegation is removed by setting code to empty
1729        // Note: Clearing a delegation does NOT destroy the account - it just empties the
1730        // bytecode
1731        let eip7702_delegations_cleared = output
1732            .state
1733            .state
1734            .values()
1735            .filter(|acc| {
1736                // Check if original bytecode was EIP-7702
1737                let original_was_eip7702 = acc
1738                    .original_info
1739                    .as_ref()
1740                    .and_then(|info| info.code.as_ref())
1741                    .is_some_and(|bytecode| bytecode.is_eip7702());
1742
1743                // Check if current code is empty (delegation cleared)
1744                let code_now_empty = acc.info.as_ref().is_some_and(AccountInfo::is_empty_code_hash);
1745
1746                original_was_eip7702 && code_now_empty
1747            })
1748            .count();
1749
1750        // Get cache statistics for detailed block logging
1751        let (account_cache_hits, account_cache_misses) = cache_stats
1752            .as_ref()
1753            .map(|s| (s.account_hits(), s.account_misses()))
1754            .unwrap_or_default();
1755        let (storage_cache_hits, storage_cache_misses) = cache_stats
1756            .as_ref()
1757            .map(|s| (s.storage_hits(), s.storage_misses()))
1758            .unwrap_or_default();
1759        let (code_cache_hits, code_cache_misses) =
1760            cache_stats.as_ref().map(|s| (s.code_hits(), s.code_misses())).unwrap_or_default();
1761        let (txpool_snapshot_account_hits, txpool_snapshot_account_misses) = cache_stats
1762            .as_ref()
1763            .map(|s| (s.txpool_snapshot_account_hits(), s.txpool_snapshot_account_misses()))
1764            .unwrap_or_default();
1765        let (txpool_snapshot_storage_hits, txpool_snapshot_storage_misses) = cache_stats
1766            .as_ref()
1767            .map(|s| (s.txpool_snapshot_storage_hits(), s.txpool_snapshot_storage_misses()))
1768            .unwrap_or_default();
1769        let (txpool_snapshot_code_hits, txpool_snapshot_code_misses) = cache_stats
1770            .as_ref()
1771            .map(|s| (s.txpool_snapshot_code_hits(), s.txpool_snapshot_code_misses()))
1772            .unwrap_or_default();
1773
1774        // Build execution timing stats for detailed block logging
1775        Box::new(ExecutionTimingStats {
1776            block_number: block.number(),
1777            block_hash: block.hash(),
1778            gas_used: output.result.gas_used,
1779            tx_count: block.transaction_count(),
1780            execution_duration,
1781            state_read_duration,
1782            state_hash_duration,
1783            accounts_read,
1784            storage_read,
1785            code_read,
1786            code_bytes_read,
1787            accounts_changed,
1788            accounts_deleted,
1789            storage_slots_changed,
1790            storage_slots_deleted,
1791            bytecodes_changed,
1792            code_bytes_written,
1793            eip7702_delegations_set,
1794            eip7702_delegations_cleared,
1795            account_cache_hits,
1796            account_cache_misses,
1797            storage_cache_hits,
1798            storage_cache_misses,
1799            code_cache_hits,
1800            code_cache_misses,
1801            txpool_snapshot_account_hits,
1802            txpool_snapshot_account_misses,
1803            txpool_snapshot_storage_hits,
1804            txpool_snapshot_storage_misses,
1805            txpool_snapshot_code_hits,
1806            txpool_snapshot_code_misses,
1807        })
1808    }
1809}
1810
1811/// Type that validates the payloads processed by the engine.
1812///
1813/// This provides the necessary functions for validating/executing payloads/blocks.
1814pub trait EngineValidator<
1815    Types: PayloadTypes,
1816    N: NodePrimitives = <<Types as PayloadTypes>::BuiltPayload as BuiltPayload>::Primitives,
1817>: Send + Sync + 'static
1818{
1819    /// Validates the payload attributes with respect to the header.
1820    ///
1821    /// By default, this enforces that the payload attributes timestamp is greater than the
1822    /// timestamp according to:
1823    ///   > 7. Client software MUST ensure that payloadAttributes.timestamp is greater than
1824    ///   > timestamp
1825    ///   > of a block referenced by forkchoiceState.headBlockHash.
1826    ///
1827    /// See also: <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#specification-1>
1828    fn validate_payload_attributes_against_header(
1829        &self,
1830        attr: &Types::PayloadAttributes,
1831        header: &N::BlockHeader,
1832    ) -> Result<(), InvalidPayloadAttributesError>;
1833
1834    /// Ensures that the given payload does not violate any consensus rules that concern the block's
1835    /// layout.
1836    ///
1837    /// This function must convert the payload into the executable block and pre-validate its
1838    /// fields.
1839    ///
1840    /// Implementers should ensure that the checks are done in the order that conforms with the
1841    /// engine-API specification.
1842    fn convert_payload_to_block(
1843        &self,
1844        payload: Types::ExecutionData,
1845    ) -> Result<SealedBlock<N::Block>, NewPayloadError>;
1846
1847    /// Validates a payload received from engine API.
1848    fn validate_payload(
1849        &mut self,
1850        payload: Types::ExecutionData,
1851        ctx: TreeCtx<'_, N>,
1852    ) -> ValidationOutcome<N>;
1853
1854    /// Validates a block downloaded from the network, optionally carrying its access list data.
1855    fn validate_block(
1856        &mut self,
1857        block: SealedBlockWithAccessList<N::Block>,
1858        ctx: TreeCtx<'_, N>,
1859    ) -> ValidationOutcome<N>;
1860
1861    /// Hook called after an executed block is inserted directly into the tree.
1862    ///
1863    /// This is invoked when blocks are inserted via `InsertExecutedBlock` (e.g., locally built
1864    /// blocks by sequencers) to allow implementations to update internal state such as caches.
1865    fn on_inserted_executed_block(
1866        &self,
1867        block: BuiltPayloadExecutedBlock<N>,
1868    ) -> ProviderResult<ExecutedBlock<N>>;
1869
1870    /// Notifies the validator that `hash` is the current canonical head.
1871    ///
1872    /// This may also be called when a forkchoice update reaffirms the existing head.
1873    fn on_canonical_head_changed(&self, _hash: B256, _state: &EngineApiTreeState<N>) {}
1874
1875    /// Prepares the resources loaned to a payload builder job.
1876    ///
1877    /// `timestamp` is taken from the payload attributes.
1878    fn payload_builder_resources(
1879        &self,
1880        parent_hash: B256,
1881        parent_header: &N::BlockHeader,
1882        timestamp: u64,
1883        state: &mut EngineApiTreeState<N>,
1884    ) -> PayloadBuilderResources;
1885}
1886
1887impl<N, Types, P, Evm, V> EngineValidator<Types> for BasicEngineValidator<P, Evm, V>
1888where
1889    P: DatabaseProviderFactory<
1890            Provider: BlockReader
1891                          + StageCheckpointReader
1892                          + PruneCheckpointReader
1893                          + ChangeSetReader
1894                          + StorageChangeSetReader
1895                          + StorageSettingsCache
1896                          + HistoryReader
1897                          + 'static,
1898        > + BlockReader<Header = N::BlockHeader>
1899        + StateProviderFactory
1900        + StateReader
1901        + ChangeSetReader
1902        + Clone
1903        + 'static,
1904    N: NodePrimitives,
1905    V: PayloadValidator<Types, Block = N::Block> + Clone,
1906    Evm: ConfigureEngineEvm<Types::ExecutionData, Primitives = N> + 'static,
1907    Types: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
1908{
1909    fn validate_payload_attributes_against_header(
1910        &self,
1911        attr: &Types::PayloadAttributes,
1912        header: &N::BlockHeader,
1913    ) -> Result<(), InvalidPayloadAttributesError> {
1914        self.validator.validate_payload_attributes_against_header(attr, header)
1915    }
1916
1917    fn convert_payload_to_block(
1918        &self,
1919        payload: Types::ExecutionData,
1920    ) -> Result<SealedBlock<N::Block>, NewPayloadError> {
1921        let block = self.validator.convert_payload_to_block(payload)?;
1922        Ok(block)
1923    }
1924
1925    fn validate_payload(
1926        &mut self,
1927        payload: Types::ExecutionData,
1928        ctx: TreeCtx<'_, N>,
1929    ) -> ValidationOutcome<N> {
1930        self.validate_block_with_state(BlockOrPayload::Payload(payload), ctx)
1931    }
1932
1933    fn validate_block(
1934        &mut self,
1935        block: SealedBlockWithAccessList<N::Block>,
1936        ctx: TreeCtx<'_, N>,
1937    ) -> ValidationOutcome<N> {
1938        self.validate_block_with_state(BlockOrPayload::Block(block), ctx)
1939    }
1940
1941    fn on_inserted_executed_block(
1942        &self,
1943        block: BuiltPayloadExecutedBlock<N>,
1944    ) -> ProviderResult<ExecutedBlock<N>> {
1945        self.payload_processor.on_inserted_executed_block(
1946            block.recovered_block.block_with_parent(),
1947            &block.execution_output.state,
1948        );
1949
1950        Ok(self.spawn_deferred_trie_task(
1951            block.recovered_block,
1952            block.execution_output,
1953            LazyHashedPostState::ready(block.hashed_state),
1954            block.trie_updates,
1955        ))
1956    }
1957
1958    fn on_canonical_head_changed(&self, hash: B256, state: &EngineApiTreeState<N>) {
1959        let Some(txpool_prewarm) = self.txpool_prewarm.as_ref() else { return };
1960
1961        // Obtain the header of the new canonical head; pool transactions are warmed on top of
1962        // its state.
1963        let parent = match self.sealed_header_by_hash(hash, state) {
1964            Ok(Some(header)) => header,
1965            Ok(None) => return,
1966            Err(err) => {
1967                trace!(
1968                    target: "engine::tree::txpool_prewarm",
1969                    %err,
1970                    block_hash = ?hash,
1971                    "failed to fetch canonical header for txpool prewarming"
1972                );
1973                return
1974            }
1975        };
1976        // Unless results are recorded, warming reuses the head block's own environment rather
1977        // than predicting the next block's: attribute-level accuracy (timestamp, basefee) doesn't
1978        // matter for collecting state reads, and the worker disables the fee checks a stale
1979        // basefee would trip.
1980        let evm_env = match self.evm_config.evm_env(parent.header()) {
1981            Ok(evm_env) => evm_env,
1982            Err(err) => {
1983                trace!(
1984                    target: "engine::tree::txpool_prewarm",
1985                    %err,
1986                    block_hash = ?parent.hash(),
1987                    "failed to derive canonical txpool prewarming environment"
1988                );
1989                return
1990            }
1991        };
1992
1993        let state_provider_factory = match self.overlay_state_provider_factory(parent.hash(), state)
1994        {
1995            Ok(Some(state_provider_factory)) => state_provider_factory,
1996            Ok(None) => return,
1997            Err(err) => {
1998                trace!(
1999                    target: "engine::tree::txpool_prewarm",
2000                    %err,
2001                    block_hash = ?parent.hash(),
2002                    "failed to derive canonical txpool prewarming provider"
2003                );
2004                return
2005            }
2006        };
2007        txpool_prewarm.start(&parent, evm_env, state_provider_factory)
2008    }
2009
2010    fn payload_builder_resources(
2011        &self,
2012        parent_hash: B256,
2013        parent_header: &N::BlockHeader,
2014        timestamp: u64,
2015        state: &mut EngineApiTreeState<N>,
2016    ) -> PayloadBuilderResources {
2017        let execution_cache = self
2018            .config
2019            .share_execution_cache_with_payload_builder()
2020            .then(|| self.payload_processor.cache_for(parent_hash));
2021        let state_root_handle =
2022            self.payload_state_root_handle_for(parent_hash, parent_header, timestamp, state);
2023        let mut resources = PayloadBuilderResources::new(execution_cache, state_root_handle)
2024            .with_lease(PayloadBuilderLease::new(JitPauseGuard::new(&self.evm_config)));
2025        // If the txpool prewarming is enabled then we should disable it for the duration
2026        // of the payload builder job. This is done by obtaining a lease that will release
2027        // the txpool prewarm when dropped.
2028        if let Some(txpool_prewarm) = self.txpool_prewarm.as_ref() {
2029            let txpool_lease = PayloadBuilderLease::new(txpool_prewarm.pause());
2030            resources = resources.with_lease(txpool_lease);
2031        }
2032        resources
2033    }
2034}
2035
2036impl<P, Evm, V> WaitForCaches for BasicEngineValidator<P, Evm, V>
2037where
2038    Evm: ConfigureEvm,
2039{
2040    fn wait_for_caches(&self) -> CacheWaitDurations {
2041        debug!(target: "engine::tree::payload_validator", "Waiting for execution cache and sparse trie locks");
2042
2043        let execution_cache = self.payload_processor.execution_cache();
2044        let overlay_manager = self.overlay_manager.clone();
2045        let (execution_tx, execution_rx) = std::sync::mpsc::channel();
2046        let (sparse_trie_tx, sparse_trie_rx) = std::sync::mpsc::channel();
2047
2048        self.runtime.spawn_blocking_named("wait-exec-cache", move || {
2049            let _ = execution_tx.send(execution_cache.wait_for_availability());
2050        });
2051        self.runtime.spawn_blocking_named("wait-sparse-tri", move || {
2052            let _ = sparse_trie_tx.send(overlay_manager.wait_for_sparse_trie_availability());
2053        });
2054
2055        let execution_cache =
2056            execution_rx.recv().expect("execution cache wait task failed to send result");
2057        let sparse_trie =
2058            sparse_trie_rx.recv().expect("sparse trie wait task failed to send result");
2059        debug!(
2060            target: "engine::tree::payload_validator",
2061            ?execution_cache,
2062            ?sparse_trie,
2063            "Execution cache and sparse trie locks acquired"
2064        );
2065        CacheWaitDurations { execution_cache, sparse_trie }
2066    }
2067}
2068
2069/// Enum representing either block or payload being validated.
2070#[derive(Debug, Clone)]
2071pub enum BlockOrPayload<T: PayloadTypes> {
2072    /// Payload.
2073    Payload(T::ExecutionData),
2074    /// Block with optional access list data, e.g. downloaded from the network.
2075    Block(SealedBlockWithAccessList<BlockTy<<T::BuiltPayload as BuiltPayload>::Primitives>>),
2076}
2077
2078impl<T: PayloadTypes> BlockOrPayload<T> {
2079    /// Returns the hash of the block.
2080    pub fn hash(&self) -> B256 {
2081        match self {
2082            Self::Payload(payload) => payload.block_hash(),
2083            Self::Block(block) => block.hash(),
2084        }
2085    }
2086
2087    /// Returns the number and hash of the block.
2088    pub fn num_hash(&self) -> NumHash {
2089        match self {
2090            Self::Payload(payload) => payload.num_hash(),
2091            Self::Block(block) => block.num_hash(),
2092        }
2093    }
2094
2095    /// Returns the parent hash of the block.
2096    pub fn parent_hash(&self) -> B256 {
2097        match self {
2098            Self::Payload(payload) => payload.parent_hash(),
2099            Self::Block(block) => block.parent_hash(),
2100        }
2101    }
2102
2103    /// Returns [`BlockWithParent`] for the block.
2104    pub fn block_with_parent(&self) -> BlockWithParent {
2105        match self {
2106            Self::Payload(payload) => payload.block_with_parent(),
2107            Self::Block(block) => block.block_with_parent(),
2108        }
2109    }
2110
2111    /// Returns a string showing whether or not this is a block or payload.
2112    pub const fn type_name(&self) -> &'static str {
2113        match self {
2114            Self::Payload(_) => "payload",
2115            Self::Block(_) => "block",
2116        }
2117    }
2118
2119    /// Returns true if this is a payload.
2120    pub const fn is_payload(&self) -> bool {
2121        matches!(self, Self::Payload(_))
2122    }
2123
2124    /// Returns true if this is a block.
2125    pub const fn is_block(&self) -> bool {
2126        matches!(self, Self::Block(_))
2127    }
2128
2129    /// Returns the decoded block access list, if present and successfully decoded.
2130    pub fn try_decoded_access_list(&self) -> Result<Option<DecodedBal>, alloy_rlp::Error> {
2131        match self {
2132            Self::Payload(payload) => payload
2133                .block_access_list()
2134                .map(|block_access_list| DecodedBal::from_rlp_bytes(block_access_list.clone()))
2135                .transpose(),
2136            Self::Block(block) => block.data().clone().map(DecodedBal::from_raw_bal).transpose(),
2137        }
2138    }
2139
2140    /// Returns whether execution must build a block access list.
2141    pub fn has_block_access_list(&self) -> bool {
2142        match self {
2143            Self::Payload(payload) => payload.block_access_list().is_some(),
2144            Self::Block(block) => block.block_access_list_hash().is_some(),
2145        }
2146    }
2147
2148    /// Returns the number of transactions in the payload or block.
2149    pub fn transaction_count(&self) -> usize
2150    where
2151        T::ExecutionData: ExecutionPayload,
2152    {
2153        match self {
2154            Self::Payload(payload) => payload.transaction_count(),
2155            Self::Block(block) => block.transaction_count(),
2156        }
2157    }
2158
2159    /// Returns the withdrawals from the payload or block.
2160    pub fn withdrawals(&self) -> Option<&[Withdrawal]>
2161    where
2162        T::ExecutionData: ExecutionPayload,
2163    {
2164        match self {
2165            Self::Payload(payload) => payload.withdrawals().map(|w| w.as_slice()),
2166            Self::Block(block) => block.body().withdrawals().map(|w| w.as_slice()),
2167        }
2168    }
2169
2170    /// Returns the total gas used by the block.
2171    pub fn gas_used(&self) -> u64
2172    where
2173        T::ExecutionData: ExecutionPayload,
2174    {
2175        match self {
2176            Self::Payload(payload) => payload.gas_used(),
2177            Self::Block(block) => block.gas_used(),
2178        }
2179    }
2180
2181    /// Returns the gas limit used by the block.
2182    pub fn gas_limit(&self) -> u64
2183    where
2184        T::ExecutionData: ExecutionPayload,
2185    {
2186        match self {
2187            Self::Payload(payload) => payload.gas_limit(),
2188            Self::Block(block) => block.gas_limit(),
2189        }
2190    }
2191}
2192
2193/// Block access list produced by executing a block.
2194struct ExecutedBal {
2195    /// Alloy form, only needed for the post-execution consensus checks (hash and gas limit).
2196    alloy: BlockAccessList,
2197    /// Revm form, shared with the executed block so consumers can reuse it.
2198    revm: Arc<RevmBal>,
2199}