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