Skip to main content

reth_engine_tree/tree/
payload_validator.rs

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