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