Skip to main content

reth_engine_tree/tree/
mod.rs

1use crate::{
2    backfill::{BackfillAction, BackfillSyncState},
3    chain::FromOrchestrator,
4    engine::{DownloadRequest, EngineApiEvent, EngineApiKind, EngineApiRequest, FromEngine},
5    persistence::PersistenceHandle,
6    tree::{error::InsertPayloadError, payload_validator::TreeCtx},
7};
8use alloy_consensus::BlockHeader;
9use alloy_eips::{eip1898::BlockWithParent, BlockNumHash, NumHash};
10use alloy_primitives::{map::B256Map, B256};
11use alloy_rpc_types_engine::{
12    ForkchoiceState, PayloadStatus, PayloadStatusEnum, PayloadValidationError,
13};
14use error::{
15    InsertBlockError, InsertBlockFatalError, InsertBlockProcessingError, InsertBlockValidationError,
16};
17use reth_chain_state::{
18    CanonicalInMemoryState, ExecutedBlock, ExecutionTimingStats, NewCanonicalChain,
19};
20use reth_consensus::{Consensus, FullConsensus};
21use reth_engine_primitives::{
22    BeaconEngineMessage, ConsensusEngineEvent, ExecutionPayload, ForkchoiceStateTracker,
23    NewPayloadTimings, OnForkChoiceUpdated, SlowBlockInfo,
24};
25use reth_errors::{ConsensusError, ProviderResult};
26use reth_evm::ConfigureEvm;
27use reth_network_p2p::full_block::SealedBlockWithAccessList;
28use reth_payload_builder::{BuildNewPayload, PayloadBuilderHandle, PayloadBuilderLease};
29use reth_payload_primitives::{BuiltPayload, NewPayloadError, PayloadAttributes, PayloadTypes};
30use reth_primitives_traits::{
31    FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
32};
33use reth_provider::{
34    BalProvider, BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
35    DatabaseProviderFactory, ProviderError, PruneCheckpointReader, SaveBlocksInput,
36    StageCheckpointReader, StateProviderFactory, StateReader, StorageChangeSetReader,
37    StorageSettingsCache, TransactionVariant,
38};
39use reth_revm::database::StateProviderDatabase;
40use reth_stages_api::ControlFlow;
41use reth_storage_overlay::OverlayManager;
42use reth_tasks::{spawn_os_thread, utils::increase_thread_priority};
43use reth_trie::{HashedPostState, KeccakKeyHasher, SortedTrieData};
44use revm::interpreter::debug_unreachable;
45use state::TreeState;
46use std::{
47    fmt::Debug,
48    ops,
49    sync::{
50        atomic::{AtomicUsize, Ordering},
51        Arc,
52    },
53    time::Duration,
54};
55
56use crossbeam_channel::{Receiver, Sender};
57use tokio::sync::{
58    mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
59    oneshot,
60};
61use tracing::*;
62
63mod block_buffer;
64pub mod error;
65pub mod instrumented_state;
66mod invalid_headers;
67mod metrics;
68pub mod payload_processor;
69pub mod payload_validator;
70mod persistence_state;
71pub mod state_root_strategy;
72#[cfg(test)]
73mod tests;
74mod trie_updates;
75mod txpool_prewarm;
76pub mod types;
77
78use crate::{persistence::PersistenceResult, tree::error::AdvancePersistenceError};
79pub use block_buffer::BlockBuffer;
80pub use invalid_headers::InvalidHeaderCache;
81pub use metrics::EngineApiMetrics;
82pub use payload_processor::*;
83pub use payload_validator::{BasicEngineValidator, EngineValidator};
84pub use persistence_state::PersistenceState;
85pub use reth_engine_primitives::TreeConfig;
86pub use reth_execution_cache::{
87    precompile_cache, CachedStateCacheMetrics, CachedStateMetrics, CachedStateMetricsSource,
88    CachedStateProvider, ExecutionCache, PayloadExecutionCache, SavedCache,
89    TxPoolPrewarmCacheSnapshot,
90};
91pub use txpool_prewarm::{
92    Source as TxPoolPrewarmSource, Transaction as TxPoolPrewarmTransaction,
93    Transactions as TxPoolPrewarmTransactions,
94};
95pub use types::{ExecutionEnv, ValidationOutcome, ValidationOutput};
96
97pub mod state;
98
99/// The minimum number of blocks to retain in the changeset cache after eviction.
100///
101/// This ensures that recent changesets are kept in memory for potential reorgs,
102/// even when the finalized block is not set (e.g., on L2s like Optimism).
103const CHANGESET_CACHE_RETENTION_BLOCKS: u64 = 64;
104
105/// Tracks the state of the engine api internals.
106///
107/// This type is not shareable.
108#[derive(Debug)]
109pub struct EngineApiTreeState<N: NodePrimitives> {
110    /// Tracks the state of the blockchain tree.
111    tree_state: TreeState<N>,
112    /// Whether the next sparse trie task should attempt cache pruning during trie preservation.
113    pending_sparse_trie_prune: bool,
114    /// Tracks the forkchoice state updates received by the CL.
115    forkchoice_state_tracker: ForkchoiceStateTracker,
116    /// Buffer of detached blocks.
117    buffer: BlockBuffer<N::Block>,
118    /// Tracks the header of invalid payloads that were rejected by the engine because they're
119    /// invalid.
120    invalid_headers: InvalidHeaderCache,
121}
122
123impl<N: NodePrimitives> EngineApiTreeState<N> {
124    fn new(
125        block_buffer_limit: u32,
126        max_invalid_header_cache_length: u32,
127        invalid_header_hit_eviction_threshold: u8,
128        canonical_block: BlockNumHash,
129        engine_kind: EngineApiKind,
130        overlay_manager: OverlayManager<N>,
131    ) -> Self {
132        Self {
133            invalid_headers: InvalidHeaderCache::new(
134                max_invalid_header_cache_length,
135                invalid_header_hit_eviction_threshold,
136            ),
137            buffer: BlockBuffer::new(block_buffer_limit),
138            tree_state: TreeState::new(canonical_block, engine_kind, overlay_manager),
139            pending_sparse_trie_prune: false,
140            forkchoice_state_tracker: ForkchoiceStateTracker::default(),
141        }
142    }
143
144    /// Returns a reference to the tree state.
145    pub const fn tree_state(&self) -> &TreeState<N> {
146        &self.tree_state
147    }
148
149    /// Returns whether sparse trie pruning is pending.
150    pub const fn pending_sparse_trie_prune(&self) -> bool {
151        self.pending_sparse_trie_prune
152    }
153
154    /// Sets whether sparse trie pruning is pending for the next sparse trie task.
155    pub const fn set_pending_sparse_trie_prune(&mut self, pending: bool) {
156        self.pending_sparse_trie_prune = pending;
157    }
158
159    /// Takes a pending sparse trie prune request, if any, and snapshots the in-memory parent chain
160    /// ending at `parent_hash`.
161    ///
162    /// `None` means no prune request is pending. `Some(Vec::new())` means a prune was requested,
163    /// but no in-memory parent-chain blocks were found for the parent hash; the sparse trie task
164    /// should still prune nodes cached before the current block's epoch.
165    pub fn take_sparse_trie_prune_blocks(
166        &mut self,
167        parent_hash: B256,
168    ) -> Option<Vec<ExecutedBlock<N>>> {
169        if !self.pending_sparse_trie_prune {
170            return None
171        }
172
173        self.pending_sparse_trie_prune = false;
174        Some(
175            self.tree_state
176                .blocks_by_hash(parent_hash)
177                .map(|(_, blocks)| blocks)
178                .unwrap_or_default(),
179        )
180    }
181
182    /// Returns true if the block has been marked as invalid.
183    pub fn has_invalid_header(&mut self, hash: &B256) -> bool {
184        self.invalid_headers.get(hash).is_some()
185    }
186}
187
188/// The outcome of a tree operation.
189#[derive(Debug)]
190pub struct TreeOutcome<T> {
191    /// The outcome of the operation.
192    pub outcome: T,
193    /// An optional event to tell the caller to do something.
194    pub event: Option<TreeEvent>,
195    /// Whether the block was already seen, meaning no real execution happened during this
196    /// `newPayload` call.
197    pub already_seen: bool,
198}
199
200impl<T> TreeOutcome<T> {
201    /// Create new tree outcome.
202    pub const fn new(outcome: T) -> Self {
203        Self { outcome, event: None, already_seen: false }
204    }
205
206    /// Set event on the outcome.
207    pub fn with_event(mut self, event: TreeEvent) -> Self {
208        self.event = Some(event);
209        self
210    }
211
212    /// Set the `already_seen` flag on the outcome.
213    pub const fn with_already_seen(mut self, value: bool) -> Self {
214        self.already_seen = value;
215        self
216    }
217}
218
219/// Result of trying to insert a new payload in [`EngineApiTreeHandler`].
220#[derive(Debug)]
221pub struct TryInsertPayloadResult {
222    /// - `Valid`: Payload successfully validated and inserted
223    /// - `Syncing`: Parent missing, payload buffered for later
224    /// - Error status: Payload is invalid
225    pub status: PayloadStatus,
226    /// Whether the block was already seen
227    pub already_seen: bool,
228}
229
230impl TryInsertPayloadResult {
231    /// Convert the result into a [`TreeOutcome`].
232    #[inline]
233    pub fn into_outcome(self) -> TreeOutcome<PayloadStatus> {
234        TreeOutcome::new(self.status).with_already_seen(self.already_seen)
235    }
236}
237
238/// Events that are triggered by Tree Chain
239#[derive(Debug)]
240pub enum TreeEvent {
241    /// Tree action is needed.
242    TreeAction(TreeAction),
243    /// Backfill action is needed.
244    BackfillAction(BackfillAction),
245    /// Block download is needed.
246    Download(DownloadRequest),
247}
248
249impl TreeEvent {
250    /// Returns true if the event is a backfill action.
251    const fn is_backfill_action(&self) -> bool {
252        matches!(self, Self::BackfillAction(_))
253    }
254}
255
256/// The actions that can be performed on the tree.
257#[derive(Debug)]
258pub enum TreeAction {
259    /// Make target canonical.
260    MakeCanonical {
261        /// The sync target head hash
262        sync_target_head: B256,
263    },
264}
265
266/// The engine API tree handler implementation.
267///
268/// This type is responsible for processing engine API requests, maintaining the canonical state and
269/// emitting events.
270pub struct EngineApiTreeHandler<N, P, T, V, C>
271where
272    N: NodePrimitives,
273    T: PayloadTypes,
274    C: ConfigureEvm<Primitives = N> + 'static,
275{
276    provider: P,
277    consensus: Arc<dyn FullConsensus<N>>,
278    payload_validator: V,
279    /// Keeps track of internals such as executed and buffered blocks.
280    state: EngineApiTreeState<N>,
281    /// The half for sending messages to the engine.
282    ///
283    /// This is kept so that we can queue in messages to ourself that we can process later, for
284    /// example distributing workload across multiple messages that would otherwise take too long
285    /// to process. E.g. we might receive a range of downloaded blocks and we want to process
286    /// them one by one so that we can handle incoming engine API in between and don't become
287    /// unresponsive. This can happen during live sync transition where we're trying to close the
288    /// gap (up to 3 epochs of blocks in the worst case).
289    incoming_tx: Sender<FromEngine<EngineApiRequest<T, N>, N::Block>>,
290    /// Incoming engine API requests.
291    incoming: Receiver<FromEngine<EngineApiRequest<T, N>, N::Block>>,
292    /// Outgoing events that are emitted to the handler.
293    outgoing: UnboundedSender<EngineApiEvent<N>>,
294    /// Channels to the persistence layer.
295    persistence: PersistenceHandle<N>,
296    /// Tracks the state changes of the persistence task.
297    persistence_state: PersistenceState,
298    /// Flag indicating the state of the node's backfill synchronization process.
299    backfill_sync_state: BackfillSyncState,
300    /// Keeps track of the state of the canonical chain that isn't persisted yet.
301    /// This is intended to be accessed from external sources, such as rpc.
302    canonical_in_memory_state: CanonicalInMemoryState<N>,
303    /// Handle to the payload builder that will receive payload attributes for valid forkchoice
304    /// updates
305    payload_builder: PayloadBuilderHandle<T>,
306    /// Configuration settings.
307    config: TreeConfig,
308    /// Metrics for the engine api.
309    metrics: EngineApiMetrics,
310    /// The engine API variant of this handler
311    engine_kind: EngineApiKind,
312    /// The EVM configuration.
313    evm_config: C,
314    /// Timing statistics for executed blocks, keyed by block hash.
315    /// Stored here (not in `ExecutedBlock`) to avoid leaking observability concerns into the block
316    /// type. Entries are removed when blocks are persisted or invalidated.
317    execution_timing_stats: B256Map<Box<ExecutionTimingStats>>,
318    /// Tracks payload jobs that may still access in-memory overlay state.
319    payload_builds: PayloadBuildTracker,
320    /// Notifies the engine when the final active payload job finishes.
321    payload_build_finished: Receiver<()>,
322    /// Task runtime for spawning blocking work on named, reusable threads.
323    runtime: reth_tasks::Runtime,
324}
325
326impl<N, P: Debug, T: PayloadTypes, V: Debug, C> std::fmt::Debug
327    for EngineApiTreeHandler<N, P, T, V, C>
328where
329    N: NodePrimitives,
330    C: ConfigureEvm<Primitives = N>,
331{
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        f.debug_struct("EngineApiTreeHandler")
334            .field("provider", &self.provider)
335            .field("consensus", &self.consensus)
336            .field("payload_validator", &self.payload_validator)
337            .field("state", &self.state)
338            .field("incoming_tx", &self.incoming_tx)
339            .field("persistence", &self.persistence)
340            .field("persistence_state", &self.persistence_state)
341            .field("backfill_sync_state", &self.backfill_sync_state)
342            .field("canonical_in_memory_state", &self.canonical_in_memory_state)
343            .field("payload_builder", &self.payload_builder)
344            .field("config", &self.config)
345            .field("metrics", &self.metrics)
346            .field("engine_kind", &self.engine_kind)
347            .field("evm_config", &self.evm_config)
348            .field("execution_timing_stats", &self.execution_timing_stats.len())
349            .field("payload_builds_active", &self.payload_builds.is_active())
350            .field("runtime", &self.runtime)
351            .finish()
352    }
353}
354
355impl<N, P, T, V, C> EngineApiTreeHandler<N, P, T, V, C>
356where
357    N: NodePrimitives,
358    P: DatabaseProviderFactory
359        + BlockReader<Block = N::Block, Header = N::BlockHeader>
360        + StateProviderFactory
361        + StateReader<Receipt = N::Receipt>
362        + BalProvider
363        + Clone
364        + 'static,
365    P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
366        + PruneCheckpointReader
367        + StageCheckpointReader
368        + ChangeSetReader
369        + StorageChangeSetReader
370        + StorageSettingsCache
371        + 'static,
372    C: ConfigureEvm<Primitives = N> + 'static,
373    T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
374    V: EngineValidator<T> + WaitForCaches,
375{
376    /// Creates a new [`EngineApiTreeHandler`].
377    #[expect(clippy::too_many_arguments)]
378    pub fn new(
379        provider: P,
380        consensus: Arc<dyn FullConsensus<N>>,
381        payload_validator: V,
382        outgoing: UnboundedSender<EngineApiEvent<N>>,
383        state: EngineApiTreeState<N>,
384        canonical_in_memory_state: CanonicalInMemoryState<N>,
385        persistence: PersistenceHandle<N>,
386        persistence_state: PersistenceState,
387        payload_builder: PayloadBuilderHandle<T>,
388        config: TreeConfig,
389        engine_kind: EngineApiKind,
390        evm_config: C,
391        runtime: reth_tasks::Runtime,
392    ) -> Self {
393        let (incoming_tx, incoming) = crossbeam_channel::unbounded();
394
395        let (payload_builds, payload_build_finished) = PayloadBuildTracker::new();
396
397        Self {
398            provider,
399            consensus,
400            payload_validator,
401            incoming,
402            outgoing,
403            persistence,
404            persistence_state,
405            backfill_sync_state: BackfillSyncState::Idle,
406            state,
407            canonical_in_memory_state,
408            payload_builder,
409            config,
410            metrics: Default::default(),
411            incoming_tx,
412            engine_kind,
413            evm_config,
414            execution_timing_stats: B256Map::default(),
415            payload_builds,
416            payload_build_finished,
417            runtime,
418        }
419    }
420
421    /// Creates a new [`EngineApiTreeHandler`] instance and spawns it in its
422    /// own thread.
423    ///
424    /// Returns the sender through which incoming requests can be sent to the task and the receiver
425    /// end of a [`EngineApiEvent`] unbounded channel to receive events from the engine.
426    #[expect(clippy::complexity)]
427    pub fn spawn_new(
428        provider: P,
429        consensus: Arc<dyn FullConsensus<N>>,
430        payload_validator: V,
431        persistence: PersistenceHandle<N>,
432        payload_builder: PayloadBuilderHandle<T>,
433        canonical_in_memory_state: CanonicalInMemoryState<N>,
434        overlay_manager: OverlayManager<N>,
435        config: TreeConfig,
436        kind: EngineApiKind,
437        evm_config: C,
438        runtime: reth_tasks::Runtime,
439    ) -> (Sender<FromEngine<EngineApiRequest<T, N>, N::Block>>, UnboundedReceiver<EngineApiEvent<N>>)
440    {
441        let best_block_number = provider.best_block_number().unwrap_or(0);
442        let header = provider.sealed_header(best_block_number).ok().flatten().unwrap_or_default();
443
444        let persistence_state = PersistenceState {
445            last_persisted_block: BlockNumHash::new(best_block_number, header.hash()),
446            last_state_trie_persisted_block: BlockNumHash::new(best_block_number, header.hash()),
447            rx: None,
448        };
449
450        let (tx, outgoing) = unbounded_channel();
451        let state = EngineApiTreeState::new(
452            config.block_buffer_limit(),
453            config.max_invalid_header_cache_length(),
454            config.invalid_header_hit_eviction_threshold(),
455            header.num_hash(),
456            kind,
457            overlay_manager,
458        );
459
460        let task = Self::new(
461            provider,
462            consensus,
463            payload_validator,
464            tx,
465            state,
466            canonical_in_memory_state,
467            persistence,
468            persistence_state,
469            payload_builder,
470            config,
471            kind,
472            evm_config,
473            runtime,
474        );
475        let incoming = task.incoming_tx.clone();
476        spawn_os_thread("engine", || {
477            increase_thread_priority();
478            task.run()
479        });
480        (incoming, outgoing)
481    }
482
483    /// Returns a [`TreeOutcome`] indicating the forkchoice head is valid and canonical.
484    fn valid_outcome(state: ForkchoiceState) -> TreeOutcome<OnForkChoiceUpdated> {
485        TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::new(
486            PayloadStatusEnum::Valid,
487            Some(state.head_block_hash),
488        )))
489    }
490
491    /// Returns a new [`Sender`] to send messages to this type.
492    pub fn sender(&self) -> Sender<FromEngine<EngineApiRequest<T, N>, N::Block>> {
493        self.incoming_tx.clone()
494    }
495
496    /// How many canonical blocks are retained in memory. A large count means persistence is
497    /// falling behind execution.
498    fn persistence_gap(&self) -> u64 {
499        self.canonical_in_memory_state.canonical_chain().count() as u64
500    }
501
502    /// How many blocks beyond the configured in-memory buffer are awaiting persistence.
503    fn persistence_backpressure_gap(&self) -> u64 {
504        self.persistence_gap().saturating_sub(self.config.memory_block_buffer_target())
505    }
506
507    /// Returns `true` when the main loop should stop draining the tree input channel.
508    ///
509    /// This is the case when persistence is already running and the number of blocks beyond the
510    /// configured in-memory buffer has reached the configured threshold.
511    fn should_backpressure(&self) -> bool {
512        self.persistence_state.in_progress() &&
513            self.persistence_backpressure_gap() >=
514                self.config.persistence_backpressure_threshold()
515    }
516
517    /// Run the engine API handler.
518    ///
519    /// This will block the current thread and process incoming messages.
520    pub fn run(mut self) {
521        let terminated = self.run_until_exit();
522        drop(self);
523        if let Some(tx) = terminated {
524            let _ = tx.send(());
525        }
526    }
527
528    fn run_until_exit(&mut self) -> Option<oneshot::Sender<()>> {
529        loop {
530            // Each iteration has three phases:
531            //
532            // 1. Non-blocking poll for persistence completion. If the background flush already
533            //    landed, absorb the result now so the gap calculation below is fresh.
534            // 2. Decide how to wait for the next event. When the canonical-to-persisted gap beyond
535            //    the in-memory buffer reaches the backpressure threshold we only block on the
536            //    persistence receiver, leaving new engine requests sitting in the unbounded
537            //    upstream channel.
538            // 3. Handle the event (engine message or persistence completion) and kick off a new
539            //    persistence cycle if the threshold is met again.
540            //
541            // The net effect: when the unbuffered persistence gap reaches the threshold, we stop
542            // processing incoming messages and let them queue in the channel. This is only a soft
543            // form of backpressure: it delays replies and, more importantly, prevents executing
544            // further blocks that would pile up in the persistence queue - where each block
545            // carries heavier state (eg. trie updates) than the raw payload sitting in the engine
546            // channel.
547            //
548            // Standard Ethereum CLs won't truly back off - the engine API has no
549            // backpressure semantics, and CLs typically timeout after ≈8s and resend - so
550            // this cannot prevent the incoming channel from growing under sustained load.
551            // But it shifts the bottleneck to the lighter-weight incoming queue rather than
552            // the costlier persistence pipeline. Other clients that respect reply latency
553            // can treat the delayed responses as a signal to chill out.
554            match self.try_poll_persistence() {
555                Ok(true) => {
556                    if let Err(err) = self.advance_persistence() {
557                        error!(target: "engine::tree", %err, "Advancing persistence failed");
558                        return None
559                    }
560                    continue;
561                }
562                Ok(false) => {}
563                Err(err) => {
564                    error!(target: "engine::tree", %err, "Polling persistence failed");
565                    return None
566                }
567            }
568
569            let event = if self.should_backpressure() {
570                self.metrics.engine.backpressure_active.set(1.0);
571                let stall_start = Instant::now();
572                let event = self.wait_for_persistence_event();
573                self.metrics.engine.backpressure_stall_duration.record(stall_start.elapsed());
574                event
575            } else {
576                self.metrics.engine.backpressure_active.set(0.0);
577                self.wait_for_event()
578            };
579
580            match event {
581                LoopEvent::EngineMessage(msg) => {
582                    debug!(target: "engine::tree", %msg, "received new engine message");
583                    match self.on_engine_message(msg) {
584                        Ok(ops::ControlFlow::Break(tx)) => return Some(tx),
585                        Ok(ops::ControlFlow::Continue(())) => {}
586                        Err(fatal) => {
587                            error!(target: "engine::tree", %fatal, "insert block fatal error");
588                            return None
589                        }
590                    }
591                }
592                LoopEvent::PersistenceComplete { result, start_time } => {
593                    if let Err(err) = self.on_persistence_complete(result, start_time) {
594                        error!(target: "engine::tree", %err, "Persistence complete handling failed");
595                        return None
596                    }
597                }
598                LoopEvent::PayloadBuildFinished => {}
599                LoopEvent::Disconnected => {
600                    error!(target: "engine::tree", "Channel disconnected");
601                    return None
602                }
603            }
604
605            // Always check if we need to trigger new persistence after any event:
606            // - After engine messages: new blocks may have been inserted that exceed the
607            //   persistence threshold
608            // - After persistence completion: we can now persist more blocks if needed
609            if let Err(err) = self.advance_persistence() {
610                error!(target: "engine::tree", %err, "Advancing persistence failed");
611                return None
612            }
613        }
614    }
615
616    /// Blocks until the in-flight persistence task completes, used when we are under
617    /// backpressure.
618    ///
619    /// Unlike `wait_for_event`, this deliberately does not read from the tree input channel. Any
620    /// requests sent to the tree remain queued upstream until persistence catches up.
621    fn wait_for_persistence_event(&mut self) -> LoopEvent<T, N> {
622        let maybe_persistence = self.persistence_state.rx.take();
623
624        if let Some((persistence_rx, start_time, _action)) = maybe_persistence {
625            match persistence_rx.recv() {
626                Ok(result) => LoopEvent::PersistenceComplete { result, start_time },
627                Err(_) => LoopEvent::Disconnected,
628            }
629        } else {
630            self.wait_for_event()
631        }
632    }
633
634    /// Blocks until the next event is ready.
635    ///
636    /// Uses biased selection to prioritize persistence completion so in-memory state is updated and
637    /// further writes are unblocked.
638    fn wait_for_event(&mut self) -> LoopEvent<T, N> {
639        // Take ownership of persistence rx if present
640        let maybe_persistence = self.persistence_state.rx.take();
641
642        if let Some((persistence_rx, start_time, action)) = maybe_persistence {
643            // Biased select prioritizes persistence completion to update in memory state and
644            // unblock further writes
645            crossbeam_channel::select_biased! {
646                recv(persistence_rx) -> result => {
647                    // Don't put it back - consumed (oneshot-like behavior)
648                    match result {
649                        Ok(result) => LoopEvent::PersistenceComplete {
650                            result,
651                            start_time,
652                        },
653                        Err(_) => LoopEvent::Disconnected,
654                    }
655                },
656                recv(self.payload_build_finished) -> result => {
657                    // Put the persistence rx back - we didn't consume it.
658                    self.persistence_state.rx = Some((persistence_rx, start_time, action));
659                    match result {
660                        Ok(()) => LoopEvent::PayloadBuildFinished,
661                        Err(_) => LoopEvent::Disconnected,
662                    }
663                },
664                recv(self.incoming) -> msg => {
665                    // Put the persistence rx back - we didn't consume it
666                    self.persistence_state.rx = Some((persistence_rx, start_time, action));
667                    match msg {
668                        Ok(m) => LoopEvent::EngineMessage(m),
669                        Err(_) => LoopEvent::Disconnected,
670                    }
671                },
672            }
673        } else {
674            // No persistence in progress - wait on an incoming message or payload job completion.
675            crossbeam_channel::select_biased! {
676                recv(self.payload_build_finished) -> result => match result {
677                    Ok(()) => LoopEvent::PayloadBuildFinished,
678                    Err(_) => LoopEvent::Disconnected,
679                },
680                recv(self.incoming) -> msg => match msg {
681                    Ok(m) => LoopEvent::EngineMessage(m),
682                    Err(_) => LoopEvent::Disconnected,
683                },
684            }
685        }
686    }
687
688    /// Invoked when previously requested blocks were downloaded.
689    ///
690    /// If the block count exceeds the configured batch size we're allowed to execute at once, this
691    /// will execute the first batch and send the remaining blocks back through the channel so that
692    /// block request processing isn't blocked for a long time.
693    fn on_downloaded(
694        &mut self,
695        mut blocks: Vec<SealedBlockWithAccessList<N::Block>>,
696    ) -> Result<Option<TreeEvent>, InsertBlockFatalError> {
697        if blocks.is_empty() {
698            // nothing to execute
699            return Ok(None)
700        }
701
702        trace!(target: "engine::tree", block_count = %blocks.len(), "received downloaded blocks");
703        let batch = self.config.max_execute_block_batch_size().min(blocks.len());
704        for block in blocks.drain(..batch) {
705            if let Some(event) = self.on_downloaded_block(block)? {
706                let needs_backfill = event.is_backfill_action();
707                self.on_tree_event(event)?;
708                if needs_backfill {
709                    // can exit early if backfill is needed
710                    return Ok(None)
711                }
712            }
713        }
714
715        // if we still have blocks to execute, send them as a followup request
716        if !blocks.is_empty() {
717            let _ = self.incoming_tx.send(FromEngine::DownloadedBlocks(blocks));
718        }
719
720        Ok(None)
721    }
722
723    /// When the Consensus layer receives a new block via the consensus gossip protocol,
724    /// the transactions in the block are sent to the execution layer in the form of a
725    /// [`PayloadTypes::ExecutionData`], for example
726    /// [`ExecutionData`](reth_payload_primitives::PayloadTypes::ExecutionData). The
727    /// Execution layer executes the transactions and validates the state in the block header,
728    /// then passes validation data back to Consensus layer, that adds the block to the head of
729    /// its own blockchain and attests to it. The block is then broadcast over the consensus p2p
730    /// network in the form of a "Beacon block".
731    ///
732    /// These responses should adhere to the [Engine API Spec for
733    /// `engine_newPayload`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#specification).
734    ///
735    /// This returns a [`PayloadStatus`] that represents the outcome of a processed new payload and
736    /// returns an error if an internal error occurred.
737    #[instrument(
738        level = "debug",
739        target = "engine::tree",
740        skip_all,
741        fields(block_hash = %payload.block_hash(), block_num = %payload.block_number()),
742    )]
743    fn on_new_payload(
744        &mut self,
745        payload: T::ExecutionData,
746    ) -> Result<TreeOutcome<PayloadStatus>, InsertBlockProcessingError> {
747        let _thread_resource_usage =
748            self.metrics.engine.new_payload.measure_thread_resource_usage();
749        trace!(target: "engine::tree", "invoked new payload");
750
751        // start timing for the new payload process
752        let start = Instant::now();
753
754        // Ensures that the given payload does not violate any consensus rules that concern the
755        // block's layout, like:
756        //    - missing or invalid base fee
757        //    - invalid extra data
758        //    - invalid transactions
759        //    - incorrect hash
760        //    - the versioned hashes passed with the payload do not exactly match transaction
761        //      versioned hashes
762        //    - the block does not contain blob transactions if it is pre-cancun
763        //
764        // This validates the following engine API rule:
765        //
766        // 3. Given the expected array of blob versioned hashes client software **MUST** run its
767        //    validation by taking the following steps:
768        //
769        //   1. Obtain the actual array by concatenating blob versioned hashes lists
770        //      (`tx.blob_versioned_hashes`) of each [blob
771        //      transaction](https://eips.ethereum.org/EIPS/eip-4844#new-transaction-type) included
772        //      in the payload, respecting the order of inclusion. If the payload has no blob
773        //      transactions the expected array **MUST** be `[]`.
774        //
775        //   2. Return `{status: INVALID, latestValidHash: null, validationError: errorMessage |
776        //      null}` if the expected and the actual arrays don't match.
777        //
778        // This validation **MUST** be instantly run in all cases even during active sync process.
779
780        let num_hash = payload.num_hash();
781        let engine_event = ConsensusEngineEvent::BlockReceived(num_hash);
782        self.emit_event(EngineApiEvent::BeaconConsensus(engine_event));
783
784        let block_hash = num_hash.hash;
785
786        // Check for invalid ancestors
787        if let Some(invalid) = self.find_invalid_ancestor(&payload) {
788            let status = self.handle_invalid_ancestor_payload(payload, invalid)?;
789            return Ok(TreeOutcome::new(status));
790        }
791
792        // record pre-execution phase duration
793        self.metrics.block_validation.record_payload_validation(start.elapsed().as_secs_f64());
794
795        let mut outcome = if self.backfill_sync_state.is_idle() {
796            self.try_insert_payload(payload)?.into_outcome()
797        } else {
798            TreeOutcome::new(self.try_buffer_payload(payload)?)
799        };
800
801        // if the block is valid and it is the current sync target head, make it canonical
802        if outcome.outcome.is_valid() && self.is_sync_target_head(block_hash) {
803            // Only create the canonical event if this block isn't already the canonical head
804            if self.state.tree_state.canonical_block_hash() != block_hash {
805                outcome = outcome.with_event(TreeEvent::TreeAction(TreeAction::MakeCanonical {
806                    sync_target_head: block_hash,
807                }));
808            }
809        }
810
811        // record total newPayload duration
812        self.metrics.block_validation.total_duration.record(start.elapsed().as_secs_f64());
813
814        Ok(outcome)
815    }
816
817    /// Processes a payload during normal sync operation.
818    #[instrument(level = "debug", target = "engine::tree", skip_all)]
819    fn try_insert_payload(
820        &mut self,
821        payload: T::ExecutionData,
822    ) -> Result<TryInsertPayloadResult, InsertBlockProcessingError> {
823        let block_hash = payload.block_hash();
824        let num_hash = payload.num_hash();
825        let parent_hash = payload.parent_hash();
826        let mut latest_valid_hash = None;
827
828        match self.insert_payload(payload) {
829            Ok(status) => {
830                let (status, already_seen) = match status {
831                    InsertPayloadOk::Inserted(BlockStatus::Valid) => {
832                        latest_valid_hash = Some(block_hash);
833                        self.try_connect_buffered_blocks(num_hash)?;
834                        (PayloadStatusEnum::Valid, false)
835                    }
836                    InsertPayloadOk::AlreadySeen(BlockStatus::Valid) => {
837                        latest_valid_hash = Some(block_hash);
838                        (PayloadStatusEnum::Valid, true)
839                    }
840                    InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) => {
841                        (PayloadStatusEnum::Syncing, false)
842                    }
843                    InsertPayloadOk::AlreadySeen(BlockStatus::Disconnected { .. }) => {
844                        // not known to be invalid, but we don't know anything else
845                        (PayloadStatusEnum::Syncing, true)
846                    }
847                };
848
849                Ok(TryInsertPayloadResult {
850                    status: PayloadStatus::new(status, latest_valid_hash),
851                    already_seen,
852                })
853            }
854            Err(error) => {
855                let status = match error {
856                    InsertPayloadError::Block(error) => self.on_insert_block_error(error)?,
857                    InsertPayloadError::Payload(error) => self
858                        .on_new_payload_error(error, num_hash, parent_hash)
859                        .map_err(InsertBlockFatalError::from)?,
860                };
861
862                Ok(TryInsertPayloadResult { status, already_seen: false })
863            }
864        }
865    }
866
867    /// Stores a payload for later processing during backfill sync.
868    ///
869    /// During backfill, the node lacks the state needed to validate payloads,
870    /// so they are buffered (stored in memory) until their parent blocks are synced.
871    ///
872    /// Returns:
873    /// - `Syncing`: Payload successfully buffered
874    /// - Error status: Payload is malformed or invalid
875    fn try_buffer_payload(
876        &mut self,
877        payload: T::ExecutionData,
878    ) -> Result<PayloadStatus, InsertBlockProcessingError> {
879        let parent_hash = payload.parent_hash();
880        let num_hash = payload.num_hash();
881
882        match self.payload_validator.convert_payload_to_block(payload) {
883            // if the block is well-formed, buffer it for later
884            Ok(block) => {
885                if let Err(error) = self.buffer_block(block) {
886                    self.on_insert_block_error(error)
887                } else {
888                    Ok(PayloadStatus::from_status(PayloadStatusEnum::Syncing))
889                }
890            }
891            Err(error) => Ok(self
892                .on_new_payload_error(error, num_hash, parent_hash)
893                .map_err(InsertBlockFatalError::from)?),
894        }
895    }
896
897    /// Returns the new chain for the given head.
898    ///
899    /// This also handles reorgs.
900    ///
901    /// Note: This does not update the tracked state and instead returns the new chain based on the
902    /// given head.
903    fn on_new_head(&self, new_head: B256) -> ProviderResult<Option<NewCanonicalChain<N>>> {
904        // get the executed new head block
905        let Some(new_head_block) = self.state.tree_state.blocks_by_hash.get(&new_head) else {
906            debug!(target: "engine::tree", new_head=?new_head, "New head block not found in inmemory tree state");
907            self.metrics.engine.executed_new_block_cache_miss.increment(1);
908            return Ok(None)
909        };
910
911        let new_head_number = new_head_block.recovered_block().number();
912        let mut current_canonical_number = self.state.tree_state.current_canonical_head.number;
913
914        let mut new_chain = vec![new_head_block.clone()];
915        let mut current_hash = new_head_block.recovered_block().parent_hash();
916        let mut current_number = new_head_number - 1;
917
918        // Walk back the new chain until we reach a block we know about
919        //
920        // This is only done for in-memory blocks, because we should not have persisted any blocks
921        // that are _above_ the current canonical head.
922        while current_number > current_canonical_number {
923            if let Some(block) = self.state.tree_state.executed_block_by_hash(current_hash).cloned()
924            {
925                current_hash = block.recovered_block().parent_hash();
926                current_number -= 1;
927                new_chain.push(block);
928            } else {
929                warn!(target: "engine::tree", current_hash=?current_hash, "Sidechain block not found in TreeState");
930                // This should never happen as we're walking back a chain that should connect to
931                // the canonical chain
932                return Ok(None)
933            }
934        }
935
936        // If we have reached the current canonical head by walking back from the target, then we
937        // know this represents an extension of the canonical chain.
938        if current_hash == self.state.tree_state.current_canonical_head.hash {
939            new_chain.reverse();
940
941            // Simple extension of the current chain
942            return Ok(Some(NewCanonicalChain::Commit { new: new_chain }))
943        }
944
945        // We have a reorg. Walk back both chains to find the fork point.
946        let mut old_chain = Vec::new();
947        let mut old_hash = self.state.tree_state.current_canonical_head.hash;
948
949        // If the canonical chain is ahead of the new chain,
950        // gather all blocks until new head number.
951        while current_canonical_number > current_number {
952            let block = self.canonical_block_by_hash(old_hash)?;
953            old_hash = block.recovered_block().parent_hash();
954            old_chain.push(block);
955            current_canonical_number -= 1;
956        }
957
958        // Both new and old chain pointers are now at the same height.
959        debug_assert_eq!(current_number, current_canonical_number);
960
961        // Walk both chains from specified hashes at same height until
962        // a common ancestor (fork block) is reached.
963        while old_hash != current_hash {
964            let block = self.canonical_block_by_hash(old_hash)?;
965            old_hash = block.recovered_block().parent_hash();
966            old_chain.push(block);
967
968            if let Some(block) = self.state.tree_state.executed_block_by_hash(current_hash).cloned()
969            {
970                current_hash = block.recovered_block().parent_hash();
971                new_chain.push(block);
972            } else {
973                // This shouldn't happen as we've already walked this path
974                warn!(target: "engine::tree", invalid_hash=?current_hash, "New chain block not found in TreeState");
975                return Ok(None)
976            }
977        }
978        new_chain.reverse();
979        old_chain.reverse();
980
981        Ok(Some(NewCanonicalChain::Reorg { new: new_chain, old: old_chain }))
982    }
983
984    /// Updates the latest block state to the specified canonical ancestor.
985    ///
986    /// This method ensures that the latest block tracks the given canonical header by resetting
987    ///
988    /// # Arguments
989    /// * `canonical_header` - The canonical header to set as the new head
990    ///
991    /// # Returns
992    /// * `ProviderResult<()>` - Ok(()) on success, error if state update fails
993    ///
994    /// Caution: This unwinds the canonical chain
995    fn update_latest_block_to_canonical_ancestor(
996        &mut self,
997        canonical_header: &SealedHeader<N::BlockHeader>,
998    ) -> ProviderResult<()> {
999        debug!(target: "engine::tree", head = ?canonical_header.num_hash(), "Update latest block to canonical ancestor");
1000        let current_head_number = self.state.tree_state.canonical_block_number();
1001        let new_head_number = canonical_header.number();
1002        let new_head_hash = canonical_header.hash();
1003
1004        // Update tree state with the new canonical head
1005        self.state.tree_state.set_canonical_head(canonical_header.num_hash());
1006
1007        // Handle the state update based on whether this is an unwind scenario
1008        if new_head_number < current_head_number {
1009            debug!(
1010                target: "engine::tree",
1011                current_head = current_head_number,
1012                new_head = new_head_number,
1013                new_head_hash = ?new_head_hash,
1014                "FCU unwind detected: reverting to canonical ancestor"
1015            );
1016
1017            self.handle_canonical_chain_unwind(current_head_number, canonical_header)
1018        } else {
1019            debug!(
1020                target: "engine::tree",
1021                previous_head = current_head_number,
1022                new_head = new_head_number,
1023                new_head_hash = ?new_head_hash,
1024                "Advancing latest block to canonical ancestor"
1025            );
1026            self.handle_chain_advance_or_same_height(canonical_header)
1027        }
1028    }
1029
1030    /// Handles chain unwind scenarios by collecting blocks to remove and performing an unwind back
1031    /// to the canonical header
1032    fn handle_canonical_chain_unwind(
1033        &self,
1034        current_head_number: u64,
1035        canonical_header: &SealedHeader<N::BlockHeader>,
1036    ) -> ProviderResult<()> {
1037        let new_head_number = canonical_header.number();
1038        debug!(
1039            target: "engine::tree",
1040            from = current_head_number,
1041            to = new_head_number,
1042            "Handling unwind: collecting blocks to remove from in-memory state"
1043        );
1044
1045        // Collect blocks that need to be removed from memory
1046        let old_blocks =
1047            self.collect_blocks_for_canonical_unwind(new_head_number, current_head_number);
1048
1049        // Load and apply the canonical ancestor block
1050        self.apply_canonical_ancestor_via_reorg(canonical_header, old_blocks)
1051    }
1052
1053    /// Collects blocks from memory that need to be removed during an unwind to a canonical block.
1054    fn collect_blocks_for_canonical_unwind(
1055        &self,
1056        new_head_number: u64,
1057        current_head_number: u64,
1058    ) -> Vec<ExecutedBlock<N>> {
1059        let mut old_blocks =
1060            Vec::with_capacity((current_head_number.saturating_sub(new_head_number)) as usize);
1061
1062        for block_num in (new_head_number + 1)..=current_head_number {
1063            if let Some(block_state) = self.canonical_in_memory_state.state_by_number(block_num) {
1064                let executed_block = block_state.block_ref().clone();
1065                old_blocks.push(executed_block);
1066                debug!(
1067                    target: "engine::tree",
1068                    block_number = block_num,
1069                    "Collected block for removal from in-memory state"
1070                );
1071            }
1072        }
1073
1074        if old_blocks.is_empty() {
1075            debug!(
1076                target: "engine::tree",
1077                "No blocks found in memory to remove, will clear and reset state"
1078            );
1079        }
1080
1081        old_blocks
1082    }
1083
1084    /// Applies the canonical ancestor block via a reorg operation.
1085    fn apply_canonical_ancestor_via_reorg(
1086        &self,
1087        canonical_header: &SealedHeader<N::BlockHeader>,
1088        old_blocks: Vec<ExecutedBlock<N>>,
1089    ) -> ProviderResult<()> {
1090        let new_head_hash = canonical_header.hash();
1091        let new_head_number = canonical_header.number();
1092
1093        // Load the canonical ancestor's block
1094        let executed_block = self.canonical_block_by_hash(new_head_hash)?;
1095        // Perform the reorg to properly handle the unwind
1096        self.canonical_in_memory_state
1097            .update_chain(NewCanonicalChain::Reorg { new: vec![executed_block], old: old_blocks });
1098
1099        // CRITICAL: Update the canonical head after the reorg
1100        // This ensures get_canonical_head() returns the correct block
1101        self.canonical_in_memory_state.set_canonical_head(canonical_header.clone());
1102
1103        debug!(
1104            target: "engine::tree",
1105            block_number = new_head_number,
1106            block_hash = ?new_head_hash,
1107            "Successfully loaded canonical ancestor into memory via reorg"
1108        );
1109
1110        Ok(())
1111    }
1112
1113    /// Handles chain advance or same height scenarios.
1114    fn handle_chain_advance_or_same_height(
1115        &self,
1116        canonical_header: &SealedHeader<N::BlockHeader>,
1117    ) -> ProviderResult<()> {
1118        // Load the block into memory if it's not already present
1119        self.ensure_block_in_memory(canonical_header.number(), canonical_header.hash())?;
1120
1121        // Update the canonical head header
1122        self.canonical_in_memory_state.set_canonical_head(canonical_header.clone());
1123
1124        Ok(())
1125    }
1126
1127    /// Ensures a block is loaded into memory if not already present.
1128    fn ensure_block_in_memory(&self, block_number: u64, block_hash: B256) -> ProviderResult<()> {
1129        // Check if block is already in memory
1130        if self.canonical_in_memory_state.state_by_number(block_number).is_some() {
1131            return Ok(());
1132        }
1133
1134        // Load the block from storage
1135        let executed_block = self.canonical_block_by_hash(block_hash)?;
1136        self.canonical_in_memory_state
1137            .update_chain(NewCanonicalChain::Commit { new: vec![executed_block] });
1138
1139        debug!(
1140            target: "engine::tree",
1141            block_number,
1142            block_hash = ?block_hash,
1143            "Added canonical block to in-memory state"
1144        );
1145
1146        Ok(())
1147    }
1148
1149    /// Invoked when we receive a new forkchoice update message. Calls into the blockchain tree
1150    /// to resolve chain forks and ensure that the Execution Layer is working with the latest valid
1151    /// chain.
1152    ///
1153    /// These responses should adhere to the [Engine API Spec for
1154    /// `engine_forkchoiceUpdated`](https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#specification-1).
1155    ///
1156    /// Returns an error if an internal error occurred like a database error.
1157    #[instrument(level = "debug", target = "engine::tree", skip_all, fields(head = % state.head_block_hash, safe = % state.safe_block_hash,finalized = % state.finalized_block_hash))]
1158    fn on_forkchoice_updated(
1159        &mut self,
1160        state: ForkchoiceState,
1161        attrs: Option<T::PayloadAttributes>,
1162    ) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
1163        trace!(target: "engine::tree", ?attrs, "invoked forkchoice update");
1164
1165        // Record metrics
1166        self.record_forkchoice_metrics();
1167
1168        // Pre-validation of forkchoice state
1169        if let Some(early_result) = self.validate_forkchoice_state(state)? {
1170            return Ok(TreeOutcome::new(early_result));
1171        }
1172
1173        // Return early if we are on the correct fork
1174        if let Some(result) = self.handle_canonical_head(state, &attrs)? {
1175            return Ok(result);
1176        }
1177
1178        // Attempt to apply a chain update when the head differs from our canonical chain.
1179        // This handles reorgs and chain extensions by making the specified head canonical.
1180        if let Some(result) = self.apply_chain_update(state, &attrs)? {
1181            return Ok(result);
1182        }
1183
1184        // Fallback that ensures to catch up to the network's state.
1185        self.handle_missing_block(state)
1186    }
1187
1188    /// Records metrics for forkchoice updated calls
1189    fn record_forkchoice_metrics(&self) {
1190        self.canonical_in_memory_state.on_forkchoice_update_received();
1191    }
1192
1193    /// Pre-validates the forkchoice state and returns early if validation fails.
1194    ///
1195    /// Returns `Some(OnForkChoiceUpdated)` if validation fails and an early response should be
1196    /// returned. Returns `None` if validation passes and processing should continue.
1197    fn validate_forkchoice_state(
1198        &mut self,
1199        state: ForkchoiceState,
1200    ) -> ProviderResult<Option<OnForkChoiceUpdated>> {
1201        if state.head_block_hash.is_zero() {
1202            return Ok(Some(OnForkChoiceUpdated::invalid_state()));
1203        }
1204
1205        // Check if the new head hash is connected to any ancestor that we previously marked as
1206        // invalid
1207        let lowest_buffered_ancestor_fcu = self.lowest_buffered_ancestor_or(state.head_block_hash);
1208        if let Some(status) = self.check_invalid_ancestor(lowest_buffered_ancestor_fcu)? {
1209            return Ok(Some(OnForkChoiceUpdated::with_invalid(status)));
1210        }
1211
1212        if !self.backfill_sync_state.is_idle() {
1213            // Forward the head and finalized block, since a long-running backfill such as snap
1214            // must follow them. A run awaiting revalidation has not started and reads the latest
1215            // forkchoice when it does.
1216            if self.backfill_sync_state.is_pending() || self.backfill_sync_state.is_active() {
1217                self.send_event(EngineApiEvent::BackfillAction(BackfillAction::UpdateTarget(
1218                    state.head_block_hash,
1219                )));
1220                self.forward_finalized(state.finalized_block_hash);
1221            }
1222            // We can only process new forkchoice updates if the pipeline is idle, since it requires
1223            // exclusive access to the database
1224            trace!(target: "engine::tree", "Pipeline is syncing, skipping forkchoice update");
1225            return Ok(Some(OnForkChoiceUpdated::syncing()));
1226        }
1227
1228        Ok(None)
1229    }
1230
1231    /// Handles the case where the forkchoice head is already canonical.
1232    ///
1233    /// Returns `Some(TreeOutcome<OnForkChoiceUpdated>)` if the head is already canonical and
1234    /// processing is complete. Returns `None` if the head is not canonical and processing
1235    /// should continue.
1236    fn handle_canonical_head(
1237        &mut self,
1238        state: ForkchoiceState,
1239        attrs: &Option<T::PayloadAttributes>, // Changed to reference
1240    ) -> ProviderResult<Option<TreeOutcome<OnForkChoiceUpdated>>> {
1241        // Process the forkchoice update by trying to make the head block canonical
1242        //
1243        // We can only process this forkchoice update if:
1244        // - we have the `head` block
1245        // - the head block is part of a chain that is connected to the canonical chain. This
1246        //   includes reorgs.
1247        //
1248        // Performing a FCU involves:
1249        // - marking the FCU's head block as canonical
1250        // - updating in memory state to reflect the new canonical chain
1251        // - updating canonical state trackers
1252        // - emitting a canonicalization event for the new chain (including reorg)
1253        // - if we have payload attributes, delegate them to the payload service
1254
1255        if self.state.tree_state.canonical_block_hash() != state.head_block_hash {
1256            return Ok(None);
1257        }
1258
1259        trace!(target: "engine::tree", "fcu head hash is already canonical");
1260
1261        if !self.is_consistent_forkchoice_state(state, None)? {
1262            return Ok(Some(TreeOutcome::new(OnForkChoiceUpdated::invalid_state())));
1263        }
1264
1265        // Update the safe and finalized blocks and ensure their values are valid
1266        if let Err(outcome) = self.ensure_consistent_forkchoice_state(state) {
1267            // safe or finalized hashes are invalid
1268            return Ok(Some(TreeOutcome::new(outcome)));
1269        }
1270
1271        self.payload_validator.on_canonical_head_changed(state.head_block_hash, &self.state);
1272
1273        // Process payload attributes if the head is already canonical
1274        if let Some(attr) = attrs {
1275            let tip = self
1276                .sealed_header_by_hash(self.state.tree_state.canonical_block_hash())?
1277                .ok_or_else(|| {
1278                    // If we can't find the canonical block, then something is wrong and we need
1279                    // to return an error
1280                    ProviderError::HeaderNotFound(state.head_block_hash.into())
1281                })?;
1282            // Clone only when we actually need to process the attributes
1283            let updated = self.process_payload_attributes(attr.clone(), &tip, state);
1284            return Ok(Some(TreeOutcome::new(updated)));
1285        }
1286
1287        // The head block is already canonical
1288        Ok(Some(Self::valid_outcome(state)))
1289    }
1290
1291    /// Applies chain update for the new head block and processes payload attributes.
1292    ///
1293    /// This method handles the case where the forkchoice head differs from our current canonical
1294    /// head. It attempts to make the specified head block canonical by:
1295    /// - Checking if the head is already part of the canonical chain
1296    /// - Applying chain reorganizations (reorgs) if necessary
1297    /// - Processing payload attributes if provided
1298    /// - Returning the appropriate forkchoice update response
1299    ///
1300    /// Returns `Some(TreeOutcome<OnForkChoiceUpdated>)` if a chain update was successfully applied.
1301    /// Returns `None` if no chain update was needed or possible.
1302    fn apply_chain_update(
1303        &mut self,
1304        state: ForkchoiceState,
1305        attrs: &Option<T::PayloadAttributes>,
1306    ) -> ProviderResult<Option<TreeOutcome<OnForkChoiceUpdated>>> {
1307        // Check if the head is already part of the canonical chain
1308        if let Ok(Some(canonical_header)) = self.find_canonical_header(state.head_block_hash) {
1309            debug!(target: "engine::tree", head = canonical_header.number(), "fcu head block is already canonical");
1310
1311            // For OpStack, or if explicitly configured, the proposers are allowed to reorg their
1312            // own chain at will, so we need to always trigger a new payload job if requested.
1313            let always_trigger_payload_job = self.engine_kind.is_opstack() ||
1314                self.config.always_process_payload_attributes_on_canonical_head();
1315
1316            // A canonical ancestor below the latest known finalized block can never become the
1317            // head again, because this would reorg out the finalized block. Such a forkchoice
1318            // update exceeds the supported reorg depth and is rejected regardless of the payload
1319            // attributes:
1320            // <https://github.com/ethereum/execution-apis/blob/bf20b4083284e677db19e7f3871bd669b88354a6/src/engine/paris.md?plain=1#L221>
1321            //
1322            // The stored finalized block is used because a forkchoice update MAY carry a zero
1323            // finalized hash without clearing previously established finality.
1324            if !always_trigger_payload_job &&
1325                self.canonical_in_memory_state
1326                    .get_finalized_num_hash()
1327                    .is_some_and(|finalized| canonical_header.number() < finalized.number)
1328            {
1329                debug!(target: "engine::tree", head = canonical_header.number(), "rejecting canonical ancestor fcu below the finalized block");
1330                return Ok(Some(TreeOutcome::new(OnForkChoiceUpdated::too_deep_reorg())));
1331            }
1332
1333            if !self.is_consistent_forkchoice_state(state, None)? {
1334                return Ok(Some(TreeOutcome::new(OnForkChoiceUpdated::invalid_state())));
1335            }
1336
1337            // We need to effectively unwind the _canonical_ chain to the FCU's head, which is
1338            // part of the canonical chain. We need to update the latest block state to reflect
1339            // the canonical ancestor. This ensures that state providers and the transaction
1340            // pool operate with the correct chain state after forkchoice update processing, and
1341            // new payloads built on the reorg'd head will be added to the tree immediately.
1342            if always_trigger_payload_job && self.config.unwind_canonical_header() {
1343                self.update_latest_block_to_canonical_ancestor(&canonical_header)?;
1344            }
1345
1346            // A canonical ancestor at or above the latest known finalized block can become the
1347            // parent of the next block, e.g. when the CL wants to reorg out the current head.
1348            // The canonical chain remains untouched here; the block built on the ancestor
1349            // triggers the actual reorg once it is inserted via newPayload and FCU'd.
1350            if let Some(attr) = attrs {
1351                debug!(target: "engine::tree", head = canonical_header.number(), "handling payload attributes for canonical head");
1352                // Clone only when we actually need to process the attributes
1353                let updated =
1354                    self.process_payload_attributes(attr.clone(), &canonical_header, state);
1355                return Ok(Some(TreeOutcome::new(updated)));
1356            }
1357
1358            // The head block is already canonical and we're not processing payload attributes,
1359            // so we're not triggering a payload job and can return right away
1360            return Ok(Some(Self::valid_outcome(state)));
1361        }
1362
1363        // Ensure we can apply a new chain update for the head block
1364        if let Some(chain_update) = self.on_new_head(state.head_block_hash)? {
1365            if !self.is_consistent_forkchoice_state(state, Some(&chain_update))? {
1366                return Ok(Some(TreeOutcome::new(OnForkChoiceUpdated::invalid_state())));
1367            }
1368
1369            let tip = chain_update.tip().clone_sealed_header();
1370            self.on_canonical_chain_update(chain_update);
1371
1372            // Update the safe and finalized blocks and ensure their values are valid
1373            if let Err(outcome) = self.ensure_consistent_forkchoice_state(state) {
1374                // safe or finalized hashes are invalid
1375                return Ok(Some(TreeOutcome::new(outcome)));
1376            }
1377
1378            if let Some(attr) = attrs {
1379                // Clone only when we actually need to process the attributes
1380                let updated = self.process_payload_attributes(attr.clone(), &tip, state);
1381                return Ok(Some(TreeOutcome::new(updated)));
1382            }
1383
1384            return Ok(Some(Self::valid_outcome(state)));
1385        }
1386
1387        Ok(None)
1388    }
1389
1390    /// Handles the case where the head block is missing and needs to be downloaded.
1391    ///
1392    /// This is the fallback case when all other forkchoice update scenarios have been exhausted.
1393    /// Returns a `TreeOutcome` with syncing status and download event.
1394    fn handle_missing_block(
1395        &self,
1396        state: ForkchoiceState,
1397    ) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
1398        // We don't have the block to perform the forkchoice update
1399        // We assume the FCU is valid and at least the head is missing,
1400        // so we need to start syncing to it
1401        //
1402        // find the appropriate target to sync to, if we don't have the safe block hash then we
1403        // start syncing to the safe block via backfill first
1404        let target = if self.state.forkchoice_state_tracker.is_empty() &&
1405        // check that safe block is valid and missing
1406        !state.safe_block_hash.is_zero() &&
1407        self.find_canonical_header(state.safe_block_hash).ok().flatten().is_none()
1408        {
1409            debug!(target: "engine::tree", "missing safe block on initial FCU, downloading safe block");
1410            state.safe_block_hash
1411        } else {
1412            state.head_block_hash
1413        };
1414
1415        let target = self.lowest_buffered_ancestor_or(target);
1416        trace!(target: "engine::tree", %target, "downloading missing block");
1417
1418        Ok(TreeOutcome::new(OnForkChoiceUpdated::valid(PayloadStatus::from_status(
1419            PayloadStatusEnum::Syncing,
1420        )))
1421        .with_event(TreeEvent::Download(
1422            DownloadRequest::single_block(target)
1423                .with_access_lists(self.should_download_access_lists()),
1424        )))
1425    }
1426
1427    /// Helper method to remove blocks and set the persistence state. This ensures we keep track of
1428    /// the current persistence action while we're removing blocks.
1429    fn remove_blocks(&mut self, new_tip_num: u64) {
1430        debug!(target: "engine::tree", ?new_tip_num, last_persisted_block_number=?self.persistence_state.last_persisted_block.number, "Removing blocks using persistence task");
1431        if new_tip_num < self.persistence_state.last_persisted_block.number {
1432            debug!(target: "engine::tree", ?new_tip_num, "Starting remove blocks job");
1433            self.state.set_pending_sparse_trie_prune(false);
1434            let (tx, rx) = crossbeam_channel::bounded(1);
1435            let _ = self.persistence.remove_blocks_above(new_tip_num, tx);
1436            self.persistence_state.start_remove(new_tip_num, rx);
1437        }
1438    }
1439
1440    /// Helper method to save blocks and set the persistence state. This ensures we keep track of
1441    /// the current persistence action while we're saving blocks.
1442    fn persist_blocks(&mut self, input: SaveBlocksInput<N>) {
1443        let highest_num_hash = input.last_block();
1444        debug!(target: "engine::tree", count=input.persist_rest_blocks().len(), blocks = ?input.persist_rest_blocks().iter().map(|block| block.recovered_block().num_hash()).collect::<Vec<_>>(), "Persisting blocks");
1445
1446        let (tx, rx) = crossbeam_channel::bounded(1);
1447        let _ = self.persistence.save_blocks(input, tx);
1448
1449        self.persistence_state.start_save(highest_num_hash, rx);
1450    }
1451
1452    /// Triggers new persistence actions if no persistence task is currently in progress.
1453    ///
1454    /// This checks if we need to remove blocks (disk reorg) or save new blocks to disk.
1455    /// Persistence completion is handled separately via the `wait_for_event` method.
1456    fn advance_persistence(&mut self) -> Result<(), AdvancePersistenceError> {
1457        if !self.persistence_state.in_progress() {
1458            let payload_build_active = self.payload_builds.is_active();
1459            if let Some(new_tip_num) = self.find_disk_reorg()? {
1460                self.remove_blocks(new_tip_num)
1461            } else if self.backfill_sync_state.is_pending_revalidation() &&
1462                !payload_build_active &&
1463                self.persistence_state.last_state_trie_persisted_block !=
1464                    self.persistence_state.last_persisted_block
1465            {
1466                let Some(input) = self.get_save_blocks_input(PersistTarget::Persisted) else {
1467                    return Err(AdvancePersistenceError::StateTrieCatchupUnavailable)
1468                };
1469                self.persist_blocks(input);
1470            } else if self.backfill_sync_state.is_pending_revalidation() && !payload_build_active {
1471                self.revalidate_pending_backfill()?;
1472            } else if let Some(input) = self.get_save_blocks_input(PersistTarget::Threshold) {
1473                self.persist_blocks(input);
1474            }
1475        }
1476
1477        Ok(())
1478    }
1479
1480    /// Persists all remaining blocks until none are left.
1481    fn persist_until_complete(&mut self) -> Result<(), AdvancePersistenceError> {
1482        loop {
1483            // Wait for any in-progress persistence to complete (blocking)
1484            if let Some((rx, start_time, action)) = self.persistence_state.rx.take() {
1485                debug!(target: "engine::tree", ?action, "waiting for in-flight persistence");
1486                let result = rx.recv().map_err(|_| AdvancePersistenceError::ChannelClosed)?;
1487                self.on_persistence_complete(result, start_time)?;
1488                continue
1489            }
1490
1491            // Persistence can finish against a branch that the in-memory canonical chain has
1492            // reorged away from. Unwind that stale disk branch before building a head-targeted
1493            // save.
1494            if let Some(new_tip_num) = self.find_disk_reorg()? {
1495                self.remove_blocks(new_tip_num);
1496                continue
1497            }
1498
1499            let Some(input) = self.get_save_blocks_input(PersistTarget::Head) else {
1500                debug!(target: "engine::tree", "persistence complete, signaling termination");
1501                return Ok(())
1502            };
1503
1504            debug!(target: "engine::tree", count = input.persist_rest_blocks().len(), "persisting remaining blocks before shutdown");
1505            self.persist_blocks(input);
1506        }
1507    }
1508
1509    /// Tries to poll for a completed persistence task (non-blocking).
1510    ///
1511    /// Returns `true` if a persistence task was completed, `false` otherwise.
1512    fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
1513        let Some((rx, start_time, action)) = self.persistence_state.rx.take() else {
1514            return Ok(false);
1515        };
1516
1517        match rx.try_recv() {
1518            Ok(result) => {
1519                self.on_persistence_complete(result, start_time)?;
1520                Ok(true)
1521            }
1522            Err(crossbeam_channel::TryRecvError::Empty) => {
1523                // Not ready yet, put it back
1524                self.persistence_state.rx = Some((rx, start_time, action));
1525                Ok(false)
1526            }
1527            Err(crossbeam_channel::TryRecvError::Disconnected) => {
1528                Err(AdvancePersistenceError::ChannelClosed)
1529            }
1530        }
1531    }
1532
1533    /// Handles a completed persistence task.
1534    fn on_persistence_complete(
1535        &mut self,
1536        result: PersistenceResult,
1537        start_time: Instant,
1538    ) -> Result<(), AdvancePersistenceError> {
1539        self.metrics.engine.persistence_duration.record(start_time.elapsed());
1540
1541        let PersistenceResult { last_block, last_state_trie_block, commit_duration } = result;
1542        debug_assert!(
1543            last_state_trie_block.number <= last_block.number,
1544            "state/trie frontier cannot exceed the last persisted block"
1545        );
1546
1547        debug!(target: "engine::tree", ?last_block, ?last_state_trie_block, elapsed=?start_time.elapsed(), "Finished persisting, calling finish");
1548        self.persistence_state.finish(last_block, last_state_trie_block);
1549
1550        let last_block_number = last_block.number;
1551
1552        // Evict cached changesets for blocks below the eviction threshold.
1553        // Keep at least CHANGESET_CACHE_RETENTION_BLOCKS from the persisted tip, and also respect
1554        // the finalized block if set.
1555        let min_threshold = last_block_number.saturating_sub(CHANGESET_CACHE_RETENTION_BLOCKS);
1556        let eviction_threshold =
1557            if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() {
1558                // Use the minimum of finalized block and retention threshold to be conservative
1559                finalized.number.min(min_threshold)
1560            } else {
1561                // When finalized is not set (e.g., on L2s), use the retention threshold
1562                min_threshold
1563            };
1564        debug!(
1565            target: "engine::tree",
1566            last_persisted = last_block_number,
1567            finalized_number = ?self.canonical_in_memory_state.get_finalized_num_hash().map(|f| f.number),
1568            eviction_threshold,
1569            "Evicting changesets below threshold"
1570        );
1571        self.state.tree_state.overlay_manager.evict_cached_changesets(eviction_threshold);
1572
1573        self.on_new_persisted_block()?;
1574
1575        self.purge_timing_stats(last_block_number, commit_duration);
1576
1577        Ok(())
1578    }
1579
1580    /// Handles a message from the engine.
1581    ///
1582    /// Returns `ControlFlow::Break` with the sender of the termination request if the engine
1583    /// should terminate.
1584    fn on_engine_message(
1585        &mut self,
1586        msg: FromEngine<EngineApiRequest<T, N>, N::Block>,
1587    ) -> Result<ops::ControlFlow<oneshot::Sender<()>>, InsertBlockFatalError> {
1588        match msg {
1589            FromEngine::Event(event) => match event {
1590                FromOrchestrator::BackfillSyncStarted => {
1591                    debug!(target: "engine::tree", "received backfill sync started event");
1592                    self.backfill_sync_state = BackfillSyncState::Active;
1593                }
1594                FromOrchestrator::BackfillSyncFinished(ctrl) => {
1595                    self.on_backfill_sync_finished(ctrl)?;
1596                }
1597                FromOrchestrator::Terminate { tx } => {
1598                    debug!(target: "engine::tree", "received terminate request");
1599                    trace!(target: "engine::tree", "finishing termination, persisting remaining blocks");
1600                    if let Err(err) = self.persist_until_complete() {
1601                        error!(target: "engine::tree", %err, "Termination failed");
1602                    }
1603                    return Ok(ops::ControlFlow::Break(tx))
1604                }
1605            },
1606            FromEngine::Request(request) => {
1607                match request {
1608                    EngineApiRequest::InsertExecutedBlock(payload) => {
1609                        let block_num_hash = payload.recovered_block.num_hash();
1610                        if block_num_hash.number <= self.state.tree_state.canonical_block_number() {
1611                            // outdated block that can be skipped
1612                            return Ok(ops::ControlFlow::Continue(()))
1613                        }
1614
1615                        if self.state.tree_state.contains_hash(&block_num_hash.hash) {
1616                            // block already known to the tree (e.g. delivered via newPayload first)
1617                            return Ok(ops::ControlFlow::Continue(()))
1618                        }
1619
1620                        debug!(target: "engine::tree", block=?block_num_hash, "inserting already executed block");
1621                        let now = Instant::now();
1622
1623                        let block = match self.payload_validator.on_inserted_executed_block(payload)
1624                        {
1625                            Ok(block) => block,
1626                            Err(err) => {
1627                                warn!(target: "engine::tree", %err, block=?block_num_hash, "Failed to insert already executed block");
1628                                return Ok(ops::ControlFlow::Continue(()))
1629                            }
1630                        };
1631
1632                        let is_pending = self.state.tree_state.canonical_block_hash() ==
1633                            block.recovered_block().parent_hash();
1634                        self.state.tree_state.insert_executed(block.clone());
1635                        self.metrics
1636                            .engine
1637                            .executed_blocks
1638                            .set(self.state.tree_state.block_count() as f64);
1639
1640                        if is_pending {
1641                            debug!(target: "engine::tree", pending=?block_num_hash, "updating pending block");
1642                            self.canonical_in_memory_state.set_pending_block(block.clone());
1643                        }
1644
1645                        self.metrics.engine.inserted_already_executed_blocks.increment(1);
1646                        self.emit_event(EngineApiEvent::BeaconConsensus(
1647                            ConsensusEngineEvent::CanonicalBlockAdded(block, now.elapsed()),
1648                        ));
1649                    }
1650                    EngineApiRequest::Beacon(request) => {
1651                        match request {
1652                            BeaconEngineMessage::ForkchoiceUpdated {
1653                                cause,
1654                                state,
1655                                payload_attrs,
1656                                tx,
1657                            } => {
1658                                let _cause = cause.enter();
1659                                let has_attrs = payload_attrs.is_some();
1660
1661                                let start = Instant::now();
1662                                let mut output = self.on_forkchoice_updated(state, payload_attrs);
1663
1664                                if let Ok(res) = &mut output {
1665                                    // track last received forkchoice state
1666                                    self.state
1667                                        .forkchoice_state_tracker
1668                                        .set_latest(state, res.outcome.forkchoice_status());
1669
1670                                    // emit an event about the handled FCU
1671                                    self.emit_event(ConsensusEngineEvent::ForkchoiceUpdated(
1672                                        state,
1673                                        res.outcome.forkchoice_status(),
1674                                    ));
1675
1676                                    // handle the event if any
1677                                    self.on_maybe_tree_event(res.event.take())?;
1678                                }
1679
1680                                if let Err(ref err) = output {
1681                                    error!(target: "engine::tree", %err, ?state, "Error processing forkchoice update");
1682                                }
1683
1684                                self.metrics.engine.forkchoice_updated.update_response_metrics(
1685                                    start,
1686                                    &mut self.metrics.engine.new_payload.latest_finish_at,
1687                                    has_attrs,
1688                                    &output,
1689                                );
1690
1691                                if let Err(err) =
1692                                    tx.send(output.map(|o| o.outcome).map_err(Into::into))
1693                                {
1694                                    self.metrics
1695                                        .engine
1696                                        .failed_forkchoice_updated_response_deliveries
1697                                        .increment(1);
1698                                    warn!(target: "engine::tree", ?state, elapsed=?start.elapsed(), "Failed to deliver forkchoiceUpdated response, receiver dropped (request cancelled): {err:?}");
1699                                }
1700                            }
1701                            BeaconEngineMessage::NewPayload { cause, payload, tx } => {
1702                                let _cause = cause.enter();
1703                                let start = Instant::now();
1704                                let gas_used = payload.gas_used();
1705                                let num_hash = payload.num_hash();
1706                                let mut output = self.on_new_payload(payload);
1707                                self.metrics.engine.new_payload.update_response_metrics(
1708                                    start,
1709                                    &mut self.metrics.engine.forkchoice_updated.latest_finish_at,
1710                                    &output,
1711                                    gas_used,
1712                                );
1713
1714                                let maybe_event =
1715                                    output.as_mut().ok().and_then(|out| out.event.take());
1716
1717                                // emit response
1718                                if let Err(err) =
1719                                    tx.send(output.map(|o| o.outcome).map_err(Into::into))
1720                                {
1721                                    warn!(target: "engine::tree", payload=?num_hash, elapsed=?start.elapsed(), "Failed to deliver newPayload response, receiver dropped (request cancelled): {err:?}");
1722                                    self.metrics
1723                                        .engine
1724                                        .failed_new_payload_response_deliveries
1725                                        .increment(1);
1726                                }
1727
1728                                // handle the event if any
1729                                self.on_maybe_tree_event(maybe_event)?;
1730                            }
1731                            BeaconEngineMessage::RethNewPayload {
1732                                cause,
1733                                payload,
1734                                wait_for_persistence,
1735                                wait_for_caches,
1736                                tx,
1737                                enqueued_at,
1738                            } => {
1739                                let _cause = cause.enter();
1740                                debug!(
1741                                    target: "engine::tree",
1742                                    wait_for_persistence,
1743                                    wait_for_caches,
1744                                    "Processing reth_newPayload"
1745                                );
1746
1747                                let backpressure_wait = enqueued_at.elapsed();
1748
1749                                let explicit_persistence_wait = if wait_for_persistence {
1750                                    let pending_persistence = self.persistence_state.rx.take();
1751                                    if let Some((rx, start_time, _action)) = pending_persistence {
1752                                        let (persistence_tx, persistence_rx) =
1753                                            std::sync::mpsc::channel();
1754                                        self.runtime.spawn_blocking_named(
1755                                            "wait-persist",
1756                                            move || {
1757                                                let start = Instant::now();
1758                                                let result = rx
1759                                                    .recv()
1760                                                    .expect("persistence state channel closed");
1761                                                let _ = persistence_tx.send((
1762                                                    result,
1763                                                    start_time,
1764                                                    start.elapsed(),
1765                                                ));
1766                                            },
1767                                        );
1768                                        let (result, start_time, wait_duration) = persistence_rx
1769                                            .recv()
1770                                            .expect("persistence result channel closed");
1771                                        let _ = self.on_persistence_complete(result, start_time);
1772                                        wait_duration
1773                                    } else {
1774                                        Duration::ZERO
1775                                    }
1776                                } else {
1777                                    Duration::ZERO
1778                                };
1779
1780                                let cache_wait = wait_for_caches
1781                                    .then(|| self.payload_validator.wait_for_caches());
1782
1783                                let start = Instant::now();
1784                                let gas_used = payload.gas_used();
1785                                let num_hash = payload.num_hash();
1786                                let mut output = self.on_new_payload(payload);
1787                                let latency = start.elapsed();
1788                                self.metrics.engine.new_payload.update_response_metrics(
1789                                    start,
1790                                    &mut self.metrics.engine.forkchoice_updated.latest_finish_at,
1791                                    &output,
1792                                    gas_used,
1793                                );
1794
1795                                let maybe_event =
1796                                    output.as_mut().ok().and_then(|out| out.event.take());
1797
1798                                let timings = NewPayloadTimings {
1799                                    latency,
1800                                    persistence_wait: backpressure_wait + explicit_persistence_wait,
1801                                    execution_cache_wait: cache_wait
1802                                        .map(|wait| wait.execution_cache),
1803                                    sparse_trie_wait: cache_wait.map(|wait| wait.sparse_trie),
1804                                };
1805                                if let Err(err) = tx
1806                                    .send(output.map(|o| (o.outcome, timings)).map_err(Into::into))
1807                                {
1808                                    error!(
1809                                        target: "engine::tree",
1810                                        payload=?num_hash,
1811                                        elapsed=?latency,
1812                                        "Failed to send event: {err:?}"
1813                                    );
1814                                    self.metrics
1815                                        .engine
1816                                        .failed_new_payload_response_deliveries
1817                                        .increment(1);
1818                                }
1819
1820                                self.on_maybe_tree_event(maybe_event)?;
1821                            }
1822                        }
1823                    }
1824                }
1825            }
1826            FromEngine::DownloadedBlocks(blocks) => {
1827                if let Some(event) = self.on_downloaded(blocks)? {
1828                    self.on_tree_event(event)?;
1829                }
1830            }
1831        }
1832        Ok(ops::ControlFlow::Continue(()))
1833    }
1834
1835    /// Invoked if the backfill sync has finished to target.
1836    ///
1837    /// At this point we consider the block synced to the backfill target.
1838    ///
1839    /// Checks the tracked finalized block against the block on disk and requests another backfill
1840    /// run if the distance to the tip exceeds the threshold for another backfill run.
1841    ///
1842    /// This will also do the necessary housekeeping of the tree state, this includes:
1843    ///  - removing all blocks below the backfill height
1844    ///  - resetting the canonical in-memory state
1845    ///
1846    /// In case backfill resulted in an unwind, this will clear the tree state above the unwind
1847    /// target block.
1848    fn on_backfill_sync_finished(
1849        &mut self,
1850        ctrl: ControlFlow,
1851    ) -> Result<(), InsertBlockFatalError> {
1852        debug!(target: "engine::tree", "received backfill sync finished event");
1853        self.backfill_sync_state = BackfillSyncState::Idle;
1854
1855        // Pipeline unwound, memorize the invalid block and wait for CL for next sync target.
1856        let backfill_height = if let ControlFlow::Unwind { bad_block, target } = &ctrl {
1857            warn!(target: "engine::tree", invalid_block=?bad_block, "Bad block detected in unwind");
1858            // update the `invalid_headers` cache with the new invalid header
1859            self.state.invalid_headers.insert(**bad_block);
1860
1861            // if this was an unwind then the target is the new height
1862            Some(*target)
1863        } else {
1864            // backfill height is the block number that the backfill finished at
1865            ctrl.block_number()
1866        };
1867
1868        // backfill height is the block number that the backfill finished at
1869        let Some(backfill_height) = backfill_height else { return Ok(()) };
1870
1871        // state house keeping after backfill sync
1872        // remove all executed blocks below the backfill height
1873        //
1874        // We set the `finalized_num` to `Some(backfill_height)` to ensure we remove all state
1875        // before that
1876        let Some(backfill_num_hash) = self
1877            .provider
1878            .block_hash(backfill_height)?
1879            .map(|hash| BlockNumHash { hash, number: backfill_height })
1880        else {
1881            debug!(target: "engine::tree", ?ctrl, "Backfill block not found");
1882            return Ok(())
1883        };
1884
1885        if ctrl.is_unwind() {
1886            // the node reset so we need to clear everything above that height so that backfill
1887            // height is the new canonical block.
1888            self.state.set_pending_sparse_trie_prune(false);
1889            self.state.tree_state.reset(backfill_num_hash)
1890        } else {
1891            self.state.tree_state.remove_until(
1892                backfill_num_hash,
1893                self.persistence_state.last_persisted_block.hash,
1894                Some(backfill_num_hash),
1895            );
1896        }
1897
1898        self.metrics.engine.executed_blocks.set(self.state.tree_state.block_count() as f64);
1899        self.metrics.tree.canonical_chain_height.set(backfill_height as f64);
1900
1901        // remove all buffered blocks below the backfill height
1902        self.state.buffer.remove_old_blocks(backfill_height);
1903        self.purge_timing_stats(backfill_height, None);
1904        // we remove all entries because now we're synced to the backfill target and consider this
1905        // the canonical chain
1906        self.canonical_in_memory_state.clear_state();
1907
1908        if let Ok(Some(new_head)) = self.provider.sealed_header(backfill_height) {
1909            // update the tracked chain height, after backfill sync both the canonical height and
1910            // persisted height are the same
1911            self.state.tree_state.set_canonical_head(new_head.num_hash());
1912            self.persistence_state.finish(new_head.num_hash(), new_head.num_hash());
1913
1914            // update the tracked canonical head
1915            self.canonical_in_memory_state.set_canonical_head(new_head);
1916
1917            // If the pipeline reached the head of the syncing FCU, apply its safe and finalized
1918            // blocks now that the head is canonical. An unwind must wait for a new FCU because the
1919            // supplied sync target may have been invalidated.
1920            if !ctrl.is_unwind() {
1921                self.on_canonicalized_sync_target(backfill_num_hash.hash);
1922            }
1923        }
1924
1925        // check if we need to run backfill again by comparing the most recent backfill target
1926        // height to the backfill height
1927        let Some(sync_target_state) = self.state.forkchoice_state_tracker.sync_target_state()
1928        else {
1929            return Ok(())
1930        };
1931        if !self.engine_kind.is_opstack() && sync_target_state.finalized_block_hash.is_zero() {
1932            // no finalized block, can't check distance on non-OP Stack chains
1933            return Ok(())
1934        }
1935        let target_hash = self.backfill_target_hash(sync_target_state);
1936        if target_hash.is_zero() {
1937            return Ok(())
1938        }
1939        // get the block number of the backfill target block, if we have it buffered
1940        let newest_target = self.state.buffer.block(&target_hash).map(|block| block.number());
1941
1942        // The block number that the backfill finished at - if the progress or newest target is
1943        // None then we can't check the distance anyways.
1944        //
1945        // If both are Some, we perform another distance check and return the desired backfill
1946        // target
1947        if let Some(backfill_target) =
1948            ctrl.block_number().zip(newest_target).and_then(|(progress, target_number)| {
1949                // Determines whether or not we should run backfill again, in case
1950                // the new gap is still large enough and requires running backfill again
1951                self.backfill_sync_target(progress, target_number, None)
1952            })
1953        {
1954            // request another backfill run
1955            self.emit_event(EngineApiEvent::BackfillAction(BackfillAction::Start(
1956                backfill_target.into(),
1957            )));
1958            return Ok(())
1959        };
1960
1961        // Check if there are more blocks to sync between current head and FCU target
1962        if let Some(lowest_buffered) =
1963            self.state.buffer.lowest_ancestor(&sync_target_state.head_block_hash)
1964        {
1965            let current_head_num = self.state.tree_state.current_canonical_head.number;
1966            let target_head_num = lowest_buffered.number();
1967
1968            if let Some(distance) = self.distance_from_local_tip(current_head_num, target_head_num)
1969            {
1970                // There are blocks between current head and FCU target, download them
1971                debug!(
1972                    target: "engine::tree",
1973                    %current_head_num,
1974                    %target_head_num,
1975                    %distance,
1976                    "Backfill complete, downloading remaining blocks to reach FCU target"
1977                );
1978
1979                self.emit_event(EngineApiEvent::Download(
1980                    DownloadRequest::block_range(lowest_buffered.parent_hash(), distance)
1981                        .with_access_lists(self.should_download_access_lists()),
1982                ));
1983                return Ok(());
1984            }
1985        } else {
1986            // We don't have the head block or any of its ancestors buffered. Request
1987            // a download for the head block which will then trigger further sync.
1988            debug!(
1989                target: "engine::tree",
1990                head_hash = %sync_target_state.head_block_hash,
1991                "Backfill complete but head block not buffered, requesting download"
1992            );
1993            self.emit_event(EngineApiEvent::Download(
1994                DownloadRequest::single_block(sync_target_state.head_block_hash)
1995                    .with_access_lists(self.should_download_access_lists()),
1996            ));
1997            return Ok(());
1998        }
1999
2000        // try to close the gap by executing buffered blocks that are child blocks of the new head
2001        self.try_connect_buffered_blocks(self.state.tree_state.current_canonical_head)
2002    }
2003
2004    /// Attempts to make the given target canonical.
2005    ///
2006    /// This will update the tracked canonical in memory state and do the necessary housekeeping.
2007    fn make_canonical(&mut self, target: B256) -> ProviderResult<()> {
2008        if let Some(chain_update) = self.on_new_head(target)? {
2009            self.on_canonical_chain_update(chain_update);
2010        }
2011
2012        self.on_canonicalized_sync_target(target);
2013
2014        Ok(())
2015    }
2016
2017    /// Applies the tracked forkchoice state once its sync target head becomes canonical.
2018    fn on_canonicalized_sync_target(&mut self, target: B256) {
2019        let Some(sync_target_state) = self
2020            .state
2021            .forkchoice_state_tracker
2022            .sync_target_state()
2023            .filter(|state| state.head_block_hash == target)
2024        else {
2025            return;
2026        };
2027
2028        if let Err(outcome) = self.ensure_consistent_forkchoice_state(sync_target_state) {
2029            debug!(
2030                target: "engine::tree",
2031                head = %sync_target_state.head_block_hash,
2032                safe = %sync_target_state.safe_block_hash,
2033                finalized = %sync_target_state.finalized_block_hash,
2034                ?outcome,
2035                "Canonicalized sync target head before safe/finalized could be applied"
2036            );
2037            return;
2038        }
2039
2040        self.state.forkchoice_state_tracker.promote_sync_target_to_valid(sync_target_state);
2041    }
2042
2043    /// Convenience function to handle an optional tree event.
2044    fn on_maybe_tree_event(&mut self, event: Option<TreeEvent>) -> ProviderResult<()> {
2045        if let Some(event) = event {
2046            self.on_tree_event(event)?;
2047        }
2048
2049        Ok(())
2050    }
2051
2052    /// Handles a tree event.
2053    ///
2054    /// Returns an error if a [`TreeAction::MakeCanonical`] results in a fatal error.
2055    fn on_tree_event(&mut self, event: TreeEvent) -> ProviderResult<()> {
2056        match event {
2057            TreeEvent::TreeAction(action) => match action {
2058                TreeAction::MakeCanonical { sync_target_head } => {
2059                    self.make_canonical(sync_target_head)?;
2060                }
2061            },
2062            TreeEvent::BackfillAction(action) => {
2063                self.emit_event(EngineApiEvent::BackfillAction(action));
2064            }
2065            TreeEvent::Download(action) => {
2066                self.emit_event(EngineApiEvent::Download(action));
2067            }
2068        }
2069
2070        Ok(())
2071    }
2072
2073    /// Removes timing stats for blocks at or below `below_number`.
2074    ///
2075    /// No-op when detailed block logging is disabled (no stats are recorded in that case).
2076    /// When `commit_duration` is provided and a slow block threshold is configured, checks
2077    /// each removed block against the threshold and emits a [`ConsensusEngineEvent::SlowBlock`]
2078    /// event for blocks that exceed it.
2079    fn purge_timing_stats(&mut self, below_number: u64, commit_duration: Option<Duration>) {
2080        let threshold = self.config.slow_block_threshold();
2081        let check_slow = commit_duration.is_some() && threshold.is_some();
2082
2083        // Two-pass: collect keys first because emit_event borrows &mut self.
2084        let keys_to_remove: Vec<B256> = self
2085            .execution_timing_stats
2086            .iter()
2087            .filter(|(_, stats)| stats.block_number <= below_number)
2088            .map(|(k, _)| *k)
2089            .collect();
2090
2091        for key in keys_to_remove {
2092            let stats = self.execution_timing_stats.remove(&key).expect("key just found");
2093            if check_slow {
2094                let commit_dur = commit_duration.expect("checked above");
2095                // state_read_duration is already included in execution_duration
2096                let total_duration =
2097                    stats.execution_duration + stats.state_hash_duration + commit_dur;
2098
2099                if total_duration > threshold.expect("checked above") {
2100                    self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
2101                        stats,
2102                        commit_duration: Some(commit_dur),
2103                        total_duration,
2104                    }));
2105                }
2106            }
2107        }
2108    }
2109
2110    /// Re-evaluates whether a deferred backfill is still required after persistence catches up.
2111    fn revalidate_pending_backfill(&mut self) -> ProviderResult<()> {
2112        debug_assert!(self.backfill_sync_state.is_pending_revalidation());
2113
2114        let sync_target_state = self.state.forkchoice_state_tracker.sync_target_state();
2115        let backfill_target = if let Some(state) = sync_target_state {
2116            let configured_target = self.backfill_target_hash(state);
2117            let target_hash =
2118                if configured_target.is_zero() { state.head_block_hash } else { configured_target };
2119            let target_number = if let Some(block) = self.state.buffer.block(&target_hash) {
2120                Some(block.number())
2121            } else {
2122                self.sealed_header_by_hash(target_hash)?.map(|header| header.number())
2123            };
2124
2125            target_number.and_then(|target_number| {
2126                self.backfill_sync_target(
2127                    self.state.tree_state.canonical_block_number(),
2128                    target_number,
2129                    None,
2130                )
2131            })
2132        } else {
2133            None
2134        };
2135
2136        if let Some(target) = backfill_target {
2137            self.dispatch_backfill_action(BackfillAction::Start(target.into()));
2138            return Ok(())
2139        }
2140
2141        self.backfill_sync_state = BackfillSyncState::Idle;
2142        debug!(target: "engine::tree", "dropping deferred backfill after re-evaluation");
2143
2144        // The target may have changed while persistence was draining. Resume the live-sync
2145        // download flow so a newer target can produce a fresh backfill decision.
2146        if let Some(state) = sync_target_state &&
2147            state.head_block_hash != self.state.tree_state.canonical_block_hash()
2148        {
2149            let target = self.lowest_buffered_ancestor_or(state.head_block_hash);
2150            self.send_event(EngineApiEvent::Download(DownloadRequest::single_block(target)));
2151        }
2152
2153        Ok(())
2154    }
2155
2156    /// Emits an outgoing event to the engine.
2157    fn emit_event(&mut self, event: impl Into<EngineApiEvent<N>>) {
2158        let event = event.into();
2159
2160        if let EngineApiEvent::BackfillAction(action) = event {
2161            debug_assert_eq!(
2162                self.backfill_sync_state,
2163                BackfillSyncState::Idle,
2164                "backfill action should only be emitted when backfill is idle"
2165            );
2166
2167            let persistence_in_progress = self.persistence_state.in_progress();
2168            let state_trie_needs_catchup = self.persistence_state.last_state_trie_persisted_block !=
2169                self.persistence_state.last_persisted_block;
2170            if self.payload_builds.is_active() ||
2171                persistence_in_progress ||
2172                state_trie_needs_catchup
2173            {
2174                // Backfill can remove the same in-memory blocks as an active payload job or a
2175                // persistence task. Enter pending mode to prevent new payload jobs and
2176                // re-evaluate the current sync target after all readers and writes drain.
2177                debug!(
2178                    target: "engine::tree",
2179                    last_persisted_block = self.persistence_state.last_persisted_block.number,
2180                    last_state_trie_persisted_block = self
2181                        .persistence_state
2182                        .last_state_trie_persisted_block
2183                        .number,
2184                    "deferring backfill until persistence and payload jobs drain"
2185                );
2186                self.backfill_sync_state = BackfillSyncState::PendingRevalidation;
2187                return
2188            }
2189
2190            self.dispatch_backfill_action(action);
2191            return
2192        }
2193
2194        self.send_event(event);
2195    }
2196
2197    /// Dispatches a validated backfill action to the orchestrator.
2198    fn dispatch_backfill_action(&mut self, action: BackfillAction) {
2199        debug_assert!(
2200            self.backfill_sync_state.is_idle() ||
2201                self.backfill_sync_state.is_pending_revalidation(),
2202            "backfill action can only be dispatched while idle or pending revalidation"
2203        );
2204        self.backfill_sync_state = BackfillSyncState::Pending;
2205        self.metrics.engine.pipeline_runs.increment(1);
2206        // Forkchoice updates only forward finality to a running backfill, so the run starting here
2207        // gets the latest one up front.
2208        if let Some(state) = self.state.forkchoice_state_tracker.latest_state() {
2209            self.forward_finalized(state.finalized_block_hash);
2210        }
2211        debug!(target: "engine::tree", "emitting backfill action event");
2212        self.send_event(EngineApiEvent::BackfillAction(action));
2213    }
2214
2215    // Forwards `finalized` to the backfill unless the forkchoice leaves it unset.
2216    fn forward_finalized(&self, finalized: B256) {
2217        if !finalized.is_zero() {
2218            self.send_event(EngineApiEvent::BackfillAction(BackfillAction::UpdateFinalized(
2219                finalized,
2220            )));
2221        }
2222    }
2223
2224    /// Sends an event to the orchestrator.
2225    fn send_event(&self, event: EngineApiEvent<N>) {
2226        let _ = self.outgoing.send(event).inspect_err(
2227            |err| error!(target: "engine::tree", "Failed to send internal event: {err:?}"),
2228        );
2229    }
2230
2231    /// Returns the blocks and frontiers for the next persistence cycle, if one should start.
2232    ///
2233    /// Threshold persistence honors the normal scheduling gates and retains the configured
2234    /// in-memory block buffer. Persisted-target persistence catches the state/trie frontier up to
2235    /// the existing database tip. Head persistence bypasses those gates during shutdown and
2236    /// returns `None` once both persistence frontiers have reached the canonical head.
2237    fn get_save_blocks_input(&self, target: PersistTarget) -> Option<SaveBlocksInput<N>> {
2238        // We will calculate the state root using the database, so we need to be sure there are no
2239        // changes
2240        debug_assert!(!self.persistence_state.in_progress());
2241
2242        let prev_partial_state_trie = self.persistence_state.last_state_trie_persisted_block.number;
2243        let prev_db_tip = self.persistence_state.last_persisted_block.number;
2244        let canonical_head_number = self.state.tree_state.canonical_block_number();
2245
2246        let (new_db_tip, new_partial_state_trie) = match target {
2247            PersistTarget::Head => (canonical_head_number, canonical_head_number),
2248            PersistTarget::Persisted => {
2249                // Catch-up persistence is the transition into pipeline sync, so it deliberately
2250                // runs while backfill is pending and bypasses the normal threshold gates.
2251                debug_assert!(self.backfill_sync_state.is_pending_revalidation());
2252                debug_assert!(!self.payload_builds.is_active());
2253                (prev_db_tip, prev_db_tip)
2254            }
2255            PersistTarget::Threshold => {
2256                if (self.config.suppress_persistence_during_build() &&
2257                    self.payload_builds.is_active()) ||
2258                    !self.backfill_sync_state.is_idle()
2259                {
2260                    return None
2261                }
2262
2263                let persistence_threshold =
2264                    usize::try_from(self.config.persistence_threshold()).unwrap_or(usize::MAX);
2265                if self.canonical_in_memory_state.canonical_chain().count() <= persistence_threshold
2266                {
2267                    return None
2268                }
2269
2270                let new_db_tip =
2271                    canonical_head_number.saturating_sub(self.config.memory_block_buffer_target());
2272                if new_db_tip <= prev_db_tip {
2273                    return None
2274                }
2275
2276                let new_partial_state_trie = new_db_tip
2277                    .saturating_sub(self.config.num_state_masking_blocks())
2278                    .max(prev_partial_state_trie);
2279                (new_db_tip, new_partial_state_trie)
2280            }
2281        };
2282
2283        debug_assert!(
2284            new_db_tip >= prev_db_tip,
2285            "disk reorg must be resolved before saving blocks"
2286        );
2287        debug_assert!(
2288            new_partial_state_trie >= prev_partial_state_trie,
2289            "disk reorg must be resolved before saving state/trie"
2290        );
2291
2292        if new_db_tip == prev_db_tip && new_partial_state_trie == prev_partial_state_trie {
2293            return None
2294        }
2295
2296        let mut blocks = Vec::new();
2297        let mut current_hash = self.state.tree_state.canonical_block_hash();
2298
2299        debug!(
2300            target: "engine::tree",
2301            ?current_hash,
2302            ?prev_partial_state_trie,
2303            ?prev_db_tip,
2304            ?canonical_head_number,
2305            ?new_partial_state_trie,
2306            ?new_db_tip,
2307            target = ?target,
2308            "Returning save input"
2309        );
2310        while let Some(block) = self.state.tree_state.blocks_by_hash.get(&current_hash) {
2311            if block.recovered_block().number() <= prev_partial_state_trie {
2312                break;
2313            }
2314
2315            if block.recovered_block().number() <= new_db_tip {
2316                blocks.push(block.clone());
2317            }
2318
2319            current_hash = block.recovered_block().parent_hash();
2320        }
2321
2322        // Reverse the order so that the oldest block comes first
2323        blocks.reverse();
2324
2325        Some(SaveBlocksInput::new(
2326            blocks,
2327            prev_db_tip,
2328            prev_partial_state_trie,
2329            new_db_tip,
2330            new_partial_state_trie,
2331        ))
2332    }
2333
2334    /// This clears the blocks from the in-memory tree state that have been persisted to the
2335    /// database.
2336    ///
2337    /// This also updates the canonical in-memory state to reflect the newest persisted block
2338    /// height.
2339    ///
2340    /// Assumes that `finish` has been called on the `persistence_state` at least once
2341    fn on_new_persisted_block(&mut self) -> ProviderResult<()> {
2342        let in_memory_persisted_block = self.persistence_state.last_state_trie_persisted_block;
2343
2344        // If we have an on-disk reorg, we need to handle it first before touching the in-memory
2345        // state.
2346        if let Some(remove_above) = self.find_disk_reorg()? {
2347            self.remove_blocks(remove_above);
2348            return Ok(())
2349        }
2350
2351        let finalized = self.state.forkchoice_state_tracker.last_valid_finalized();
2352        // Trim the canonical in-memory state first: state providers build their overlays from the
2353        // canonical chain, so it must never reference blocks whose overlays the manager has
2354        // already pruned. `remove_before` does not read the canonical in-memory state, so the
2355        // order between the two trims is free to choose.
2356        self.canonical_in_memory_state.remove_persisted_blocks_until(
2357            self.persistence_state.last_persisted_block,
2358            in_memory_persisted_block.number,
2359        );
2360        self.remove_before(in_memory_persisted_block, finalized)?;
2361        // Persistence changes the overlay anchor. Prepare the remaining canonical range before
2362        // the next payload needs to read execution state against the new durable frontier.
2363        self.state.tree_state.overlay_manager.precompute_execution_overlay(
2364            self.state.tree_state.canonical_block_hash(),
2365            in_memory_persisted_block.hash,
2366        );
2367        self.state.set_pending_sparse_trie_prune(self.should_prune_sparse_trie());
2368        Ok(())
2369    }
2370
2371    /// Returns whether sparse trie pruning should be attempted by the next sparse trie task.
2372    const fn should_prune_sparse_trie(&self) -> bool {
2373        self.config.use_state_root_task()
2374    }
2375
2376    /// Return an [`ExecutedBlock`] from database or in-memory state by hash.
2377    ///
2378    /// Note: This function attempts to fetch the `ExecutedBlock` from either in-memory state
2379    /// or the database. If the required historical data (such as trie change sets) has been
2380    /// pruned for a given block, this operation will return an error. On archive nodes, it
2381    /// can retrieve any block.
2382    #[instrument(level = "debug", target = "engine::tree", skip(self))]
2383    fn canonical_block_by_hash(&self, hash: B256) -> ProviderResult<ExecutedBlock<N>> {
2384        trace!(target: "engine::tree", ?hash, "Fetching executed block by hash");
2385        // check memory first
2386        if let Some(block) = self.state.tree_state.executed_block_by_hash(hash) {
2387            return Ok(block.clone())
2388        }
2389
2390        let (block, senders) = self
2391            .provider
2392            .sealed_block_with_senders(hash.into(), TransactionVariant::WithHash)?
2393            .ok_or_else(|| ProviderError::HeaderNotFound(hash.into()))?
2394            .split_sealed();
2395        let mut execution_output = self
2396            .provider
2397            .get_state(block.header().number())?
2398            .ok_or_else(|| ProviderError::StateForNumberNotFound(block.header().number()))?;
2399        let bundle_state = execution_output.state();
2400        // `get_state` can return an in-memory execution outcome that retains destruction statuses.
2401        // Hashing it requires the parent provider to expand a pre-existing destroyed account's
2402        // storage into zero-valued slots. Genesis has no parent state, and no account existed
2403        // before it to be destroyed.
2404        let hashed_state = if block.parent_hash().is_zero() {
2405            HashedPostState::from_bundle_state::<KeccakKeyHasher>(bundle_state.state())
2406        } else {
2407            self.provider
2408                .state_by_block_hash(block.parent_hash())?
2409                .hashed_post_state(bundle_state)?
2410        };
2411
2412        debug!(
2413            target: "engine::tree",
2414            number = ?block.number(),
2415            "computing block trie updates",
2416        );
2417        let db_provider = self.provider.database_provider_ro()?;
2418        let trie_updates = self
2419            .state
2420            .tree_state
2421            .overlay_manager
2422            .compute_block_trie_updates(&db_provider, block.number())?;
2423
2424        let sorted_hashed_state = Arc::new(hashed_state.into_sorted());
2425        let sorted_trie_updates = Arc::new(trie_updates);
2426        let trie_data = SortedTrieData::new(sorted_hashed_state, sorted_trie_updates);
2427
2428        let execution_output = Arc::new(BlockExecutionOutput {
2429            state: execution_output.bundle,
2430            result: BlockExecutionResult {
2431                receipts: execution_output.receipts.pop().unwrap_or_default(),
2432                requests: execution_output.requests.pop().unwrap_or_default(),
2433                gas_used: block.gas_used(),
2434                blob_gas_used: block.blob_gas_used().unwrap_or_default(),
2435            },
2436        });
2437
2438        Ok(ExecutedBlock::new(
2439            Arc::new(RecoveredBlock::new_sealed(block, senders)),
2440            execution_output,
2441            trie_data,
2442        ))
2443    }
2444
2445    /// Returns `true` if a block with the given hash is known, either in memory or in the
2446    /// database. This is a lightweight existence check that avoids constructing a full
2447    /// [`SealedHeader`].
2448    fn has_block_by_hash(&self, hash: B256) -> ProviderResult<bool> {
2449        if self.state.tree_state.contains_hash(&hash) {
2450            Ok(true)
2451        } else {
2452            self.provider.is_known(hash)
2453        }
2454    }
2455
2456    /// Return sealed block header from in-memory state or database by hash.
2457    fn sealed_header_by_hash(
2458        &self,
2459        hash: B256,
2460    ) -> ProviderResult<Option<SealedHeader<N::BlockHeader>>> {
2461        // check memory first
2462        let header = self.state.tree_state.sealed_header_by_hash(&hash);
2463
2464        if header.is_some() {
2465            Ok(header)
2466        } else {
2467            self.provider.sealed_header_by_hash(hash)
2468        }
2469    }
2470
2471    /// Return the parent hash of the lowest buffered ancestor for the requested block, if there
2472    /// are any buffered ancestors. If there are no buffered ancestors, and the block itself does
2473    /// not exist in the buffer, this returns the hash that is passed in.
2474    ///
2475    /// Returns the parent hash of the block itself if the block is buffered and has no other
2476    /// buffered ancestors.
2477    fn lowest_buffered_ancestor_or(&self, hash: B256) -> B256 {
2478        self.state
2479            .buffer
2480            .lowest_ancestor(&hash)
2481            .map(|block| block.parent_hash())
2482            .unwrap_or_else(|| hash)
2483    }
2484
2485    /// Returns whether block downloads should also attempt to fetch the blocks' access lists.
2486    ///
2487    /// This is the case once Amsterdam is active at the current canonical head, indicated by the
2488    /// head header carrying a block access list hash.
2489    ///
2490    /// This is a coarse gate: a download issued while the head is still pre-Amsterdam won't ask
2491    /// for access lists of blocks past the fork. Access list downloads are best-effort anyway, so
2492    /// those blocks simply take the regular execution path.
2493    fn should_download_access_lists(&self) -> bool {
2494        self.canonical_in_memory_state.get_canonical_head().block_access_list_hash().is_some()
2495    }
2496
2497    /// If validation fails, the response MUST contain the latest valid hash:
2498    ///
2499    ///   - The block hash of the ancestor of the invalid payload satisfying the following two
2500    ///     conditions:
2501    ///     - It is fully validated and deemed VALID
2502    ///     - Any other ancestor of the invalid payload with a higher blockNumber is INVALID
2503    ///   - 0x0000000000000000000000000000000000000000000000000000000000000000 if the above
2504    ///     conditions are satisfied by a `PoW` block.
2505    ///   - null if client software cannot determine the ancestor of the invalid payload satisfying
2506    ///     the above conditions.
2507    fn latest_valid_hash_for_invalid_payload(
2508        &mut self,
2509        parent_hash: B256,
2510    ) -> ProviderResult<Option<B256>> {
2511        // Check if parent exists in side chain or in canonical chain.
2512        if self.has_block_by_hash(parent_hash)? {
2513            return Ok(Some(parent_hash))
2514        }
2515
2516        // iterate over ancestors in the invalid cache
2517        // until we encounter the first valid ancestor
2518        let mut current_hash = parent_hash;
2519        let mut current_block = self.state.invalid_headers.get(&current_hash);
2520        while let Some(block_with_parent) = current_block {
2521            current_hash = block_with_parent.parent;
2522            current_block = self.state.invalid_headers.get(&current_hash);
2523
2524            // If current_header is None, then the current_hash does not have an invalid
2525            // ancestor in the cache, check its presence in blockchain tree
2526            if current_block.is_none() && self.has_block_by_hash(current_hash)? {
2527                return Ok(Some(current_hash))
2528            }
2529        }
2530        Ok(None)
2531    }
2532
2533    /// Prepares the invalid payload response for the given hash, checking the
2534    /// database for the parent hash and populating the payload status with the latest valid hash
2535    /// according to the engine api spec.
2536    fn prepare_invalid_response(&mut self, parent_hash: B256) -> ProviderResult<PayloadStatus> {
2537        let valid_parent_hash = match self.sealed_header_by_hash(parent_hash)? {
2538            // Edge case: the `latestValid` field is the zero hash if the parent block is the
2539            // terminal PoW block, which we need to identify by looking at the parent's block
2540            // difficulty
2541            Some(parent) if !parent.difficulty().is_zero() => Some(B256::ZERO),
2542            Some(_) => Some(parent_hash),
2543            None => self.latest_valid_hash_for_invalid_payload(parent_hash)?,
2544        };
2545
2546        Ok(PayloadStatus::from_status(PayloadStatusEnum::Invalid {
2547            validation_error: PayloadValidationError::LinksToRejectedPayload.to_string(),
2548        })
2549        .with_latest_valid_hash(valid_parent_hash.unwrap_or_default()))
2550    }
2551
2552    /// Returns true if the given hash is the last received sync target block.
2553    ///
2554    /// See [`ForkchoiceStateTracker::sync_target_state`]
2555    fn is_sync_target_head(&self, block_hash: B256) -> bool {
2556        if let Some(target) = self.state.forkchoice_state_tracker.sync_target_state() {
2557            return target.head_block_hash == block_hash
2558        }
2559        false
2560    }
2561
2562    /// Returns true if the given hash is part of the last received sync target fork choice update.
2563    ///
2564    /// See [`ForkchoiceStateTracker::sync_target_state`]
2565    fn is_any_sync_target(&self, block_hash: B256) -> bool {
2566        if let Some(target) = self.state.forkchoice_state_tracker.sync_target_state() {
2567            return target.contains(block_hash)
2568        }
2569        false
2570    }
2571
2572    /// Checks if the given `check` hash points to an invalid header, inserting the given `head`
2573    /// block into the invalid header cache if the `check` hash has a known invalid ancestor.
2574    ///
2575    /// Returns a payload status response according to the engine API spec if the block is known to
2576    /// be invalid.
2577    fn check_invalid_ancestor_with_head(
2578        &mut self,
2579        check: B256,
2580        head: &SealedBlock<N::Block>,
2581    ) -> ProviderResult<Option<PayloadStatus>> {
2582        // check if the check hash was previously marked as invalid
2583        let Some(header) = self.state.invalid_headers.get(&check) else { return Ok(None) };
2584
2585        Ok(Some(self.on_invalid_new_payload(head.clone(), header)?))
2586    }
2587
2588    /// Invoked when a new payload received is invalid.
2589    fn on_invalid_new_payload(
2590        &mut self,
2591        head: SealedBlock<N::Block>,
2592        invalid: BlockWithParent,
2593    ) -> ProviderResult<PayloadStatus> {
2594        // populate the latest valid hash field
2595        let status = self.prepare_invalid_response(invalid.parent)?;
2596
2597        // insert the head block into the invalid header cache
2598        self.state.invalid_headers.insert_with_invalid_ancestor(head.hash(), invalid);
2599        self.emit_event(ConsensusEngineEvent::InvalidBlock {
2600            block: Box::new(head),
2601            error: PayloadValidationError::LinksToRejectedPayload.to_string(),
2602        });
2603
2604        Ok(status)
2605    }
2606
2607    /// Finds any invalid ancestor for the given payload.
2608    ///
2609    /// This function first checks if the block itself is in the invalid headers cache (to
2610    /// avoid re-executing a known-invalid block). Then it walks up the chain of buffered
2611    /// ancestors and checks if any ancestor is marked as invalid.
2612    ///
2613    /// The check works by:
2614    /// 1. Checking if the block hash itself is in the `invalid_headers` map
2615    /// 2. Finding the lowest buffered ancestor for the given block hash
2616    /// 3. If the ancestor is the same as the block hash itself, using the parent hash instead
2617    /// 4. Checking if this ancestor is in the `invalid_headers` map
2618    ///
2619    /// Returns the invalid ancestor block info if found, or None if no invalid ancestor exists.
2620    fn find_invalid_ancestor(&mut self, payload: &T::ExecutionData) -> Option<BlockWithParent> {
2621        let parent_hash = payload.parent_hash();
2622        let block_hash = payload.block_hash();
2623
2624        // Check if the block itself is already known to be invalid, avoiding re-execution
2625        if let Some(entry) = self.state.invalid_headers.get(&block_hash) {
2626            return Some(entry);
2627        }
2628
2629        let mut lowest_buffered_ancestor = self.lowest_buffered_ancestor_or(block_hash);
2630        if lowest_buffered_ancestor == block_hash {
2631            lowest_buffered_ancestor = parent_hash;
2632        }
2633
2634        // Check if the block has an invalid ancestor
2635        self.state.invalid_headers.get(&lowest_buffered_ancestor)
2636    }
2637
2638    /// Handles a payload that has an invalid ancestor.
2639    ///
2640    /// This function validates the payload and processes it according to whether it's
2641    /// well-formed or malformed:
2642    /// 1. **Well-formed payload**: The payload is marked as invalid since it descends from a
2643    ///    known-bad block, which violates consensus rules
2644    /// 2. **Malformed payload**: Returns an appropriate error status since the payload cannot be
2645    ///    validated due to its own structural issues
2646    fn handle_invalid_ancestor_payload(
2647        &mut self,
2648        payload: T::ExecutionData,
2649        invalid: BlockWithParent,
2650    ) -> Result<PayloadStatus, InsertBlockFatalError> {
2651        let parent_hash = payload.parent_hash();
2652        let num_hash = payload.num_hash();
2653
2654        // Here we might have 2 cases
2655        // 1. the block is well formed and indeed links to an invalid header, meaning we should
2656        //    remember it as invalid
2657        // 2. the block is not well formed (i.e block hash is incorrect), and we should just return
2658        //    an error and forget it
2659        let block = match self.payload_validator.convert_payload_to_block(payload) {
2660            Ok(block) => block,
2661            Err(error) => return Ok(self.on_new_payload_error(error, num_hash, parent_hash)?),
2662        };
2663
2664        Ok(self.on_invalid_new_payload(block, invalid)?)
2665    }
2666
2667    /// Checks if the given `head` points to an invalid header, which requires a specific response
2668    /// to a forkchoice update.
2669    fn check_invalid_ancestor(&mut self, head: B256) -> ProviderResult<Option<PayloadStatus>> {
2670        // check if the head was previously marked as invalid
2671        let Some(header) = self.state.invalid_headers.get(&head) else { return Ok(None) };
2672
2673        // Try to prepare invalid response, but handle errors gracefully
2674        match self.prepare_invalid_response(header.parent) {
2675            Ok(status) => Ok(Some(status)),
2676            Err(err) => {
2677                debug!(target: "engine::tree", %err, "Failed to prepare invalid response for ancestor check");
2678                // Return a basic invalid status without latest valid hash
2679                Ok(Some(PayloadStatus::from_status(PayloadStatusEnum::Invalid {
2680                    validation_error: PayloadValidationError::LinksToRejectedPayload.to_string(),
2681                })))
2682            }
2683        }
2684    }
2685
2686    /// Validate if block is correct and satisfies all the consensus rules that concern the header
2687    /// and block body itself.
2688    fn validate_block(&self, block: &SealedBlock<N::Block>) -> Result<(), ConsensusError> {
2689        if let Err(e) = self.consensus.validate_header(block.sealed_header()) {
2690            error!(target: "engine::tree", block = ?block.block_with_parent(), %e, "Failed to validate header");
2691            return Err(e)
2692        }
2693
2694        if let Err(e) = self.consensus.validate_block_pre_execution(block) {
2695            error!(target: "engine::tree", block = ?block.block_with_parent(), %e, "Failed to validate block");
2696            return Err(e)
2697        }
2698
2699        Ok(())
2700    }
2701
2702    /// Attempts to connect any buffered blocks that are connected to the given parent hash.
2703    #[instrument(level = "debug", target = "engine::tree", skip(self))]
2704    fn try_connect_buffered_blocks(
2705        &mut self,
2706        parent: BlockNumHash,
2707    ) -> Result<(), InsertBlockFatalError> {
2708        let blocks = self.state.buffer.remove_block_with_children(&parent.hash);
2709
2710        if blocks.is_empty() {
2711            // nothing to append
2712            return Ok(())
2713        }
2714
2715        let now = Instant::now();
2716        let block_count = blocks.len();
2717        for child in blocks {
2718            let child_num_hash = child.num_hash();
2719            match self.insert_block(child) {
2720                Ok(res) => {
2721                    debug!(target: "engine::tree", child =?child_num_hash, ?res, "connected buffered block");
2722                    if self.is_any_sync_target(child_num_hash.hash) &&
2723                        matches!(res, InsertPayloadOk::Inserted(BlockStatus::Valid))
2724                    {
2725                        debug!(target: "engine::tree", child =?child_num_hash, "connected sync target block");
2726                        // we just inserted a block that we know is part of the canonical chain, so
2727                        // we can make it canonical
2728                        self.make_canonical(child_num_hash.hash)?;
2729                    }
2730                }
2731                Err(err) => {
2732                    if let InsertPayloadError::Block(err) = err {
2733                        debug!(target: "engine::tree", ?err, "failed to connect buffered block to tree");
2734                        if let Err(InsertBlockProcessingError::Fatal(fatal)) =
2735                            self.on_insert_block_error(err)
2736                        {
2737                            warn!(target: "engine::tree", %fatal, "fatal error occurred while connecting buffered blocks");
2738                        }
2739                    }
2740                }
2741            }
2742        }
2743
2744        debug!(target: "engine::tree", elapsed = ?now.elapsed(), %block_count, "connected buffered blocks");
2745        Ok(())
2746    }
2747
2748    /// Pre-validates the block and inserts it into the buffer.
2749    fn buffer_block(
2750        &mut self,
2751        block: SealedBlock<N::Block>,
2752    ) -> Result<(), InsertBlockError<N::Block>> {
2753        if let Err(err) = self.validate_block(&block) {
2754            return Err(InsertBlockError::consensus_error(err, block))
2755        }
2756        self.state.buffer.insert_block(block.into());
2757        Ok(())
2758    }
2759
2760    /// Returns true if the distance from the local tip to the block is greater than the configured
2761    /// threshold.
2762    ///
2763    /// If the `local_tip` is greater than the `block`, then this will return false.
2764    #[inline]
2765    const fn exceeds_backfill_run_threshold(&self, local_tip: u64, block: u64) -> bool {
2766        block > local_tip && block - local_tip > self.config.backfill_run_threshold()
2767    }
2768
2769    /// Returns how far the local tip is from the given block. If the local tip is at the same
2770    /// height or its block number is greater than the given block, this returns None.
2771    #[inline]
2772    const fn distance_from_local_tip(&self, local_tip: u64, block: u64) -> Option<u64> {
2773        if block > local_tip {
2774            Some(block - local_tip)
2775        } else {
2776            None
2777        }
2778    }
2779
2780    /// Returns the block hash that backfill should target.
2781    ///
2782    /// Defaults to the finalized block hash. On OP Stack, the CL finalizes in large batches and the
2783    /// finalized hash can lag the canonical tip by a wide margin, so backfill targets the head.
2784    ///
2785    /// The zero-finalized optimistic-sync fallback for non-OP Stack chains is handled by
2786    /// [`Self::backfill_sync_target`].
2787    const fn backfill_target_hash(&self, state: ForkchoiceState) -> B256 {
2788        if self.engine_kind.is_opstack() {
2789            state.head_block_hash
2790        } else {
2791            state.finalized_block_hash
2792        }
2793    }
2794
2795    /// Returns the target hash to sync to if the distance from the local tip is greater than the
2796    /// threshold and we're not yet synced to the backfill target (see
2797    /// [`Self::backfill_target_hash`]).
2798    ///
2799    /// If this is invoked after a new block has been downloaded, the downloaded block could be
2800    /// the (missing) target block.
2801    fn backfill_sync_target(
2802        &self,
2803        canonical_tip_num: u64,
2804        target_block_number: u64,
2805        downloaded_block: Option<BlockNumHash>,
2806    ) -> Option<B256> {
2807        let state = self.state.forkchoice_state_tracker.sync_target_state()?;
2808        let target_hash = self.backfill_target_hash(state);
2809
2810        // check if the downloaded block is the tracked backfill target
2811        let exceeds_backfill_threshold = match downloaded_block.as_ref() {
2812            // if we downloaded the target block we can now check how far we're off
2813            Some(downloaded_block) if downloaded_block.hash == target_hash => {
2814                self.exceeds_backfill_run_threshold(canonical_tip_num, downloaded_block.number)
2815            }
2816            _ => match self.state.buffer.block(&target_hash) {
2817                // if we have buffered the target block, we should check how far we're off
2818                Some(buffered_target) => {
2819                    self.exceeds_backfill_run_threshold(canonical_tip_num, buffered_target.number())
2820                }
2821                // check if the distance exceeds the threshold for backfill sync
2822                None => self.exceeds_backfill_run_threshold(canonical_tip_num, target_block_number),
2823            },
2824        };
2825
2826        if !exceeds_backfill_threshold {
2827            return None
2828        }
2829
2830        // if we have already canonicalized the target block, we should skip backfill
2831        match self.provider.header_by_hash_or_number(target_hash.into()) {
2832            Err(err) => {
2833                warn!(target: "engine::tree", %err, "Failed to get backfill target block header");
2834                None
2835            }
2836            // we don't have the block yet and the distance exceeds the allowed threshold
2837            Ok(None) if !target_hash.is_zero() => Some(target_hash),
2838            Ok(None) => {
2839                // OPTIMISTIC SYNCING
2840                //
2841                // It can happen when the node is doing an
2842                // optimistic sync, where the CL has no knowledge of the finalized hash,
2843                // but is expecting the EL to sync as high
2844                // as possible before finalizing.
2845                //
2846                // This usually doesn't happen on ETH mainnet since CLs use the more
2847                // secure checkpoint syncing.
2848                //
2849                // However, optimism chains will do this. The risk of a reorg is however
2850                // low.
2851                debug!(target: "engine::tree", hash=?state.head_block_hash, "Setting head hash as an optimistic backfill target.");
2852                Some(state.head_block_hash)
2853            }
2854            // we're fully synced to the target block
2855            Ok(Some(_)) => None,
2856        }
2857    }
2858
2859    /// This method tries to detect whether on-disk and in-memory states have diverged. It might
2860    /// happen if a reorg is happening while we are persisting a block.
2861    fn find_disk_reorg(&self) -> ProviderResult<Option<u64>> {
2862        let mut canonical = self.state.tree_state.current_canonical_head;
2863        let mut persisted = self.persistence_state.last_persisted_block;
2864
2865        let parent_num_hash = |num_hash: NumHash| -> ProviderResult<NumHash> {
2866            Ok(self
2867                .sealed_header_by_hash(num_hash.hash)?
2868                .ok_or(ProviderError::BlockHashNotFound(num_hash.hash))?
2869                .parent_num_hash())
2870        };
2871
2872        // Happy path, canonical chain is ahead or equal to persisted chain.
2873        // Walk canonical chain back to make sure that it connects to persisted chain.
2874        while canonical.number > persisted.number {
2875            canonical = parent_num_hash(canonical)?;
2876        }
2877
2878        // If we've reached persisted tip by walking the canonical chain back, everything is fine.
2879        if canonical == persisted {
2880            return Ok(None);
2881        }
2882
2883        // At this point, we know that `persisted` block can't be reached by walking the canonical
2884        // chain back. In this case we need to truncate it to the first canonical block it connects
2885        // to.
2886
2887        // Firstly, walk back until we reach the same height as `canonical`.
2888        while persisted.number > canonical.number {
2889            persisted = parent_num_hash(persisted)?;
2890        }
2891
2892        debug_assert_eq!(persisted.number, canonical.number);
2893
2894        // Now walk both chains back until we find a common ancestor.
2895        while persisted.hash != canonical.hash {
2896            canonical = parent_num_hash(canonical)?;
2897            persisted = parent_num_hash(persisted)?;
2898        }
2899
2900        debug!(target: "engine::tree", remove_above=persisted.number, "on-disk reorg detected");
2901
2902        Ok(Some(persisted.number))
2903    }
2904
2905    /// Invoked when we the canonical chain has been updated.
2906    ///
2907    /// This is invoked on a valid forkchoice update, or if we can make the target block canonical.
2908    fn on_canonical_chain_update(&mut self, chain_update: NewCanonicalChain<N>) {
2909        trace!(target: "engine::tree", new_blocks = %chain_update.new_block_count(), reorged_blocks =  %chain_update.reorged_block_count(), "applying new chain update");
2910        let start = Instant::now();
2911
2912        // update the tracked canonical head
2913        self.state.tree_state.set_canonical_head(chain_update.tip().num_hash());
2914
2915        let tip = chain_update.tip().clone_sealed_header();
2916        let notification = chain_update.to_chain_notification();
2917
2918        // reinsert any missing reorged blocks
2919        if let NewCanonicalChain::Reorg { new, old } = &chain_update {
2920            let new_first = new.first().map(|first| first.recovered_block().num_hash());
2921            let old_first = old.first().map(|first| first.recovered_block().num_hash());
2922            trace!(target: "engine::tree", ?new_first, ?old_first, "Reorg detected, new and old first blocks");
2923
2924            self.state.set_pending_sparse_trie_prune(false);
2925            self.update_reorg_metrics(old.len(), old_first);
2926            self.reinsert_reorged_blocks(new.clone());
2927            self.reinsert_reorged_blocks(old.clone());
2928        }
2929
2930        // update the tracked in-memory state with the new chain
2931        self.canonical_in_memory_state.update_chain(chain_update);
2932        self.canonical_in_memory_state.set_canonical_head(tip.clone());
2933        self.payload_validator.on_canonical_head_changed(tip.hash(), &self.state);
2934
2935        // Update metrics based on new tip
2936        self.metrics.tree.canonical_chain_height.set(tip.number() as f64);
2937
2938        // sends an event to all active listeners about the new canonical chain
2939        self.canonical_in_memory_state.notify_canon_state(notification);
2940
2941        // emit event
2942        self.emit_event(ConsensusEngineEvent::CanonicalChainCommitted(
2943            Box::new(tip),
2944            start.elapsed(),
2945        ));
2946    }
2947
2948    /// This updates metrics based on the given reorg length and first reorged block number.
2949    fn update_reorg_metrics(&self, old_chain_length: usize, first_reorged_block: Option<NumHash>) {
2950        if let Some(first_reorged_block) = first_reorged_block.map(|block| block.number) {
2951            if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() &&
2952                first_reorged_block <= finalized.number
2953            {
2954                self.metrics.tree.reorgs.finalized.increment(1);
2955            } else if let Some(safe) = self.canonical_in_memory_state.get_safe_num_hash() &&
2956                first_reorged_block <= safe.number
2957            {
2958                self.metrics.tree.reorgs.safe.increment(1);
2959            } else {
2960                self.metrics.tree.reorgs.head.increment(1);
2961            }
2962        } else {
2963            debug_unreachable!("Reorged chain doesn't have any blocks");
2964        }
2965        self.metrics.tree.latest_reorg_depth.set(old_chain_length as f64);
2966    }
2967
2968    /// This reinserts any blocks in the new chain that do not already exist in the tree
2969    fn reinsert_reorged_blocks(&mut self, new_chain: Vec<ExecutedBlock<N>>) {
2970        for block in new_chain {
2971            if self
2972                .state
2973                .tree_state
2974                .executed_block_by_hash(block.recovered_block().hash())
2975                .is_none()
2976            {
2977                trace!(target: "engine::tree", num=?block.recovered_block().number(), hash=?block.recovered_block().hash(), "Reinserting block into tree state");
2978                self.state.tree_state.insert_executed(block);
2979            }
2980        }
2981        self.metrics.engine.executed_blocks.set(self.state.tree_state.block_count() as f64);
2982    }
2983
2984    /// This handles downloaded blocks that are shown to be disconnected from the canonical chain.
2985    ///
2986    /// This mainly compares the missing parent of the downloaded block with the current canonical
2987    /// tip, and decides whether or not backfill sync should be triggered.
2988    fn on_disconnected_downloaded_block(
2989        &self,
2990        downloaded_block: BlockNumHash,
2991        missing_parent: BlockNumHash,
2992        head: BlockNumHash,
2993    ) -> Option<TreeEvent> {
2994        // compare the missing parent with the canonical tip
2995        if let Some(target) =
2996            self.backfill_sync_target(head.number, missing_parent.number, Some(downloaded_block))
2997        {
2998            trace!(target: "engine::tree", %target, "triggering backfill on downloaded block");
2999            return Some(TreeEvent::BackfillAction(BackfillAction::Start(target.into())));
3000        }
3001
3002        // continue downloading the missing parent
3003        //
3004        // this happens if either:
3005        //  * the missing parent block num < canonical tip num
3006        //    * this case represents a missing block on a fork that is shorter than the canonical
3007        //      chain
3008        //  * the missing parent block num >= canonical tip num, but the number of missing blocks is
3009        //    less than the backfill threshold
3010        //    * this case represents a potentially long range of blocks to download and execute
3011        let request = if let Some(distance) =
3012            self.distance_from_local_tip(head.number, missing_parent.number)
3013        {
3014            trace!(target: "engine::tree", %distance, missing=?missing_parent, "downloading missing parent block range");
3015            DownloadRequest::block_range(missing_parent.hash, distance)
3016        } else {
3017            trace!(target: "engine::tree", missing=?missing_parent, "downloading missing parent block");
3018            // This happens when the missing parent is on an outdated
3019            // sidechain and we can only download the missing block itself
3020            DownloadRequest::single_block(missing_parent.hash)
3021        };
3022
3023        Some(TreeEvent::Download(request.with_access_lists(self.should_download_access_lists())))
3024    }
3025
3026    /// Handles a downloaded block that was successfully inserted as valid.
3027    ///
3028    /// If the block matches the sync target head, returns [`TreeAction::MakeCanonical`].
3029    /// If it matches a non-head sync target (safe or finalized), makes it canonical inline
3030    /// and triggers a download for the remaining blocks towards the actual head.
3031    /// Otherwise, tries to connect buffered blocks.
3032    fn on_valid_downloaded_block(
3033        &mut self,
3034        block_num_hash: BlockNumHash,
3035    ) -> Result<Option<TreeEvent>, InsertBlockFatalError> {
3036        // check if we just inserted a block that's part of sync targets,
3037        // i.e. head, safe, or finalized
3038        if let Some(sync_target) = self.state.forkchoice_state_tracker.sync_target_state() &&
3039            sync_target.contains(block_num_hash.hash)
3040        {
3041            debug!(target: "engine::tree", ?sync_target, "appended downloaded sync target block");
3042
3043            if sync_target.head_block_hash == block_num_hash.hash {
3044                // we just inserted the sync target head block, make it canonical
3045                return Ok(Some(TreeEvent::TreeAction(TreeAction::MakeCanonical {
3046                    sync_target_head: block_num_hash.hash,
3047                })))
3048            }
3049
3050            // This block is part of the sync target (safe or finalized) but not the
3051            // head. Make it canonical and try to connect any buffered children, then
3052            // continue downloading towards the actual head if needed.
3053            self.make_canonical(block_num_hash.hash)?;
3054            self.try_connect_buffered_blocks(block_num_hash)?;
3055
3056            // Check if we've reached the sync target head after connecting buffered
3057            // blocks (e.g. the head block may have already been buffered).
3058            if self.state.tree_state.canonical_block_hash() != sync_target.head_block_hash {
3059                let target = self.lowest_buffered_ancestor_or(sync_target.head_block_hash);
3060                trace!(target: "engine::tree", %target, "sync target head not yet reached, downloading head block");
3061                return Ok(Some(TreeEvent::Download(
3062                    DownloadRequest::single_block(target)
3063                        .with_access_lists(self.should_download_access_lists()),
3064                )))
3065            }
3066
3067            return Ok(None)
3068        }
3069        trace!(target: "engine::tree", "appended downloaded block");
3070        self.try_connect_buffered_blocks(block_num_hash)?;
3071        Ok(None)
3072    }
3073
3074    /// Invoked with a block downloaded from the network
3075    ///
3076    /// Returns an event with the appropriate action to take, such as:
3077    ///  - download more missing blocks
3078    ///  - try to canonicalize the target if the `block` is the tracked target (head) block.
3079    #[instrument(level = "debug", target = "engine::tree", skip_all, fields(block_hash = %block.hash(), block_num = %block.number()))]
3080    fn on_downloaded_block(
3081        &mut self,
3082        block: SealedBlockWithAccessList<N::Block>,
3083    ) -> Result<Option<TreeEvent>, InsertBlockFatalError> {
3084        let block_num_hash = block.num_hash();
3085        let lowest_buffered_ancestor = self.lowest_buffered_ancestor_or(block_num_hash.hash);
3086        if self.check_invalid_ancestor_with_head(lowest_buffered_ancestor, &block)?.is_some() {
3087            return Ok(None)
3088        }
3089
3090        if !self.backfill_sync_state.is_idle() {
3091            return Ok(None)
3092        }
3093
3094        // try to append the block
3095        match self.insert_block(block) {
3096            Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) => {
3097                return self.on_valid_downloaded_block(block_num_hash);
3098            }
3099            Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected { head, missing_ancestor })) => {
3100                // block is not connected to the canonical head, we need to download
3101                // its missing branch first
3102                return Ok(self.on_disconnected_downloaded_block(
3103                    block_num_hash,
3104                    missing_ancestor,
3105                    head,
3106                ))
3107            }
3108            Ok(InsertPayloadOk::AlreadySeen(_)) => {
3109                trace!(target: "engine::tree", "downloaded block already executed");
3110            }
3111            Err(err) => {
3112                if let InsertPayloadError::Block(err) = err {
3113                    debug!(target: "engine::tree", err=%err.kind(), "failed to insert downloaded block");
3114                    if let Err(InsertBlockProcessingError::Fatal(fatal)) =
3115                        self.on_insert_block_error(err)
3116                    {
3117                        warn!(target: "engine::tree", %fatal, "fatal error occurred while inserting downloaded block");
3118                    }
3119                }
3120            }
3121        }
3122        Ok(None)
3123    }
3124
3125    /// Inserts a payload into the tree and executes it.
3126    ///
3127    /// This function validates the payload's basic structure, then executes it using the
3128    /// payload validator. The execution includes running all transactions in the payload
3129    /// and validating the resulting state transitions.
3130    ///
3131    /// Returns `InsertPayloadOk` if the payload was successfully inserted and executed,
3132    /// or `InsertPayloadError` if validation or execution failed.
3133    fn insert_payload(
3134        &mut self,
3135        payload: T::ExecutionData,
3136    ) -> Result<InsertPayloadOk, InsertPayloadError<N::Block>> {
3137        self.insert_block_or_payload(
3138            payload.block_with_parent(),
3139            payload,
3140            |validator, payload, ctx| validator.validate_payload(payload, ctx),
3141            |this, payload| Ok(this.payload_validator.convert_payload_to_block(payload)?.into()),
3142        )
3143    }
3144
3145    fn insert_block(
3146        &mut self,
3147        block: SealedBlockWithAccessList<N::Block>,
3148    ) -> Result<InsertPayloadOk, InsertPayloadError<N::Block>> {
3149        self.insert_block_or_payload(
3150            block.block_with_parent(),
3151            block,
3152            |validator, block, ctx| validator.validate_block(block, ctx),
3153            |_, block| Ok(block),
3154        )
3155    }
3156
3157    /// Inserts a block or payload into the blockchain tree with full execution.
3158    ///
3159    /// This is a generic function that handles both blocks and payloads by accepting
3160    /// a block identifier, input data, and execution/validation functions. It performs
3161    /// comprehensive checks and execution:
3162    ///
3163    /// - Validates that the block doesn't already exist in the tree
3164    /// - Ensures parent state is available, buffering if necessary
3165    /// - Executes the block/payload using the provided execute function
3166    /// - Handles both canonical and fork chain insertions
3167    /// - Updates pending block state when appropriate
3168    /// - Emits consensus engine events and records metrics
3169    ///
3170    /// Returns `InsertPayloadOk::Inserted(BlockStatus::Valid)` on successful execution,
3171    /// `InsertPayloadOk::AlreadySeen` if the block already exists, or
3172    /// `InsertPayloadOk::Inserted(BlockStatus::Disconnected)` if parent state is missing.
3173    #[instrument(level = "debug", target = "engine::tree", skip_all, fields(?block_id))]
3174    fn insert_block_or_payload<Input, Err>(
3175        &mut self,
3176        block_id: BlockWithParent,
3177        input: Input,
3178        execute: impl FnOnce(&mut V, Input, TreeCtx<'_, N>) -> Result<ValidationOutput<N>, Err>,
3179        convert_to_block: impl FnOnce(
3180            &mut Self,
3181            Input,
3182        ) -> Result<SealedBlockWithAccessList<N::Block>, Err>,
3183    ) -> Result<InsertPayloadOk, Err>
3184    where
3185        Err: From<InsertBlockError<N::Block>>,
3186    {
3187        let block_insert_start = Instant::now();
3188        let block_num_hash = block_id.block;
3189        debug!(target: "engine::tree", block=?block_num_hash, parent = ?block_id.parent, "Inserting new block into tree");
3190
3191        // Check if block already exists - first in memory, then DB only if it could be persisted
3192        if self.state.tree_state.contains_hash(&block_num_hash.hash) {
3193            convert_to_block(self, input)?;
3194            return Ok(InsertPayloadOk::AlreadySeen(BlockStatus::Valid));
3195        }
3196
3197        // Only query DB if block could be persisted (number <= last persisted block).
3198        // New blocks from CL always have number > last persisted, so skip DB lookup for them.
3199        if block_num_hash.number <= self.persistence_state.last_persisted_block.number {
3200            match self.provider.sealed_header_by_hash(block_num_hash.hash) {
3201                Err(err) => {
3202                    let block = convert_to_block(self, input)?;
3203                    return Err(InsertBlockError::new(block.split().0, err.into()).into());
3204                }
3205                Ok(Some(_)) => {
3206                    convert_to_block(self, input)?;
3207                    return Ok(InsertPayloadOk::AlreadySeen(BlockStatus::Valid));
3208                }
3209                Ok(None) => {}
3210            }
3211        }
3212
3213        // Ensure that the parent state is available.
3214        if !self.state.tree_state.contains_hash(&block_id.parent) {
3215            let parent_exists = match self.provider.header(block_id.parent) {
3216                Ok(header) => header.is_some(),
3217                Err(err) => {
3218                    let block = convert_to_block(self, input)?;
3219                    return Err(InsertBlockError::new(block.split().0, err.into()).into());
3220                }
3221            };
3222
3223            if !parent_exists {
3224                let block = convert_to_block(self, input)?;
3225                // we don't have the state required to execute this block, buffering it and find the
3226                // missing parent block
3227                let missing_ancestor = self
3228                    .state
3229                    .buffer
3230                    .lowest_ancestor(&block.parent_hash())
3231                    .map(|block| block.parent_num_hash())
3232                    .unwrap_or_else(|| block.parent_num_hash());
3233
3234                self.state.buffer.insert_block(block);
3235
3236                return Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected {
3237                    head: self.state.tree_state.current_canonical_head,
3238                    missing_ancestor,
3239                }))
3240            }
3241        }
3242
3243        // determine whether we are on a fork chain by comparing the block number with the
3244        // canonical head. This is a simple check that is sufficient for the event emission below.
3245        // A block is considered a fork if its number is less than or equal to the canonical head,
3246        // as this indicates there's already a canonical block at that height.
3247        let is_fork = block_id.block.number <= self.state.tree_state.current_canonical_head.number;
3248
3249        let ctx = TreeCtx::new(&mut self.state, &self.canonical_in_memory_state);
3250
3251        let start = Instant::now();
3252
3253        let ValidationOutput { executed_block: executed, execution_timing_stats: timing_stats } =
3254            execute(&mut self.payload_validator, input, ctx)?;
3255
3256        if let Some(raw_bal) = executed.bal().map(|bal| bal.as_raw_bal().clone()) {
3257            let num_hash = executed.recovered_block().num_hash();
3258            if let Err(err) = self.provider.bal_store().insert(num_hash, raw_bal) {
3259                warn!(
3260                    target: "engine::tree",
3261                    ?num_hash,
3262                    %err,
3263                    "Failed to store validated block access list"
3264                );
3265            }
3266        }
3267
3268        // Emit slow block event immediately after execution so it appears even when
3269        // persistence hasn't completed yet (e.g. blocks arriving faster than persistence).
3270        if let Some(stats) = timing_stats {
3271            if let Some(threshold) = self.config.slow_block_threshold() {
3272                let total_duration = stats.execution_duration + stats.state_hash_duration;
3273                if total_duration > threshold {
3274                    self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
3275                        stats: stats.clone(),
3276                        commit_duration: None,
3277                        total_duration,
3278                    }));
3279                }
3280            }
3281            self.execution_timing_stats.insert(executed.recovered_block().hash(), stats);
3282        }
3283
3284        let is_pending = self.state.tree_state.canonical_block_hash() ==
3285            executed.recovered_block().parent_hash();
3286        self.state.tree_state.insert_executed(executed.clone());
3287
3288        if is_pending {
3289            debug!(target: "engine::tree", pending=?block_num_hash, "updating pending block");
3290            self.canonical_in_memory_state.set_pending_block(executed.clone());
3291        }
3292
3293        self.metrics.engine.executed_blocks.set(self.state.tree_state.block_count() as f64);
3294
3295        // emit insert event
3296        let elapsed = start.elapsed();
3297        let engine_event = if is_fork {
3298            ConsensusEngineEvent::ForkBlockAdded(executed, elapsed)
3299        } else {
3300            ConsensusEngineEvent::CanonicalBlockAdded(executed, elapsed)
3301        };
3302        self.emit_event(EngineApiEvent::BeaconConsensus(engine_event));
3303
3304        self.metrics
3305            .engine
3306            .block_insert_total_duration
3307            .record(block_insert_start.elapsed().as_secs_f64());
3308        debug!(target: "engine::tree", block=?block_num_hash, "Finished inserting block");
3309        Ok(InsertPayloadOk::Inserted(BlockStatus::Valid))
3310    }
3311
3312    /// Handles an error that occurred while inserting a block.
3313    ///
3314    /// If this is a validation error this will mark the block as invalid.
3315    ///
3316    /// Returns the proper payload status response if the block is invalid.
3317    fn on_insert_block_error(
3318        &mut self,
3319        error: InsertBlockError<N::Block>,
3320    ) -> Result<PayloadStatus, InsertBlockProcessingError> {
3321        let (block, error) = error.split();
3322
3323        let validation_err = error.ensure_validation_error()?;
3324
3325        // If the error was due to an invalid payload, the payload is added to the
3326        // invalid headers cache and `Ok` with [PayloadStatusEnum::Invalid] is
3327        // returned.
3328        warn!(
3329            target: "engine::tree",
3330            invalid_hash=%block.hash(),
3331            invalid_number=block.number(),
3332            %validation_err,
3333            "Invalid block error on new payload",
3334        );
3335        // The Amsterdam Engine API requires `latestValidHash: null` for an undecodable BAL.
3336        // <https://github.com/ethereum/execution-apis/blob/df75e230befef0de56ee8833322ed714bacb479c/src/engine/amsterdam.md?plain=1#L129>
3337        let latest_valid_hash =
3338            if matches!(&validation_err, InsertBlockValidationError::BlockAccessListDecode(_)) {
3339                None
3340            } else {
3341                self.latest_valid_hash_for_invalid_payload(block.parent_hash())
3342                    .map_err(InsertBlockFatalError::from)?
3343            };
3344
3345        // keep track of the invalid header unless the consensus impl considers it transient
3346        let is_transient = match &validation_err {
3347            InsertBlockValidationError::Consensus(err) => self.consensus.is_transient_error(err),
3348            _ => false,
3349        };
3350        if is_transient {
3351            warn!(
3352                target: "engine::tree",
3353                invalid_hash=%block.hash(),
3354                invalid_number=block.number(),
3355                %validation_err,
3356                "Skipping invalid header cache insert for transient validation error",
3357            );
3358        } else {
3359            self.state.invalid_headers.insert(block.block_with_parent());
3360        }
3361        self.emit_event(EngineApiEvent::BeaconConsensus(ConsensusEngineEvent::InvalidBlock {
3362            block: Box::new(block),
3363            error: validation_err.to_string(),
3364        }));
3365
3366        Ok(PayloadStatus::new(
3367            PayloadStatusEnum::Invalid { validation_error: validation_err.to_string() },
3368            latest_valid_hash,
3369        ))
3370    }
3371
3372    /// Handles a [`NewPayloadError`] by converting it to a [`PayloadStatus`].
3373    fn on_new_payload_error(
3374        &mut self,
3375        error: NewPayloadError,
3376        payload_num_hash: NumHash,
3377        parent_hash: B256,
3378    ) -> ProviderResult<PayloadStatus> {
3379        error!(target: "engine::tree", payload=?payload_num_hash, %error, "Invalid payload");
3380        // we need to convert the error to a payload status (response to the CL)
3381
3382        let latest_valid_hash =
3383            if error.is_block_hash_mismatch() || error.is_invalid_versioned_hashes() {
3384                // Engine-API rules:
3385                // > `latestValidHash: null` if the blockHash validation has failed (<https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/shanghai.md?plain=1#L113>)
3386                // > `latestValidHash: null` if the expected and the actual arrays don't match (<https://github.com/ethereum/execution-apis/blob/fe8e13c288c592ec154ce25c534e26cb7ce0530d/src/engine/cancun.md?plain=1#L103>)
3387                None
3388            } else {
3389                self.latest_valid_hash_for_invalid_payload(parent_hash)?
3390            };
3391
3392        let status = PayloadStatusEnum::from(error);
3393        Ok(PayloadStatus::new(status, latest_valid_hash))
3394    }
3395
3396    /// Attempts to find the header for the given block hash if it is canonical.
3397    ///
3398    /// A persisted header can still be found by hash while its disk reorg is pending, so it is
3399    /// only canonical if it is not above the canonical head and matches the canonical hash at its
3400    /// height. The in-memory canonical hash is preferred over the persisted one.
3401    pub fn find_canonical_header(
3402        &self,
3403        hash: B256,
3404    ) -> Result<Option<SealedHeader<N::BlockHeader>>, ProviderError> {
3405        if let Some(header) = self.canonical_in_memory_state.header_by_hash(hash) {
3406            return Ok(Some(header))
3407        }
3408
3409        let Some(header) = self.provider.sealed_header_by_hash(hash)? else { return Ok(None) };
3410        // Reorged-out headers remain on disk until persistence catches up, so reject old tips above
3411        // the in-memory head and use in-memory hashes to reject old blocks at shared heights.
3412        let number = header.number();
3413        if number > self.canonical_in_memory_state.get_canonical_block_number() {
3414            return Ok(None)
3415        }
3416        let canonical_hash =
3417            if let Some(hash) = self.canonical_in_memory_state.hash_by_number(number) {
3418                Some(hash)
3419            } else {
3420                self.provider.block_hash(number)?
3421            };
3422
3423        Ok((canonical_hash == Some(hash)).then_some(header))
3424    }
3425
3426    /// Checks that nonzero safe and finalized hashes belong to the chain defined by the FCU head.
3427    ///
3428    /// The [Engine API forkchoiceUpdated specification] requires `-38002` when a `VALID` head's
3429    /// safe or finalized hash is outside that chain, and requires all forkchoice state updates to
3430    /// be atomic. In direct FCU processing, this check must therefore run before canonicalizing
3431    /// the head or updating either marker; checking only after canonicalization would leave an
3432    /// invalid reorg applied.
3433    ///
3434    /// `chain_update` describes a proposed commit or reorg that has not yet been applied. Its new
3435    /// blocks and the canonical prefix below its first block form the proposed chain. Without a
3436    /// chain update, the proposed head is already canonical, so only canonical blocks through its
3437    /// height are eligible. Canonical-prefix hashes are resolved via
3438    /// [`Self::find_canonical_header`], which rejects stale persisted headers whose disk reorg
3439    /// cleanup is pending. A zero safe or finalized hash leaves that marker unchanged.
3440    /// Returns `Ok(false)` for an unknown or off-chain hash and propagates provider errors.
3441    ///
3442    /// [Engine API forkchoiceUpdated specification]: https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#specification-1
3443    fn is_consistent_forkchoice_state(
3444        &self,
3445        state: ForkchoiceState,
3446        chain_update: Option<&NewCanonicalChain<N>>,
3447    ) -> ProviderResult<bool> {
3448        let canonical_head_number = match chain_update {
3449            Some(chain_update) => {
3450                let new = chain_update.new_blocks();
3451                // Only the canonical prefix below the new branch remains on the proposed chain.
3452                new.first().expect("non empty chain").block_number() - 1
3453            }
3454            None => {
3455                let Some(head) = self.find_canonical_header(state.head_block_hash)? else {
3456                    return Ok(false)
3457                };
3458                head.number()
3459            }
3460        };
3461
3462        for hash in [state.finalized_block_hash, state.safe_block_hash] {
3463            if hash.is_zero() || chain_update.is_some_and(|update| update.contains(hash)) {
3464                continue
3465            }
3466            let Some(header) = self.find_canonical_header(hash)? else { return Ok(false) };
3467            if header.number() > canonical_head_number {
3468                return Ok(false)
3469            }
3470        }
3471        Ok(true)
3472    }
3473
3474    /// Updates the tracked finalized block if we have it.
3475    fn update_finalized_block(
3476        &self,
3477        finalized_block_hash: B256,
3478    ) -> Result<(), OnForkChoiceUpdated> {
3479        if finalized_block_hash.is_zero() {
3480            return Ok(())
3481        }
3482
3483        match self.find_canonical_header(finalized_block_hash) {
3484            Ok(None) => {
3485                debug!(target: "engine::tree", "Finalized block not found in canonical chain");
3486                // if the finalized block is not known, we can't update the finalized block
3487                return Err(OnForkChoiceUpdated::invalid_state())
3488            }
3489            Ok(Some(finalized)) => {
3490                if Some(finalized.num_hash()) !=
3491                    self.canonical_in_memory_state.get_finalized_num_hash()
3492                {
3493                    // we're also persisting the finalized block on disk so we can reload it on
3494                    // restart this is required by optimism which queries the finalized block: <https://github.com/ethereum-optimism/optimism/blob/c383eb880f307caa3ca41010ec10f30f08396b2e/op-node/rollup/sync/start.go#L65-L65>
3495                    let _ = self.persistence.save_finalized_block_number(finalized.number());
3496                    self.canonical_in_memory_state.set_finalized(finalized.clone());
3497                    // Update finalized block height metric
3498                    self.metrics.tree.finalized_block_height.set(finalized.number() as f64);
3499                }
3500            }
3501            Err(err) => {
3502                error!(target: "engine::tree", %err, "Failed to fetch finalized block header");
3503            }
3504        }
3505
3506        Ok(())
3507    }
3508
3509    /// Updates the tracked safe block if we have it
3510    fn update_safe_block(&self, safe_block_hash: B256) -> Result<(), OnForkChoiceUpdated> {
3511        if safe_block_hash.is_zero() {
3512            return Ok(())
3513        }
3514
3515        match self.find_canonical_header(safe_block_hash) {
3516            Ok(None) => {
3517                debug!(target: "engine::tree", "Safe block not found in canonical chain");
3518                // if the safe block is not known, we can't update the safe block
3519                return Err(OnForkChoiceUpdated::invalid_state())
3520            }
3521            Ok(Some(safe)) => {
3522                if Some(safe.num_hash()) != self.canonical_in_memory_state.get_safe_num_hash() {
3523                    // we're also persisting the safe block on disk so we can reload it on
3524                    // restart this is required by optimism which queries the safe block: <https://github.com/ethereum-optimism/optimism/blob/c383eb880f307caa3ca41010ec10f30f08396b2e/op-node/rollup/sync/start.go#L65-L65>
3525                    let _ = self.persistence.save_safe_block_number(safe.number());
3526                    self.canonical_in_memory_state.set_safe(safe.clone());
3527                    // Update safe block height metric
3528                    self.metrics.tree.safe_block_height.set(safe.number() as f64);
3529                }
3530            }
3531            Err(err) => {
3532                error!(target: "engine::tree", %err, "Failed to fetch safe block header");
3533            }
3534        }
3535
3536        Ok(())
3537    }
3538
3539    /// Ensures that the given forkchoice state is consistent, assuming the head block has been
3540    /// made canonical.
3541    ///
3542    /// If the forkchoice state is consistent, this will return Ok(()). Otherwise, this will
3543    /// return an instance of [`OnForkChoiceUpdated`] that is INVALID.
3544    ///
3545    /// This also updates the safe and finalized blocks in the [`CanonicalInMemoryState`], if they
3546    /// are consistent with the head block.
3547    fn ensure_consistent_forkchoice_state(
3548        &self,
3549        state: ForkchoiceState,
3550    ) -> Result<(), OnForkChoiceUpdated> {
3551        // Ensure that the finalized block, if not zero, is known and in the canonical chain
3552        // after the head block is canonicalized.
3553        //
3554        // This ensures that the finalized block is consistent with the head block, i.e. the
3555        // finalized block is an ancestor of the head block.
3556        self.update_finalized_block(state.finalized_block_hash)?;
3557
3558        // Also ensure that the safe block, if not zero, is known and in the canonical chain
3559        // after the head block is canonicalized.
3560        //
3561        // This ensures that the safe block is consistent with the head block, i.e. the safe
3562        // block is an ancestor of the head block.
3563        self.update_safe_block(state.safe_block_hash)
3564    }
3565
3566    /// Validates the payload attributes with respect to the header and fork choice state.
3567    ///
3568    /// This is called during `engine_forkchoiceUpdated` when the CL provides payload attributes,
3569    /// indicating it wants the EL to start building a new block.
3570    ///
3571    /// Runs [`PayloadValidator::validate_payload_attributes_against_header`](reth_engine_primitives::PayloadValidator::validate_payload_attributes_against_header) to ensure
3572    /// `payloadAttributes.timestamp > headBlock.timestamp` per the Engine API spec.
3573    ///
3574    /// If validation passes, sends the attributes to the payload builder to start a new
3575    /// payload job. If it fails, returns `INVALID_PAYLOAD_ATTRIBUTES` without rolling back
3576    /// the forkchoice update.
3577    ///
3578    /// Note: At this point, the fork choice update is considered to be VALID, however, we can still
3579    /// return an error if the payload attributes are invalid.
3580    fn process_payload_attributes(
3581        &mut self,
3582        attributes: T::PayloadAttributes,
3583        head: &N::BlockHeader,
3584        state: ForkchoiceState,
3585    ) -> OnForkChoiceUpdated {
3586        if let Err(err) =
3587            self.payload_validator.validate_payload_attributes_against_header(&attributes, head)
3588        {
3589            warn!(target: "engine::tree", %err, ?head, "Invalid payload attributes");
3590            return OnForkChoiceUpdated::invalid_payload_attributes()
3591        }
3592
3593        // 8. Client software MUST begin a payload build process building on top of
3594        //    forkchoiceState.headBlockHash and identified via buildProcessId value if
3595        //    payloadAttributes is not null and the forkchoice state has been updated successfully.
3596        //    The build process is specified in the Payload building section.
3597
3598        // Acquire this before preparing resources because state-root setup can already start
3599        // workers that need the current in-memory overlay.
3600        let payload_build = self.payload_builds.acquire();
3601
3602        let resources = self
3603            .payload_validator
3604            .payload_builder_resources(
3605                state.head_block_hash,
3606                head,
3607                attributes.timestamp(),
3608                &mut self.state,
3609            )
3610            .with_lease(PayloadBuilderLease::new(payload_build));
3611
3612        // send the payload to the builder and return the receiver for the pending payload
3613        // id, initiating payload job is handled asynchronously
3614        let pending_payload_id = self.payload_builder.send_new_payload(BuildNewPayload {
3615            parent_hash: state.head_block_hash,
3616            attributes,
3617            resources,
3618        });
3619
3620        // Client software MUST respond to this method call in the following way:
3621        // {
3622        //      payloadStatus: {
3623        //          status: VALID,
3624        //          latestValidHash: forkchoiceState.headBlockHash,
3625        //          validationError: null
3626        //      },
3627        //      payloadId: buildProcessId
3628        // }
3629        //
3630        // if the payload is deemed VALID and the build process has begun.
3631        OnForkChoiceUpdated::updated_with_pending_payload_id(
3632            PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)),
3633            pending_payload_id,
3634        )
3635    }
3636
3637    /// Remove all blocks up to __and including__ the given block number.
3638    ///
3639    /// If a finalized hash is provided, the only non-canonical blocks which will be removed are
3640    /// those which have a fork point at or below the finalized hash.
3641    ///
3642    /// Canonical blocks below the upper bound will still be removed.
3643    pub(crate) fn remove_before(
3644        &mut self,
3645        upper_bound: BlockNumHash,
3646        finalized_hash: Option<B256>,
3647    ) -> ProviderResult<()> {
3648        // first fetch the finalized block number and then call the remove_before method on
3649        // tree_state
3650        let num = if let Some(hash) = finalized_hash {
3651            self.provider.block_number(hash)?.map(|number| BlockNumHash { number, hash })
3652        } else {
3653            None
3654        };
3655
3656        self.state.tree_state.remove_until(
3657            upper_bound,
3658            self.persistence_state.last_persisted_block.hash,
3659            num,
3660        );
3661        self.metrics.engine.executed_blocks.set(self.state.tree_state.block_count() as f64);
3662        Ok(())
3663    }
3664}
3665
3666/// Events received in the main engine loop.
3667#[derive(Debug)]
3668enum LoopEvent<T, N>
3669where
3670    N: NodePrimitives,
3671    T: PayloadTypes,
3672{
3673    /// An engine API message was received.
3674    EngineMessage(FromEngine<EngineApiRequest<T, N>, N::Block>),
3675    /// A persistence task completed.
3676    PersistenceComplete {
3677        /// The unified result of the persistence operation.
3678        result: PersistenceResult,
3679        /// When the persistence operation started.
3680        start_time: Instant,
3681    },
3682    /// The last active payload job has finished, so suppressed persistence may resume.
3683    PayloadBuildFinished,
3684    /// A channel was disconnected.
3685    Disconnected,
3686}
3687
3688/// Tracks payload jobs that may access the current in-memory overlay.
3689#[derive(Clone, Debug)]
3690struct PayloadBuildTracker {
3691    active: Arc<AtomicUsize>,
3692    finished_tx: Sender<()>,
3693}
3694
3695impl PayloadBuildTracker {
3696    /// Creates a tracker and a receiver notified when its active count reaches zero.
3697    fn new() -> (Self, Receiver<()>) {
3698        let (finished_tx, finished_rx) = crossbeam_channel::bounded(1);
3699        (Self { active: Arc::new(AtomicUsize::new(0)), finished_tx }, finished_rx)
3700    }
3701
3702    /// Acquires a lease for one payload job.
3703    fn acquire(&self) -> PayloadBuildLease {
3704        self.active.fetch_add(1, Ordering::AcqRel);
3705        PayloadBuildLease {
3706            active: Arc::clone(&self.active),
3707            finished_tx: self.finished_tx.clone(),
3708        }
3709    }
3710
3711    /// Returns whether at least one payload job is active.
3712    fn is_active(&self) -> bool {
3713        self.active.load(Ordering::Acquire) != 0
3714    }
3715}
3716
3717/// A lease held for the lifetime of a payload job.
3718#[derive(Debug)]
3719struct PayloadBuildLease {
3720    active: Arc<AtomicUsize>,
3721    finished_tx: Sender<()>,
3722}
3723
3724impl Drop for PayloadBuildLease {
3725    fn drop(&mut self) {
3726        let previous = self.active.fetch_sub(1, Ordering::AcqRel);
3727        debug_assert!(previous > 0, "payload build lease count underflow");
3728
3729        if previous == 1 {
3730            // The bounded channel coalesces completion notifications. The engine always checks
3731            // the counter again before applying a pending handoff.
3732            let _ = self.finished_tx.try_send(());
3733        }
3734    }
3735}
3736
3737/// Block inclusion can be valid, accepted, or invalid. Invalid blocks are returned as an error
3738/// variant.
3739///
3740/// If we don't know the block's parent, we return `Disconnected`, as we can't claim that the block
3741/// is valid or not.
3742#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3743pub enum BlockStatus {
3744    /// The block is valid: its parent state was available, so it was executed and inserted into
3745    /// the tree.
3746    ///
3747    /// Note: this does not imply the block extends the canonical chain. Blocks on a fork are
3748    /// executed and inserted the same way and report this status as well.
3749    Valid,
3750    /// The block may be valid and has an unknown missing ancestor.
3751    Disconnected {
3752        /// Current canonical head.
3753        head: BlockNumHash,
3754        /// The lowest ancestor block that is not connected to the canonical chain.
3755        missing_ancestor: BlockNumHash,
3756    },
3757}
3758
3759/// How a payload was inserted if it was valid.
3760///
3761/// If the payload was valid, but has already been seen, [`InsertPayloadOk::AlreadySeen`] is
3762/// returned, otherwise [`InsertPayloadOk::Inserted`] is returned.
3763#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3764pub enum InsertPayloadOk {
3765    /// The payload was valid, but we have already seen it.
3766    AlreadySeen(BlockStatus),
3767    /// The payload was valid and inserted into the tree.
3768    Inserted(BlockStatus),
3769}
3770
3771/// Target for block persistence.
3772#[derive(Debug, Clone, Copy)]
3773enum PersistTarget {
3774    /// Persist up to `canonical_head - memory_block_buffer_target`.
3775    Threshold,
3776    /// Persist all blocks up to and including the canonical head.
3777    Head,
3778    /// Persist state/trie updates through the persisted block frontier.
3779    Persisted,
3780}
3781
3782/// Result of waiting for caches to become available.
3783#[derive(Debug, Clone, Copy, Default)]
3784pub struct CacheWaitDurations {
3785    /// Time spent waiting for the execution cache lock, excluding post-unlock cache destruction.
3786    pub execution_cache: Duration,
3787    /// Time spent waiting for the sparse trie lock.
3788    pub sparse_trie: Duration,
3789}
3790
3791/// Trait for types that can wait for caches to become available.
3792///
3793/// Used by `reth_newPayload` to wait for cache updates before starting payload processing.
3794/// Removed execution-cache allocations may still be destroyed concurrently after unlocking.
3795pub trait WaitForCaches {
3796    /// Waits for cache updates to complete.
3797    ///
3798    /// Returns the time spent waiting for each cache separately.
3799    fn wait_for_caches(&self) -> CacheWaitDurations;
3800}