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