Skip to main content

reth_engine_tree/tree/payload_processor/
mod.rs

1//! Entrypoint for payload processing.
2
3use super::precompile_cache::PrecompileCacheMap;
4use crate::tree::{
5    payload_processor::prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
6    CachedStateCacheMetrics, CachedStateMetrics, CachedStateMetricsSource, ExecutionCache,
7    ExecutionEnv, PayloadExecutionCache, SavedCache, StateProviderBuilder, TreeConfig,
8};
9use alloy_eips::eip1898::BlockWithParent;
10use alloy_primitives::B256;
11use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
12use prewarm::PrewarmMetrics;
13use rayon::prelude::*;
14use reth_evm::{
15    block::ExecutableTxParts,
16    execute::{ExecutableTxFor, WithTxEnv},
17    ConfigureEvm, ConvertTx, ExecutableTxIterator, ExecutableTxTuple, SpecFor, TxEnvFor,
18};
19use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
20use reth_provider::{
21    BlockExecutionOutput, BlockNumReader, DatabaseProviderFactory, PruneCheckpointReader,
22    StageCheckpointReader, StorageSettingsCache, TryIntoHistoricalStateProvider,
23};
24use reth_revm::db::BundleState;
25use reth_tasks::Runtime;
26pub use reth_trie_parallel::{
27    error::StateRootTaskError,
28    state_root_task::{
29        evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
30        StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
31        StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
32    },
33};
34use std::{
35    ops::Not,
36    sync::{
37        atomic::{AtomicBool, AtomicUsize},
38        mpsc, Arc, OnceLock,
39    },
40};
41use tracing::{debug, instrument, trace, warn, Span};
42
43pub mod bal;
44pub(crate) mod bal_prewarm_pool;
45pub mod prewarm;
46pub mod receipt_root_task;
47
48/// Blocks with fewer transactions than this skip prewarming, since the fixed overhead of spawning
49/// prewarm workers exceeds the execution time saved.
50pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
51
52/// Type alias for [`PayloadHandle`] returned by payload processor spawn methods.
53type IteratorTx<Evm, I> = RecoveredTx<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
54
55type IteratorPayloadHandle<Evm, I> = PayloadHandle<
56    IteratorTx<Evm, I>,
57    <I as ExecutableTxTuple>::Error,
58    <<Evm as ConfigureEvm>::Primitives as NodePrimitives>::Receipt,
59>;
60
61type IteratorPrewarmTxReceiver<Evm, I> =
62    PrewarmTxReceiver<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
63
64type IteratorExecuteTxReceiver<Evm, I> = ExecuteTxReceiver<
65    TxEnvFor<Evm>,
66    <I as ExecutableTxIterator<Evm>>::Recovered,
67    <I as ExecutableTxTuple>::Error,
68>;
69
70type RecoveredTx<TxEnv, Recovered> = WithTxEnv<TxEnv, Recovered>;
71type IndexedTxResult<Tx, Err> = (usize, Result<Tx, Err>);
72type IndexedTxReceiver<Tx, Err> = CrossbeamReceiver<IndexedTxResult<Tx, Err>>;
73type IndexedTxSender<Tx, Err> = CrossbeamSender<IndexedTxResult<Tx, Err>>;
74type PrewarmTxReceiver<TxEnv, Recovered> = mpsc::Receiver<(usize, RecoveredTx<TxEnv, Recovered>)>;
75type ExecuteTxReceiver<TxEnv, Recovered, Err> =
76    IndexedTxReceiver<RecoveredTx<TxEnv, Recovered>, Err>;
77type ExecuteTxSender<TxEnv, Recovered, Err> = IndexedTxSender<RecoveredTx<TxEnv, Recovered>, Err>;
78
79/// Entrypoint for executing the payload.
80#[derive(Debug)]
81pub struct PayloadProcessor<Evm>
82where
83    Evm: ConfigureEvm,
84{
85    /// The executor used by to spawn tasks.
86    executor: Runtime,
87    /// The most recent cache used for execution.
88    execution_cache: PayloadExecutionCache,
89    /// Metrics for the execution cache.
90    cache_metrics: Option<CachedStateMetrics>,
91    /// Metrics for shared execution cache state.
92    cache_state_metrics: Option<CachedStateCacheMetrics>,
93    /// Cross-block cache size in bytes.
94    cross_block_cache_size: usize,
95    /// Whether transactions should not be executed on prewarming task.
96    disable_transaction_prewarming: bool,
97    /// Whether state cache should be disable
98    disable_state_cache: bool,
99    /// Determines how to configure the evm for execution.
100    evm_config: Evm,
101    /// Whether precompile cache should be disabled.
102    precompile_cache_disabled: bool,
103    /// Precompile cache map.
104    precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
105    /// Whether to disable BAL-driven parallel state root computation.
106    /// Only valid when BAL parallel execution is also disabled.
107    disable_bal_parallel_state_root: bool,
108    /// Whether BAL state prefetching during prewarm is disabled.
109    disable_bal_batch_io: bool,
110    /// Dedicated blocking pool for warming the BAL read-set, created lazily on the first BAL block
111    /// (see [`Self::bal_prewarm_pool`]). Its threads exit when the processor is dropped.
112    bal_prewarm_pool: OnceLock<Arc<bal_prewarm_pool::BalPrewarmPool>>,
113}
114
115impl<Evm> PayloadProcessor<Evm>
116where
117    Evm: ConfigureEvm,
118{
119    /// Creates a new payload processor.
120    pub fn new(
121        executor: Runtime,
122        evm_config: Evm,
123        config: &TreeConfig,
124        precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
125    ) -> Self {
126        Self {
127            executor,
128            execution_cache: Default::default(),
129            cross_block_cache_size: config.cross_block_cache_size(),
130            disable_transaction_prewarming: config.disable_prewarming(),
131            evm_config,
132            disable_state_cache: config.disable_state_cache(),
133            precompile_cache_disabled: config.precompile_cache_disabled(),
134            precompile_cache_map,
135            cache_metrics: (!config.disable_cache_metrics())
136                .then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
137            cache_state_metrics: (!config.disable_cache_metrics())
138                .then(CachedStateCacheMetrics::default),
139            disable_bal_parallel_state_root: config.disable_bal_parallel_state_root(),
140            disable_bal_batch_io: config.disable_bal_batch_io(),
141            bal_prewarm_pool: OnceLock::new(),
142        }
143    }
144
145    /// Returns the dedicated BAL read-set prewarm pool, spawning its blocking worker threads on
146    /// first use (only the BAL parallel execution path calls this).
147    fn bal_prewarm_pool(&self) -> Arc<bal_prewarm_pool::BalPrewarmPool> {
148        self.bal_prewarm_pool
149            .get_or_init(|| {
150                bal_prewarm_pool::BalPrewarmPool::new(bal_prewarm_pool::DEFAULT_BAL_PREWARM_THREADS)
151            })
152            .clone()
153    }
154
155    /// Returns the shared execution cache handle used for engine backpressure.
156    pub(crate) fn execution_cache(&self) -> PayloadExecutionCache {
157        self.execution_cache.clone()
158    }
159}
160
161impl<Evm> PayloadProcessor<Evm>
162where
163    Evm: ConfigureEvm + 'static,
164{
165    /// Spawns transaction conversion and cache prewarming, optionally wiring prewarm output into
166    /// an externally-owned state-root task.
167    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
168    pub fn spawn_with_state_root_streams<P, I: ExecutableTxIterator<Evm>>(
169        &self,
170        env: ExecutionEnv<Evm>,
171        transactions: I,
172        provider_builder: StateProviderBuilder<Evm::Primitives, P>,
173        hint_stream: Option<StateRootHintStream>,
174        hashed_update_stream: Option<StateRootUpdateStream>,
175        parallel_bal_execution: bool,
176    ) -> IteratorPayloadHandle<Evm, I>
177    where
178        P: DatabaseProviderFactory + Clone + 'static,
179        P::Provider: BlockNumReader
180            + PruneCheckpointReader
181            + StageCheckpointReader
182            + StorageSettingsCache
183            + TryIntoHistoricalStateProvider
184            + 'static,
185    {
186        let (prewarm_rx, execution_rx) =
187            self.spawn_tx_iterator(transactions, env.transaction_count, parallel_bal_execution);
188        let prewarm_handle = self.spawn_caching_with(
189            env,
190            prewarm_rx,
191            provider_builder,
192            hint_stream,
193            hashed_update_stream,
194            parallel_bal_execution,
195        );
196        PayloadHandle { prewarm_handle, transactions: execution_rx, _span: Span::current() }
197    }
198
199    /// Transaction count threshold below which sequential conversion is used.
200    ///
201    /// For blocks with fewer than this many transactions, the rayon parallel iterator overhead
202    /// (work-stealing setup, channel-based reorder) exceeds the cost of sequential conversion.
203    /// Inspired by Nethermind's `RecoverSignature` which uses sequential `foreach` for small
204    /// blocks.
205    const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
206
207    /// Number of leading transactions to convert sequentially before entering the rayon
208    /// parallel path.
209    ///
210    /// Rayon's work-stealing does not guarantee that index 0 is processed first, so the
211    /// ordered consumer can block for up to ~1ms waiting for the first slot. By converting
212    /// a small head sequentially and sending it immediately, execution can start without
213    /// waiting for rayon scheduling.
214    const PARALLEL_PREFETCH_COUNT: usize = 4;
215
216    /// Size of the first parallel tx batch in non-BAL path.
217    const FIRST_PARALLEL_TX_WINDOW_SIZE: usize = 64;
218
219    /// Spawns a task advancing transaction env iterator and streaming updates through a channel.
220    ///
221    /// For blocks with fewer than [`Self::SMALL_BLOCK_TX_THRESHOLD`] transactions, uses
222    /// sequential iteration to avoid rayon overhead. For larger blocks, uses rayon parallel
223    /// iteration to convert transactions in parallel while streaming results to execution.
224    ///
225    /// When `parallel_bal_execution` is disabled, preserves the original transaction order.
226    /// Otherwise, streams results as they become available.
227    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
228    fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
229        &self,
230        transactions: I,
231        transaction_count: usize,
232        parallel_bal_execution: bool,
233    ) -> (IteratorPrewarmTxReceiver<Evm, I>, IteratorExecuteTxReceiver<Evm, I>) {
234        let (prewarm_tx, prewarm_rx) = mpsc::sync_channel(transaction_count);
235        let (execute_tx, execute_rx) = crossbeam_channel::bounded(transaction_count);
236
237        if transaction_count == 0 {
238            // Empty block — nothing to do.
239        } else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
240            // Sequential path for small blocks — avoids rayon work-stealing setup and
241            // channel-based reorder overhead when it costs more than sequential conversion.
242            debug!(
243                target: "engine::tree::payload_processor",
244                transaction_count,
245                "using sequential sig recovery for small block"
246            );
247            self.executor.spawn_blocking_named("tx-iterator", move || {
248                let (transactions, convert) = transactions.into_parts();
249                convert_serial(transactions.into_iter(), &convert, &prewarm_tx, &execute_tx);
250            });
251        } else {
252            // Parallel path — recover signatures in parallel on rayon, stream results
253            // to prewarming and execution.
254            let executor = self.executor.clone();
255            self.executor.spawn_blocking_named("tx-iterator", move || {
256                let (transactions, convert) = transactions.into_parts();
257                if parallel_bal_execution {
258                    // With BALs, we don't care about the order of transactions in execution and
259                    // prewarming, so we don't have to use `for_each_ordered_in`.
260                    executor.cpu_pool().install(|| {
261                        transactions
262                            .into_par_iter()
263                            .enumerate()
264                            .map(|(i, tx)| {
265                                let tx = convert.convert(tx);
266                                (i, tx)
267                            })
268                            .for_each(|(idx, tx)| {
269                                let tx = tx.map(|tx| {
270                                    let tx = WithTxEnv::new(tx);
271                                    let _ = prewarm_tx.send((idx, tx.clone()));
272                                    tx
273                                });
274                                let _ = execute_tx.send((idx, tx));
275                                trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
276                            });
277                    });
278                } else {
279                    // To avoid a ~1ms stall waiting for rayon to schedule index 0, the first
280                    // few transactions are recovered sequentially and sent immediately before
281                    // entering the parallel iterator for the remainder.
282                    let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
283                    let mut iter = transactions.into_iter();
284
285                    // Convert the first few transactions sequentially so execution can
286                    // start immediately without waiting for rayon work-stealing.
287                    convert_serial(iter.by_ref().take(prefetch), &convert, &prewarm_tx, &execute_tx);
288
289                    let mut iter = iter.enumerate();
290
291                    let mut batch_size = Self::FIRST_PARALLEL_TX_WINDOW_SIZE;
292
293                    // Without BALs, we need to preserve the initial order of transactions.
294                    // Process exponentially increasing windows to make sure that first transactions are prioritized.
295                    executor.cpu_pool().install(move || {
296                        loop {
297                            let chunk = iter
298                                .by_ref()
299                                .take(batch_size)
300                                .collect::<Vec<_>>();
301                            if chunk.is_empty() {
302                                break;
303                            }
304
305                            batch_size = batch_size.saturating_mul(2);
306
307                            let chunk = chunk
308                                .into_par_iter()
309                                .map(|(i, tx)| {
310                                    let idx = i + prefetch;
311                                    let tx = convert.convert(tx).map(WithTxEnv::new);
312                                    (idx, tx)
313                                })
314                                .collect::<Vec<_>>();
315
316                            for (idx, tx) in chunk {
317                                if let Ok(tx) = &tx {
318                                    let _ = prewarm_tx.send((idx, tx.clone()));
319                                }
320                                let _ = execute_tx.send((idx, tx));
321                                trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
322                            }
323                        }
324                    });
325                }
326            });
327        }
328
329        (prewarm_rx, execute_rx)
330    }
331
332    /// Spawn prewarming optionally wired to the sparse trie task for target updates.
333    ///
334    /// `parallel_bal_execution` is true when the BAL execute path will execute this block. In
335    /// that case prewarm runs in BAL mode: it streams BAL-derived sparse-trie updates and,
336    /// unless `disable_bal_batch_io` is set, prefetches BAL-declared state into the shared cache.
337    #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
338    fn spawn_caching_with<P>(
339        &self,
340        env: ExecutionEnv<Evm>,
341        transactions: mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + 'static)>,
342        provider_builder: StateProviderBuilder<Evm::Primitives, P>,
343        hint_stream: Option<StateRootHintStream>,
344        hashed_update_stream: Option<StateRootUpdateStream>,
345        parallel_bal_execution: bool,
346    ) -> CacheTaskHandle<<Evm::Primitives as NodePrimitives>::Receipt>
347    where
348        P: DatabaseProviderFactory + Clone + 'static,
349        P::Provider: BlockNumReader
350            + PruneCheckpointReader
351            + StageCheckpointReader
352            + StorageSettingsCache
353            + TryIntoHistoricalStateProvider
354            + 'static,
355    {
356        // Each mode carries the capability its producers use; the rest is dropped here, so
357        // unused capabilities do not keep the state-root task's update channel open.
358        let mode = if parallel_bal_execution {
359            PrewarmMode::BlockAccessList {
360                bal: env.decoded_bal.clone().expect("BAL dispatch implies decoded BAL"),
361                updates: hashed_update_stream,
362            }
363        } else if self.disable_transaction_prewarming ||
364            env.transaction_count < SMALL_BLOCK_TX_THRESHOLD
365        {
366            PrewarmMode::Skipped
367        } else {
368            PrewarmMode::Transactions { pending: transactions, hints: hint_stream }
369        };
370        let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
371
372        let executed_tx_index = Arc::new(AtomicUsize::new(0));
373        // configure prewarming
374        let prewarm_ctx = PrewarmContext {
375            env,
376            evm_config: self.evm_config.clone(),
377            saved_cache: saved_cache.clone(),
378            provider: provider_builder,
379            bal_prewarm_pool: parallel_bal_execution.then(|| self.bal_prewarm_pool()),
380            metrics: PrewarmMetrics::default(),
381            cache_metrics: self.cache_metrics.clone(),
382            cache_state_metrics: self.cache_state_metrics.clone(),
383            terminate_execution: Arc::new(AtomicBool::new(false)),
384            executed_tx_index: Arc::clone(&executed_tx_index),
385            precompile_cache_disabled: self.precompile_cache_disabled,
386            precompile_cache_map: self.precompile_cache_map.clone(),
387            disable_bal_parallel_state_root: self.disable_bal_parallel_state_root,
388            disable_bal_batch_io: self.disable_bal_batch_io,
389        };
390
391        let (prewarm_task, to_prewarm_task) =
392            PrewarmCacheTask::new(self.executor.clone(), self.execution_cache.clone(), prewarm_ctx);
393        {
394            let to_prewarm_task = to_prewarm_task.clone();
395            self.executor.spawn_blocking_named("prewarm", move || {
396                prewarm_task.run(mode, to_prewarm_task);
397            });
398        }
399
400        CacheTaskHandle {
401            saved_cache,
402            to_prewarm_task: Some(to_prewarm_task),
403            executed_tx_index,
404            cache_metrics: self.cache_metrics.clone(),
405        }
406    }
407
408    /// Returns the cache for the given parent hash.
409    ///
410    /// If the given hash is different then what is recently cached, then this will create a new
411    /// instance.
412    #[instrument(level = "debug", target = "engine::caching", skip(self))]
413    pub fn cache_for(&self, parent_hash: B256) -> SavedCache {
414        if let Some(cache) = self.execution_cache.get_cache_for(parent_hash) {
415            debug!("reusing execution cache");
416            cache
417        } else {
418            debug!("creating new execution cache on cache miss");
419            let start = Instant::now();
420            let cache = ExecutionCache::new(self.cross_block_cache_size);
421            if let Some(metrics) = &self.cache_metrics {
422                metrics.record_cache_creation(start.elapsed());
423            }
424            SavedCache::new(parent_hash, cache)
425        }
426    }
427
428    /// Updates the execution cache with the post-execution state from an inserted block.
429    ///
430    /// This is used when blocks are inserted directly (e.g., locally built blocks by sequencers)
431    /// to ensure the cache remains warm for subsequent block execution.
432    ///
433    /// The cache enables subsequent blocks to reuse account, storage, and bytecode data without
434    /// hitting the database, maintaining performance consistency.
435    pub fn on_inserted_executed_block(
436        &self,
437        block_with_parent: BlockWithParent,
438        bundle_state: &BundleState,
439    ) {
440        let cache_state_metrics = self.cache_state_metrics.clone();
441        self.execution_cache.update_with_guard(|cached| {
442            if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
443                debug!(
444                    target: "engine::caching",
445                    parent_hash = %block_with_parent.parent,
446                    "Cannot find cache for parent hash, skip updating cache with new state for inserted executed block",
447                );
448                return
449            }
450
451            if let Some(cache) = cached.as_ref().filter(|cache| !cache.is_available()) {
452                debug!(
453                    target: "engine::caching",
454                    parent_hash = %block_with_parent.parent,
455                    usage_count = cache.usage_count(),
456                    "Execution cache is in use, skip updating cache with new state for inserted executed block",
457                );
458                return
459            }
460
461            // Take existing cache (if any) or create fresh caches
462            let caches = match cached.take() {
463                Some(existing) => existing.cache().clone(),
464                None => ExecutionCache::new(self.cross_block_cache_size),
465            };
466
467            // Insert the block's bundle state into cache
468            let new_cache = SavedCache::new(block_with_parent.block.hash, caches);
469            if new_cache.cache().insert_state(bundle_state).is_err() {
470                *cached = None;
471                debug!(target: "engine::caching", "cleared execution cache on update error");
472                return
473            }
474            new_cache.update_metrics(cache_state_metrics.as_ref());
475
476            // Replace with the updated cache
477            *cached = Some(new_cache);
478            debug!(target: "engine::caching", ?block_with_parent, "Updated execution cache for inserted block");
479        });
480    }
481}
482
483/// Converts transactions sequentially and sends them to the prewarm and execute channels.
484fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
485    iter: impl Iterator<Item = RawTx>,
486    convert: &C,
487    prewarm_tx: &mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>,
488    execute_tx: &ExecuteTxSender<TxEnv, Recovered, Err>,
489) where
490    Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
491    TxEnv: Clone,
492    C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
493{
494    for (idx, raw_tx) in iter.enumerate() {
495        let tx = convert.convert(raw_tx);
496        let tx = tx.map(|tx| WithTxEnv::new(tx));
497        if let Ok(tx) = &tx {
498            let _ = prewarm_tx.send((idx, tx.clone()));
499        }
500        let _ = execute_tx.send((idx, tx));
501        trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
502    }
503}
504
505/// Handle to all the spawned tasks.
506///
507/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the
508/// caching task without cloning the expensive `BundleState`.
509#[derive(Debug)]
510pub struct PayloadHandle<Tx, Err, R> {
511    prewarm_handle: CacheTaskHandle<R>,
512    /// Stream of block transactions and their indices in the block.
513    transactions: IndexedTxReceiver<Tx, Err>,
514    /// Span for tracing
515    _span: Span,
516}
517
518impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
519    /// Returns a clone of the caches used by prewarming
520    pub fn caches(&self) -> Option<ExecutionCache> {
521        self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
522    }
523
524    /// Returns engine cache metrics if a cache exists for prewarming.
525    pub fn cache_metrics(&self) -> Option<CachedStateMetrics> {
526        self.prewarm_handle.cache_metrics.clone()
527    }
528
529    /// Returns a reference to the shared executed transaction index counter.
530    ///
531    /// The main execution loop should store `index + 1` after executing each transaction so that
532    /// prewarm workers can skip transactions that have already been processed.
533    pub const fn executed_tx_index(&self) -> &Arc<AtomicUsize> {
534        &self.prewarm_handle.executed_tx_index
535    }
536
537    /// Terminates the pre-warming transaction processing.
538    ///
539    /// Note: This does not terminate the task yet.
540    pub fn stop_prewarming_execution(&self) {
541        self.prewarm_handle.stop_prewarming_execution()
542    }
543
544    /// Terminates the entire caching task.
545    ///
546    /// If the [`BlockExecutionOutput`] is provided it will update the shared cache using its
547    /// bundle state. Using `Arc<ExecutionOutcome>` allows sharing with the main execution
548    /// path without cloning the expensive `BundleState`.
549    ///
550    /// Returns a sender for the channel that should be notified on block validation success.
551    pub fn terminate_caching(
552        &mut self,
553        execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
554    ) -> Option<mpsc::Sender<()>> {
555        self.prewarm_handle.terminate_caching(execution_outcome)
556    }
557
558    /// Returns iterator yielding transactions from the stream.
559    pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
560        self.transactions.iter().map(|(_, tx)| tx)
561    }
562
563    /// Returns a clone of the indexed transaction receiver.
564    pub fn clone_transaction_receiver(&self) -> IndexedTxReceiver<Tx, Err> {
565        self.transactions.clone()
566    }
567}
568
569/// Access to the spawned [`PrewarmCacheTask`].
570///
571/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the
572/// prewarm task without cloning the expensive `BundleState`.
573#[derive(Debug)]
574pub struct CacheTaskHandle<R> {
575    /// The shared cache the task operates with.
576    saved_cache: Option<SavedCache>,
577    /// Channel to the spawned prewarm task if any
578    to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
579    /// Shared counter tracking the next transaction index to be executed by the main execution
580    /// loop. Prewarm workers skip transactions below this index.
581    executed_tx_index: Arc<AtomicUsize>,
582    /// Metrics for the execution cache.
583    cache_metrics: Option<CachedStateMetrics>,
584}
585
586impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
587    /// Terminates the pre-warming transaction processing.
588    ///
589    /// Note: This does not terminate the task yet.
590    pub fn stop_prewarming_execution(&self) {
591        self.to_prewarm_task
592            .as_ref()
593            .map(|tx| tx.send(PrewarmTaskEvent::TerminateTransactionExecution).ok());
594    }
595
596    /// Terminates the entire pre-warming task.
597    ///
598    /// If the [`BlockExecutionOutput`] is provided it will update the shared cache using its
599    /// bundle state. Using `Arc<ExecutionOutcome>` avoids cloning the expensive `BundleState`.
600    #[must_use = "sender must be used and notified on block validation success"]
601    pub fn terminate_caching(
602        &mut self,
603        execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
604    ) -> Option<mpsc::Sender<()>> {
605        if let Some(tx) = self.to_prewarm_task.take() {
606            let (valid_block_tx, valid_block_rx) = mpsc::channel();
607            let event = PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx };
608            let _ = tx.send(event);
609
610            Some(valid_block_tx)
611        } else {
612            None
613        }
614    }
615}
616
617impl<R> Drop for CacheTaskHandle<R> {
618    fn drop(&mut self) {
619        // Ensure we always terminate on drop - send None without needing Send + Sync bounds
620        if let Some(tx) = self.to_prewarm_task.take() {
621            let _ = tx.send(PrewarmTaskEvent::Terminate {
622                execution_outcome: None,
623                valid_block_rx: mpsc::channel().1,
624            });
625        }
626    }
627}
628
629#[cfg(test)]
630mod tests {
631    use crate::tree::{
632        payload_processor::PayloadProcessor, precompile_cache::PrecompileCacheMap, ExecutionCache,
633        PayloadExecutionCache, SavedCache, TreeConfig,
634    };
635    use alloy_consensus::constants::KECCAK_EMPTY;
636    use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
637    use alloy_primitives::{Address, B256, U256};
638    use reth_chainspec::ChainSpec;
639    use reth_evm_ethereum::EthEvmConfig;
640    use reth_execution_cache::CachedStatus;
641    use reth_revm::db::BundleState;
642    use revm::state::AccountInfo;
643    use std::sync::Arc;
644
645    fn make_saved_cache(hash: B256) -> SavedCache {
646        let execution_cache = ExecutionCache::new(1_000);
647        SavedCache::new(hash, execution_cache)
648    }
649
650    #[test]
651    fn execution_cache_allows_single_checkout() {
652        let execution_cache = PayloadExecutionCache::default();
653        let hash = B256::from([1u8; 32]);
654
655        execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
656
657        let first = execution_cache.get_cache_for(hash);
658        assert!(first.is_some(), "expected initial checkout to succeed");
659
660        let second = execution_cache.get_cache_for(hash);
661        assert!(second.is_none(), "second checkout should be blocked while guard is active");
662
663        drop(first);
664
665        let third = execution_cache.get_cache_for(hash);
666        assert!(third.is_some(), "third checkout should succeed after guard is dropped");
667    }
668
669    #[test]
670    fn execution_cache_checkout_releases_on_drop() {
671        let execution_cache = PayloadExecutionCache::default();
672        let hash = B256::from([2u8; 32]);
673
674        execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
675
676        {
677            let guard = execution_cache.get_cache_for(hash);
678            assert!(guard.is_some(), "expected checkout to succeed");
679            // Guard dropped at end of scope
680        }
681
682        let retry = execution_cache.get_cache_for(hash);
683        assert!(retry.is_some(), "checkout should succeed after guard drop");
684    }
685
686    #[test]
687    fn execution_cache_mismatch_parent_clears_and_returns() {
688        let execution_cache = PayloadExecutionCache::default();
689        let hash = B256::from([3u8; 32]);
690
691        execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
692
693        // When the parent hash doesn't match (fork block), the cache is cleared,
694        // hash updated on the original, and clone returned for reuse
695        let different_hash = B256::from([4u8; 32]);
696        let cache = execution_cache.get_cache_for(different_hash);
697        assert!(cache.is_some(), "cache should be returned for reuse after clearing");
698
699        drop(cache);
700
701        // The stored cache now has the fork block's parent hash.
702        // Canonical chain looking for original hash sees a mismatch → clears and reuses.
703        let original = execution_cache.get_cache_for(hash);
704        assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
705    }
706
707    #[test]
708    fn execution_cache_update_after_release_succeeds() {
709        let execution_cache = PayloadExecutionCache::default();
710        let initial = B256::from([5u8; 32]);
711
712        execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(initial)));
713
714        let guard =
715            execution_cache.get_cache_for(initial).expect("expected initial checkout to succeed");
716
717        drop(guard);
718
719        let updated = B256::from([6u8; 32]);
720        execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(updated)));
721
722        let new_checkout = execution_cache.get_cache_for(updated);
723        assert!(new_checkout.is_some(), "new checkout should succeed after release and update");
724    }
725
726    #[test]
727    fn on_inserted_executed_block_populates_cache() {
728        let payload_processor = PayloadProcessor::new(
729            reth_tasks::Runtime::test(),
730            EthEvmConfig::new(Arc::new(ChainSpec::default())),
731            &TreeConfig::default(),
732            PrecompileCacheMap::default(),
733        );
734
735        let parent_hash = B256::from([1u8; 32]);
736        let block_hash = B256::from([10u8; 32]);
737        let block_with_parent = BlockWithParent {
738            block: BlockNumHash { hash: block_hash, number: 1 },
739            parent: parent_hash,
740        };
741        let bundle_state = BundleState::default();
742
743        // Cache should be empty initially
744        assert!(payload_processor.execution_cache.get_cache_for(block_hash).is_none());
745
746        // Update cache with inserted block
747        payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
748
749        // Cache should now exist for the block hash
750        let cached = payload_processor.execution_cache.get_cache_for(block_hash);
751        assert!(cached.is_some());
752        assert_eq!(cached.unwrap().executed_block_hash(), block_hash);
753    }
754
755    #[test]
756    fn on_inserted_executed_block_skips_on_parent_mismatch() {
757        let payload_processor = PayloadProcessor::new(
758            reth_tasks::Runtime::test(),
759            EthEvmConfig::new(Arc::new(ChainSpec::default())),
760            &TreeConfig::default(),
761            PrecompileCacheMap::default(),
762        );
763
764        // Setup: populate cache with block 1
765        let block1_hash = B256::from([1u8; 32]);
766        payload_processor
767            .execution_cache
768            .update_with_guard(|slot| *slot = Some(make_saved_cache(block1_hash)));
769
770        // Try to insert block 3 with wrong parent (should skip and keep block 1's cache)
771        let wrong_parent = B256::from([99u8; 32]);
772        let block3_hash = B256::from([3u8; 32]);
773        let block_with_parent = BlockWithParent {
774            block: BlockNumHash { hash: block3_hash, number: 3 },
775            parent: wrong_parent,
776        };
777        let bundle_state = BundleState::default();
778
779        payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
780
781        // Cache should still be for block 1 (unchanged)
782        let cached = payload_processor.execution_cache.get_cache_for(block1_hash);
783        assert!(cached.is_some(), "Original cache should be preserved");
784
785        // Cache for block 3 should not exist
786        let cached3 = payload_processor.execution_cache.get_cache_for(block3_hash);
787        assert!(cached3.is_none(), "New block cache should not be created on mismatch");
788    }
789
790    #[test]
791    fn on_inserted_executed_block_does_not_mutate_checked_out_parent_cache() {
792        let payload_processor = PayloadProcessor::new(
793            reth_tasks::Runtime::test(),
794            EthEvmConfig::new(Arc::new(ChainSpec::default())),
795            &TreeConfig::default(),
796            PrecompileCacheMap::default(),
797        );
798
799        let parent_hash = B256::from([1u8; 32]);
800        payload_processor
801            .execution_cache
802            .update_with_guard(|slot| *slot = Some(make_saved_cache(parent_hash)));
803
804        // Checking out the cache bumps its `ExecutionCache` refcount, marking the slot as in-use.
805        // The returned SavedCache shares the same underlying ExecutionCache Arc as the slot,
806        // so any writes through the slot are observable here
807        let checked_out = payload_processor
808            .execution_cache
809            .get_cache_for(parent_hash)
810            .expect("expected parent cache checkout to succeed");
811
812        let polluted_address = Address::random();
813        let bundle_state = BundleState::builder(2..=2)
814            .state_present_account_info(
815                polluted_address,
816                AccountInfo {
817                    balance: U256::from(1337),
818                    nonce: 7,
819                    code_hash: KECCAK_EMPTY,
820                    code: None,
821                    account_id: None,
822                },
823            )
824            .build();
825
826        // Make parent match the cached slot so we bypass the parent-mismatch guard and exercise
827        // the in-use guard specifically.
828        let block_with_parent = BlockWithParent {
829            block: BlockNumHash { hash: B256::from([2u8; 32]), number: 2 },
830            parent: parent_hash,
831        };
832
833        payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
834
835        // The closure runs only on a cache miss, so NotCached(None) means polluted_address was
836        // absent and Cached(Some(_)) means it was written by on_inserted_executed_block.
837        let account = checked_out
838            .cache()
839            .get_or_try_insert_account_with(polluted_address, || Ok::<_, ()>(None))
840            .expect("cache read should succeed");
841
842        assert_eq!(
843            account,
844            CachedStatus::NotCached(None),
845            "checked-out parent cache should not observe state from inserted local block"
846        );
847    }
848
849    /// Tests the full prewarm lifecycle for a fork block:
850    ///
851    /// 1. Cache is at canonical block 4.
852    /// 2. Fork block (parent = block 2) checks out the cache via `get_cache_for`, simulating what
853    ///    `PrewarmCacheTask` does when it receives a `SavedCache`.
854    /// 3. Prewarm populates the shared cache with fork-specific state.
855    /// 4. While the prewarm clone is alive, the cache is unavailable (`usage_count` > 1).
856    /// 5. Prewarm drops without calling `save_cache` (fork block was invalid).
857    /// 6. Canonical block 5 (parent = block 4) must get a cache with correct hash and no stale fork
858    ///    data.
859    #[test]
860    fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
861        let execution_cache = PayloadExecutionCache::default();
862
863        // Canonical chain at block 4.
864        let block4_hash = B256::from([4u8; 32]);
865        execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
866
867        // Fork block arrives with parent = block 2. Prewarm task checks out the cache.
868        // This simulates PrewarmCacheTask receiving a SavedCache clone from get_cache_for.
869        let fork_parent = B256::from([2u8; 32]);
870        let prewarm_cache = execution_cache.get_cache_for(fork_parent);
871        assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
872        let prewarm_cache = prewarm_cache.unwrap();
873        assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
874
875        // Prewarm populates cache with fork-specific state (ancestor data for block 2).
876        // Since ExecutionCache uses Arc<Inner>, this data is shared with the stored original.
877        let fork_addr = Address::from([0xBB; 20]);
878        let fork_key = B256::from([0xCC; 32]);
879        prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
880
881        // While prewarm holds the clone, the cache handle count > 1 so the cache is in use.
882        let during_prewarm = execution_cache.get_cache_for(block4_hash);
883        assert!(
884            during_prewarm.is_none(),
885            "cache must be unavailable while prewarm holds a reference"
886        );
887
888        // Fork block fails — prewarm task drops without calling save_cache/update_with_guard.
889        drop(prewarm_cache);
890
891        // Canonical block 5 arrives (parent = block 4).
892        // Stored hash = fork_parent (our fix), so get_cache_for sees a mismatch,
893        // clears the stale fork data, and returns a cache with hash = block4_hash.
894        let block5_cache = execution_cache.get_cache_for(block4_hash);
895        assert!(
896            block5_cache.is_some(),
897            "canonical chain must get cache after fork prewarm is dropped"
898        );
899        assert_eq!(
900            block5_cache.as_ref().unwrap().executed_block_hash(),
901            block4_hash,
902            "cache must carry the canonical parent hash, not the fork parent"
903        );
904    }
905}