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