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