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