Skip to main content

reth_engine_tree/tree/payload_processor/
mod.rs

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