Skip to main content

reth_engine_tree/tree/payload_processor/
prewarm.rs

1//! Caching and prewarming related functionality.
2//!
3//! Prewarming executes transactions in parallel before the actual block execution
4//! to populate the execution cache with state that will likely be accessed during
5//! block processing.
6//!
7//! ## How Prewarming Works
8//!
9//! 1. Incoming transactions are split into two streams: one for prewarming (executed in parallel)
10//!    and one for actual execution (executed sequentially)
11//! 2. Prewarming tasks execute transactions in parallel using shared caches
12//! 3. When actual block execution happens, it benefits from the warmed cache
13//! 4. Block execution commits prewarm results whose reads are still valid instead of executing the
14//!    transactions again
15
16use super::{
17    bal_prewarm_pool::BalPrewarmPool,
18    reuse::{BalanceTracker, PoolTx, PrewarmResults, RecordingDb, SpeculativeTx},
19    StateRootHintStream, StateRootUpdateStream,
20};
21use crate::tree::{
22    precompile_cache::{CachedPrecompile, PrecompileCacheMap},
23    CachedStateCacheMetrics, CachedStateMetrics, CachedStateProvider, ExecutionEnv,
24    PayloadExecutionCache, SavedCache,
25};
26use alloy_consensus::transaction::TxHashRef;
27use alloy_eip7928::bal::DecodedBal;
28use alloy_eips::eip4895::Withdrawal;
29use alloy_primitives::keccak256;
30use metrics::{Counter, Gauge, Histogram};
31use rayon::prelude::*;
32use reth_evm::{
33    execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, HaltReasonFor, RecoveredTx, SpecFor,
34};
35use reth_metrics::Metrics;
36use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
37use reth_provider::{
38    BlockExecutionOutput, BlockNumReader, ChangeSetReader, DatabaseProviderFactory,
39    DatabaseProviderROFactory, EvmStateProvider, EvmStateProviderBox, HistoryReader,
40    PruneCheckpointReader, StageCheckpointReader, StateProvider, StorageChangeSetReader,
41    StorageSettingsCache,
42};
43use reth_revm::database::StateProviderDatabase;
44use reth_storage_overlay::OverlayStateProviderFactory;
45use reth_tasks::{pool::WorkerPool, Runtime};
46use reth_trie_common::MultiProofTargetsV2;
47use revm::{context::Transaction as _, state::EvmState};
48use std::{
49    sync::{
50        atomic::{AtomicBool, AtomicUsize, Ordering},
51        mpsc::{self, channel, Receiver, Sender},
52        Arc,
53    },
54    time::Duration,
55};
56use tokio::sync::oneshot;
57use tracing::{debug, debug_span, instrument, trace, trace_span, warn, Span};
58
59/// Maximum number of consecutive rounds that execute stale prewarm results again.
60const MAX_REFRESH_ROUNDS: usize = 4;
61
62/// Number of transactions ahead of block execution that refreshing checks after block execution
63/// published actual writes.
64const REFRESH_WINDOW: usize = 64;
65
66/// How often refreshing checks whether block execution published actual writes.
67const REFRESH_POLL_INTERVAL: Duration = Duration::from_micros(50);
68
69/// Determines the prewarming mode: transaction-based, BAL-based, or skipped.
70///
71/// Each variant carries the state-root capability its producers use, so the capability dies
72/// with the workers instead of outliving them.
73#[derive(Debug)]
74pub enum PrewarmMode<Tx> {
75    /// Prewarm by executing transactions from a stream, each paired with its block index.
76    Transactions {
77        /// Stream of transactions pending prewarm execution.
78        pending: Receiver<(usize, Tx)>,
79        /// Best-effort access hints emitted by the prewarm workers.
80        hints: Option<StateRootHintStream>,
81    },
82    /// Prewarm by prefetching slots from a Block Access List.
83    BlockAccessList {
84        /// The decoded block access list.
85        bal: Arc<DecodedBal>,
86        /// Authoritative pre-hashed updates derived from the BAL.
87        updates: Option<StateRootUpdateStream>,
88    },
89    /// Transaction prewarming is skipped (e.g. small blocks where the overhead exceeds the
90    /// benefit). No workers are spawned.
91    Skipped,
92}
93
94/// A task that is responsible for caching and prewarming the cache by executing transactions
95/// individually in parallel.
96///
97/// Note: This task runs until cancelled externally.
98#[derive(Debug)]
99pub struct PrewarmCacheTask<N, P, Evm>
100where
101    N: NodePrimitives,
102    Evm: ConfigureEvm<Primitives = N>,
103{
104    /// The executor used to spawn execution tasks.
105    executor: Runtime,
106    /// Shared execution cache.
107    execution_cache: PayloadExecutionCache,
108    /// Context provided to execution tasks
109    ctx: PrewarmContext<N, P, Evm>,
110    /// Receiver for events produced by tx execution
111    actions_rx: Receiver<PrewarmTaskEvent<N::Receipt>>,
112    /// Parent span for tracing
113    parent_span: Span,
114}
115
116impl<N, P, Evm> PrewarmCacheTask<N, P, Evm>
117where
118    N: NodePrimitives,
119    P: DatabaseProviderFactory + Clone + 'static,
120    P::Provider: BlockNumReader
121        + PruneCheckpointReader
122        + StageCheckpointReader
123        + ChangeSetReader
124        + StorageChangeSetReader
125        + StorageSettingsCache
126        + HistoryReader
127        + 'static,
128    Evm: ConfigureEvm<Primitives = N> + 'static,
129{
130    /// Initializes the task with the given transactions pending execution
131    pub fn new(
132        executor: Runtime,
133        execution_cache: PayloadExecutionCache,
134        ctx: PrewarmContext<N, P, Evm>,
135    ) -> (Self, Sender<PrewarmTaskEvent<N::Receipt>>) {
136        let (actions_tx, actions_rx) = channel();
137
138        trace!(
139            target: "engine::tree::payload_processor::prewarm",
140            prewarming_threads = executor.prewarming_pool().current_num_threads(),
141            transaction_count = ctx.env.transaction_count,
142            "Initialized prewarm task"
143        );
144
145        (
146            Self { executor, execution_cache, ctx, actions_rx, parent_span: Span::current() },
147            actions_tx,
148        )
149    }
150
151    /// Streams pending transactions and executes them in parallel on the prewarming pool.
152    ///
153    /// Kicks off EVM init on every pool thread, then uses `in_place_scope` to dispatch
154    /// transactions as they arrive and wait for all spawned tasks to complete before
155    /// clearing per-thread state. Workers that start via work-stealing lazily initialise
156    /// their EVM state on first access via [`get_or_init`](reth_tasks::pool::Worker::get_or_init).
157    fn spawn_txs_prewarm<Tx>(
158        &self,
159        pending: mpsc::Receiver<(usize, Tx)>,
160        actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
161        state_root_hint_stream: Option<StateRootHintStream>,
162    ) where
163        Tx: ExecutableTxFor<Evm> + Clone + Send + Sync + 'static,
164    {
165        let executor = self.executor.clone();
166        let ctx = self.ctx.clone();
167        let span = Span::current();
168
169        self.executor.spawn_blocking_named("prewarm-txs", move || {
170            let _enter = debug_span!(
171                target: "engine::tree::payload_processor::prewarm",
172                parent: &span,
173                "prewarm_txs"
174            )
175            .entered();
176
177            let ctx = &ctx;
178            let pool = executor.prewarming_pool();
179
180            let mut tx_count = 0usize;
181            let state_root_hint_stream = state_root_hint_stream.as_ref();
182            // Transactions to execute again when their results become stale.
183            let mut refresh_txs = Vec::new();
184            pool.in_place_scope(|s| {
185                s.spawn(|_| {
186                    pool.init::<PrewarmEvmState<Evm>>(|_| ctx.evm_for_ctx());
187                });
188
189                while let Ok((index, tx)) = pending.recv() {
190                    if ctx.should_stop() {
191                        trace!(
192                            target: "engine::tree::payload_processor::prewarm",
193                            "Termination requested, stopping transaction distribution"
194                        );
195                        break;
196                    }
197
198                    // skip transactions already executed by the main loop
199                    if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
200                        continue;
201                    }
202
203                    tx_count += 1;
204                    if ctx.prewarm_results.is_some() {
205                        refresh_txs.push((index, tx.clone()));
206                    }
207                    // Seed results before queueing, so that they are ready as soon as possible.
208                    let seeded = if let Some(results) = &ctx.prewarm_results &&
209                        let Some(seeds) = &ctx.env.pool_seeds
210                    {
211                        results.seed(index, tx.tx().tx_hash(), seeds)
212                    } else {
213                        None
214                    };
215                    if seeded.is_some() {
216                        ctx.metrics.seeded_transactions.increment(1);
217                    }
218                    let parent_span = Span::current();
219                    s.spawn(move |_| {
220                        let _enter = trace_span!(
221                            target: "engine::tree::payload_processor::prewarm",
222                            parent: parent_span,
223                            "prewarm_tx",
224                            i = index,
225                        )
226                        .entered();
227                        Self::transact_worker(ctx, index, tx, seeded, state_root_hint_stream);
228                    });
229                }
230
231                // Send withdrawal prefetch targets after all transactions dispatched
232                if let Some(state_root_hint_stream) = state_root_hint_stream &&
233                    let Some(withdrawals) = &ctx.env.withdrawals &&
234                    !withdrawals.is_empty()
235                {
236                    let targets = multiproof_targets_from_withdrawals(withdrawals);
237                    state_root_hint_stream.on_access_hint(targets.into());
238                }
239            });
240
241            if let Some(results) = &ctx.prewarm_results {
242                Self::refresh_results(ctx, pool, results, &refresh_txs);
243            }
244
245            // All tasks are done — clear per-thread EVM state for the next block.
246            pool.clear();
247
248            let _ = actions_tx
249                .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: tx_count });
250        });
251    }
252
253    /// Executes transactions again whose results became stale, because the predicted writes of
254    /// earlier transactions changed after they were executed.
255    ///
256    /// Each round checks the transactions that block execution has not reached yet, in block
257    /// order, on all pool threads. Rounds repeat while they find stale results, then wait for
258    /// block execution to publish actual writes that differ from the predicted ones, until
259    /// prewarming stops.
260    fn refresh_results<Tx>(
261        ctx: &PrewarmContext<N, P, Evm>,
262        pool: &WorkerPool,
263        results: &PrewarmResults<HaltReasonFor<Evm>>,
264        txs: &[(usize, Tx)],
265    ) where
266        Tx: ExecutableTxFor<Evm> + Clone + Send + Sync,
267    {
268        let mut rounds = 0;
269        // Rounds after block execution published writes only check the transactions it reaches
270        // soon, the others are checked in later rounds.
271        let mut window_len = usize::MAX;
272        while !ctx.should_stop() {
273            let generation = results.predicted().generation();
274            // Skip the transaction block execution is executing and the one it reaches next.
275            let next = ctx.executed_tx_index.load(Ordering::Relaxed) + 1;
276            let start = txs.partition_point(|(index, _)| *index <= next);
277            if start == txs.len() {
278                break
279            }
280            let window = &txs[start..txs.len().min(start.saturating_add(window_len))];
281            // Checking is cheap, so only stale transactions are handed to the pool.
282            let stale = window
283                .iter()
284                .filter(|(index, _)| results.needs_refresh(*index))
285                .collect::<Vec<_>>();
286            let refreshed = !stale.is_empty();
287            if refreshed {
288                let cursor = AtomicUsize::new(0);
289                pool.in_place_scope(|s| {
290                    for _ in 0..pool.current_num_threads().min(stale.len()) {
291                        s.spawn(|_| {
292                            while let Some((index, tx)) =
293                                stale.get(cursor.fetch_add(1, Ordering::Relaxed))
294                            {
295                                if ctx.should_stop() {
296                                    break
297                                }
298                                Self::transact_worker(ctx, *index, tx.clone(), None, None);
299                            }
300                        });
301                    }
302                });
303            }
304
305            rounds += 1;
306            if refreshed && rounds < MAX_REFRESH_ROUNDS {
307                continue
308            }
309            rounds = 0;
310            window_len = REFRESH_WINDOW;
311            while results.predicted().generation() == generation && !ctx.should_stop() {
312                std::thread::sleep(REFRESH_POLL_INTERVAL);
313            }
314        }
315    }
316
317    /// Executes a single prewarm transaction on the current pool thread's EVM, or only loads the
318    /// reads of its `seeded` pool transaction result.
319    ///
320    /// Lazily initialises per-thread [`PrewarmEvmState`] via
321    /// [`get_or_init`](reth_tasks::pool::Worker::get_or_init) on first access.
322    fn transact_worker<Tx>(
323        ctx: &PrewarmContext<N, P, Evm>,
324        index: usize,
325        tx: Tx,
326        seeded: Option<Arc<PoolTx<HaltReasonFor<Evm>>>>,
327        state_root_hint_stream: Option<&StateRootHintStream>,
328    ) where
329        Tx: ExecutableTxFor<Evm>,
330    {
331        WorkerPool::with_worker_mut(|worker| {
332            if ctx.should_stop() {
333                return;
334            }
335
336            // skip if main execution has already processed this transaction
337            if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
338                return;
339            }
340
341            let state = worker.get_or_init::<PrewarmEvmState<Evm>>(|| ctx.evm_for_ctx());
342            // The pool is shared across blocks, so this thread may still hold the EVM of a
343            // previous prewarm run that has not finished yet. Runs of payloads that claim the same
344            // block hash may use different block environments, so the hash cannot tell them apart.
345            if !Arc::ptr_eq(&state.run, &ctx.terminate_execution) {
346                *state = ctx.evm_for_ctx();
347            }
348            let Some(evm) = state.evm.as_mut() else { return };
349
350            let start = Instant::now();
351
352            let (tx_env, tx) = tx.into_parts();
353            let send_hints = |state: &EvmState| {
354                if index > 0 &&
355                    let Some(state_root_hint_stream) = state_root_hint_stream
356                {
357                    let (targets, storage_targets) = MultiProofTargetsV2::from_state(state);
358                    ctx.metrics.prefetch_storage_targets.record(storage_targets as f64);
359                    state_root_hint_stream.on_access_hint(targets.into());
360                }
361            };
362
363            if let Some(seeded) = seeded {
364                // Block execution loads the reads to validate the result.
365                if let Err(err) = seeded.load_reads(evm.db_mut()) {
366                    trace!(
367                        target: "engine::tree::payload_processor::prewarm",
368                        %err,
369                        tx_hash=%tx.tx().tx_hash(),
370                        "Failed to load reads of seeded pool transaction",
371                    );
372                }
373                send_hints(seeded.state());
374                return;
375            }
376
377            // Prewarming disables nonce and balance checks, so a reusable result must also show
378            // that the caller would have passed them.
379            let speculative = SpeculativeTx {
380                caller: *tx.signer(),
381                nonce: tx_env.nonce(),
382                max_balance_spending: tx_env.max_balance_spending(),
383            };
384            if let Some(results) = &ctx.prewarm_results {
385                evm.db_mut().start_recording(Arc::clone(results.predicted()), index);
386                evm.inspector_mut().clear();
387            }
388            let res = evm.transact(tx_env);
389            let reads = evm.db_mut().take_reads();
390            let res = match res {
391                Ok(res) => res,
392                Err(err) => {
393                    trace!(
394                        target: "engine::tree::payload_processor::prewarm",
395                        %err,
396                        tx_hash=%tx.tx().tx_hash(),
397                        sender=%tx.signer(),
398                        "Error when executing prewarm transaction",
399                    );
400                    ctx.metrics.transaction_errors.increment(1);
401                    if let Some(results) = &ctx.prewarm_results &&
402                        let Some(reads) = reads
403                    {
404                        results.reject(index, reads);
405                    }
406                    return;
407                }
408            };
409            ctx.metrics.execution_duration.record(start.elapsed());
410
411            if ctx.should_stop() {
412                return;
413            }
414
415            send_hints(&res.state);
416
417            if let Some(results) = &ctx.prewarm_results &&
418                let Some(reads) = reads &&
419                index >= ctx.executed_tx_index.load(Ordering::Relaxed)
420            {
421                results.store(index, res, reads, evm.inspector_mut(), speculative);
422            }
423
424            ctx.metrics.total_runtime.record(start.elapsed());
425        });
426    }
427
428    /// Saves the warmed cache in `self.execution_cache` after prewarming completes.
429    ///
430    /// This method calls [`PayloadExecutionCache::update_with_guard`], which requires exclusive
431    /// access. It should only be called after ensuring that:
432    /// 1. All prewarming tasks have completed execution
433    /// 2. No other concurrent operations are accessing the cache
434    ///
435    /// This moves the task's `ExecutionCache` into `self.execution_cache` when the block is valid,
436    /// without retaining an extra Arc reference that would prevent reuse after unlocking.
437    /// This method is called from `run()` only after all execution tasks are complete.
438    ///
439    /// State insertion and block validation run under the mutex because the cache being updated
440    /// may also be stored in `self.execution_cache`. Removed `SavedCache` values are dropped after
441    /// unlocking, before the next prewarm task. Their contents are freed only when the last
442    /// `ExecutionCache` clone is dropped, which may happen later on another thread.
443    #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
444    fn save_cache(
445        self,
446        execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
447        valid_block_rx: mpsc::Receiver<()>,
448    ) {
449        let start = Instant::now();
450
451        let Self {
452            execution_cache,
453            ctx: PrewarmContext { env, metrics, cache_state_metrics, saved_cache, .. },
454            ..
455        } = self;
456        let hash = env.hash;
457
458        if let Some(saved_cache) = saved_cache {
459            debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
460            let (previous, rejected) = execution_cache.update_with_guard(|cached| {
461                let new_cache = SavedCache::new(hash, saved_cache.into_cache());
462
463                // Update under the mutex so no checkout can observe partially updated state.
464                if new_cache.cache().insert_state(&execution_outcome.state).is_err() {
465                    debug!(target: "engine::caching", "cleared execution cache on update error");
466                    return (cached.take(), Some(new_cache));
467                }
468
469                new_cache.update_metrics(cache_state_metrics.as_ref());
470
471                // `cached` and `new_cache` can point to the same cache.
472                // insert_state has already applied this block's changes to it, so keep the mutex
473                // locked until validation succeeds or we clear `cached` on failure.
474                if valid_block_rx.recv().is_err() {
475                    debug!(target: "engine::caching", "cleared execution cache on invalid block");
476                    return (cached.take(), Some(new_cache));
477                }
478
479                let reused =
480                    cached.as_ref().is_some_and(|previous| previous.shares_cache_with(&new_cache));
481                let previous = cached.replace(new_cache);
482                if reused {
483                    // `cached` and `previous` hold the same cache. Drop the extra Arc reference
484                    // before unlocking so get_cache_for can reuse it.
485                    drop(previous);
486                    (None, None)
487                } else {
488                    // Drop the old cache after unlocking; freeing it may be expensive.
489                    (previous, None)
490                }
491            });
492
493            // Drop these SavedCache values on this worker, without a background drop queue.
494            // This frees their contents only if no other ExecutionCache clones remain.
495            // Otherwise, the thread dropping the last ExecutionCache clone frees them later.
496            // Another payload may access or allocate a cache while these drops run.
497            drop((previous, rejected));
498
499            let elapsed = start.elapsed();
500            debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
501
502            metrics.cache_saving_duration.set(elapsed.as_secs_f64());
503        }
504    }
505
506    /// Runs BAL-based prewarming and state-root streaming inline.
507    ///
508    /// Spawns two halves concurrently on separate pools, then waits for both to complete:
509    /// 1. Hashed state streaming on the BAL streaming pool so storage updates can reach the
510    ///    state-root job before account reads finish.
511    /// 2. Storage prefetch on the prewarming pool to populate the execution cache, unless BAL batch
512    ///    I/O is disabled.
513    #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
514    fn run_bal_prewarm(
515        &self,
516        decoded_bal: Arc<DecodedBal>,
517        actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
518        hashed_update_stream: Option<StateRootUpdateStream>,
519    ) {
520        let bal = decoded_bal.as_bal();
521        if bal.is_empty() {
522            if let Some(hashed_update_stream) = hashed_update_stream {
523                hashed_update_stream.finish();
524            }
525            let _ =
526                actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
527            return;
528        }
529
530        trace!(
531            target: "engine::tree::payload_processor::prewarm",
532            accounts = bal.len(),
533            "Starting BAL prewarm"
534        );
535
536        let ctx = self.ctx.clone();
537        let executor = self.executor.clone();
538        let parent_span = Span::current();
539        let stream_parent_span = parent_span;
540        let prefetch_bal = Arc::clone(&decoded_bal);
541        let stream_bal = Arc::clone(&decoded_bal);
542        let (stream_tx, stream_rx) = oneshot::channel();
543
544        if let Some(hashed_update_stream) = hashed_update_stream {
545            let ctx = ctx.clone();
546            executor.bal_streaming_pool().spawn(move || {
547                let branch_span = debug_span!(
548                    target: "engine::tree::payload_processor::prewarm",
549                    parent: &stream_parent_span,
550                    "bal_hashed_state_stream",
551                    bal_accounts = stream_bal.as_bal().len(),
552                );
553                let parent_span = branch_span.clone();
554                let _span = branch_span.entered();
555
556                stream_bal.as_bal().par_iter().for_each(|account_changes| {
557                    WorkerPool::with_worker_mut(|worker| {
558                        let provider = worker.get_or_init::<Option<EvmStateProviderBox>>(|| None);
559                        ctx.send_bal_hashed_state(
560                            &parent_span,
561                            provider,
562                            account_changes,
563                            &hashed_update_stream,
564                        );
565                    });
566                });
567
568                hashed_update_stream.finish();
569                let _ = stream_tx.send(());
570            });
571        } else {
572            let _ = stream_tx.send(());
573        }
574
575        if let Some(saved_cache) = ctx.saved_cache &&
576            !ctx.disable_bal_batch_io &&
577            let Some(pool) = ctx.bal_prewarm_pool.as_ref()
578        {
579            // If
580            //
581            // - BAL path is enabled (and so bal_prewarm_pool is present),
582            // - dispatch_bal_batch_io is false
583            // - execution cache is not disabled
584            //
585            // we launch prewarming of the BAL accounts and declared storages (both storage_reads
586            // and storage_changes). Bytecode is loaded on demand during execution.
587            //
588            // This runs side-by-side with the parallel transaction execution reducing the time it
589            // spends blocking on the data.
590            let caches = saved_cache.cache().clone();
591            let state_provider_factory = ctx.provider.clone();
592            let build = Arc::new(move || {
593                state_provider_factory.database_provider_ro().map(|provider| {
594                    Box::new(provider.into_evm_state_provider()) as EvmStateProviderBox
595                })
596            });
597
598            pool.begin_block(build, caches, ctx.env.txpool_snapshot.clone());
599            let dispatch_start = Instant::now();
600            for account in prefetch_bal.as_bal() {
601                pool.warm_account(account.address, account.storage_slots().map(Into::into));
602            }
603            ctx.metrics.bal_slot_iteration_duration.record(dispatch_start.elapsed());
604            pool.end_block();
605        }
606
607        stream_rx
608            .blocking_recv()
609            .expect("BAL hashed-state streaming task dropped without signaling completion");
610
611        // Drop the per-thread providers
612        executor.bal_streaming_pool().clear();
613
614        let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
615    }
616
617    /// Executes the task.
618    ///
619    /// This will execute the transactions until all transactions have been processed or the task
620    /// was cancelled.
621    #[instrument(
622        parent = &self.parent_span,
623        level = "debug",
624        target = "engine::tree::payload_processor::prewarm",
625        name = "prewarm and caching",
626        skip_all
627    )]
628    pub fn run<Tx>(self, mode: PrewarmMode<Tx>, actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>)
629    where
630        Tx: ExecutableTxFor<Evm> + Clone + Send + Sync + 'static,
631    {
632        // Spawn execution tasks based on mode. The state-root capabilities arrive inside the
633        // mode and move into the spawned producers, so they die with the producers instead of
634        // living for the full lifetime of this task.
635        match mode {
636            PrewarmMode::Transactions { pending, hints } => {
637                self.spawn_txs_prewarm(pending, actions_tx, hints);
638            }
639            PrewarmMode::BlockAccessList { bal, updates } => {
640                self.run_bal_prewarm(bal, actions_tx, updates);
641            }
642            PrewarmMode::Skipped => {
643                let _ = actions_tx
644                    .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
645            }
646        }
647
648        let mut final_execution_outcome = None;
649        let mut finished_execution = false;
650        while let Ok(event) = self.actions_rx.recv() {
651            match event {
652                PrewarmTaskEvent::TerminateTransactionExecution => {
653                    // stop tx processing
654                    debug!(target: "engine::tree::prewarm", "Terminating prewarm execution");
655                    self.ctx.stop();
656                }
657                PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx } => {
658                    trace!(target: "engine::tree::payload_processor::prewarm", "Received termination signal");
659                    // `Terminate` can arrive without `TerminateTransactionExecution` when the
660                    // handle is dropped on an execution error, so stop workers before waiting.
661                    self.ctx.stop();
662                    final_execution_outcome =
663                        Some(execution_outcome.map(|outcome| (outcome, valid_block_rx)));
664
665                    if finished_execution {
666                        // all tasks are done, we can exit, which will save caches and exit
667                        break
668                    }
669                }
670                PrewarmTaskEvent::FinishedTxExecution { executed_transactions } => {
671                    trace!(target: "engine::tree::payload_processor::prewarm", "Finished prewarm execution signal");
672                    self.ctx.metrics.transactions.set(executed_transactions as f64);
673                    self.ctx.metrics.transactions_histogram.record(executed_transactions as f64);
674
675                    finished_execution = true;
676
677                    if final_execution_outcome.is_some() {
678                        // all tasks are done, we can exit, which will save caches and exit
679                        break
680                    }
681                }
682            }
683        }
684
685        debug!(target: "engine::tree::payload_processor::prewarm", "Completed prewarm execution");
686
687        // save caches and finish using the shared ExecutionOutcome
688        if let Some(Some((execution_outcome, valid_block_rx))) = final_execution_outcome {
689            self.save_cache(execution_outcome, valid_block_rx);
690        }
691    }
692}
693
694/// Context required by tx execution tasks.
695#[derive(Debug, Clone)]
696pub struct PrewarmContext<N, P, Evm>
697where
698    N: NodePrimitives,
699    Evm: ConfigureEvm<Primitives = N>,
700{
701    /// The execution environment.
702    pub env: ExecutionEnv<Evm>,
703    /// The EVM configuration.
704    pub evm_config: Evm,
705    /// The saved cache.
706    pub saved_cache: Option<SavedCache>,
707    /// Provider to obtain the state
708    pub provider: OverlayStateProviderFactory<P, N>,
709    /// Dedicated blocking pool for warming the BAL read-set. `Some` only on the BAL parallel
710    /// execution path; the pool is owned by the [`PayloadProcessor`](super::PayloadProcessor).
711    pub(crate) bal_prewarm_pool: Option<Arc<BalPrewarmPool>>,
712    /// The metrics for the prewarm task.
713    pub metrics: PrewarmMetrics,
714    /// Metrics for the execution cache.
715    /// Metrics for the execution cache. `None` disables metrics recording.
716    pub cache_metrics: Option<CachedStateMetrics>,
717    /// Metrics for shared execution cache state. `None` disables metrics recording.
718    pub cache_state_metrics: Option<CachedStateCacheMetrics>,
719    /// An atomic bool that tells prewarm tasks to not start any more execution.
720    pub terminate_execution: Arc<AtomicBool>,
721    /// Shared counter tracking the next transaction index to be executed by the main execution
722    /// loop. Prewarm workers skip transactions with `index < counter` since those have already
723    /// been executed.
724    pub executed_tx_index: Arc<AtomicUsize>,
725    /// Whether the precompile cache is disabled.
726    pub precompile_cache_disabled: bool,
727    /// The precompile cache map.
728    pub precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
729    /// Whether to disable BAL-driven parallel state root computation.
730    /// Only valid when BAL parallel execution is also disabled.
731    pub disable_bal_parallel_state_root: bool,
732    /// Whether BAL state prefetching during prewarm is disabled.
733    pub disable_bal_batch_io: bool,
734    /// Results of transaction prewarming that block execution may reuse, `None` if disabled.
735    pub(crate) prewarm_results: Option<Arc<PrewarmResults<HaltReasonFor<Evm>>>>,
736}
737
738/// Per-thread EVM state initialised by [`PrewarmContext::evm_for_ctx`] and stored in
739/// [`WorkerPool`] workers via [`Worker::get_or_init`](reth_tasks::pool::Worker::get_or_init).
740struct PrewarmEvmState<Evm: ConfigureEvm> {
741    /// Termination flag of the prewarm run that created the EVM, which identifies the run.
742    run: Arc<AtomicBool>,
743    /// The EVM, `None` if its state provider could not be created.
744    evm: Option<
745        EvmFor<Evm, RecordingDb<StateProviderDatabase<EvmStateProviderBox>>, BalanceTracker>,
746    >,
747}
748
749impl<N, P, Evm> PrewarmContext<N, P, Evm>
750where
751    N: NodePrimitives,
752    P: DatabaseProviderFactory,
753    P::Provider: BlockNumReader
754        + PruneCheckpointReader
755        + StageCheckpointReader
756        + ChangeSetReader
757        + StorageChangeSetReader
758        + StorageSettingsCache
759        + HistoryReader
760        + 'static,
761    Evm: ConfigureEvm<Primitives = N> + 'static,
762{
763    /// Creates a per-thread EVM for prewarming, tagged with the prewarm run it belongs to.
764    #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
765    fn evm_for_ctx(&self) -> PrewarmEvmState<Evm> {
766        let mut state_provider = match self.provider.database_provider_ro() {
767            Ok(provider) => Box::new(provider.into_evm_state_provider()) as EvmStateProviderBox,
768            Err(err) => {
769                trace!(
770                    target: "engine::tree::payload_processor::prewarm",
771                    %err,
772                    "Failed to build state provider in prewarm thread"
773                );
774                return PrewarmEvmState { run: Arc::clone(&self.terminate_execution), evm: None }
775            }
776        };
777
778        // Use the caches to create a new provider with caching
779        if let Some(saved_cache) = &self.saved_cache {
780            let caches = saved_cache.cache().clone();
781            state_provider = Box::new(
782                CachedStateProvider::new_prewarm(state_provider, caches)
783                    .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
784            );
785        }
786
787        let state_provider = StateProviderDatabase::new(state_provider);
788
789        let mut evm_env = self.env.evm_env.clone();
790
791        // we must disable the nonce check so that we can execute the transaction even if the nonce
792        // doesn't match what's on chain.
793        evm_env.cfg_env.disable_nonce_check = true;
794
795        // disable the balance check so that transactions from senders who were funded by earlier
796        // transactions in the block can still be prewarmed
797        evm_env.cfg_env.disable_balance_check = true;
798
799        // create a new executor and disable nonce checks in the env
800        let spec_id = *evm_env.spec_id();
801        // Balances only need to be tracked to record reads for result reuse, and JIT-compiled
802        // code would skip the inspector hooks that track them.
803        let reuse = self.prewarm_results.is_some();
804        let mut evm_config = self.evm_config.clone();
805        if reuse {
806            evm_config = evm_config.with_jit_support_enabled(false);
807        }
808        let mut evm = evm_config.evm_with_env_and_inspector(
809            RecordingDb::new(state_provider),
810            evm_env,
811            BalanceTracker::default(),
812        );
813        evm.set_inspector_enabled(reuse);
814
815        if !self.precompile_cache_disabled {
816            // Only cache pure precompiles to avoid issues with stateful precompiles
817            evm.precompiles_mut().map_cacheable_precompiles(|address, precompile| {
818                CachedPrecompile::wrap(
819                    precompile,
820                    self.precompile_cache_map.cache_for_address(*address),
821                    spec_id,
822                    None, // No metrics for prewarm
823                )
824            });
825        }
826
827        PrewarmEvmState { run: Arc::clone(&self.terminate_execution), evm: Some(evm) }
828    }
829
830    /// Returns `true` if prewarming should stop.
831    #[inline]
832    pub fn should_stop(&self) -> bool {
833        self.terminate_execution.load(Ordering::Relaxed)
834    }
835
836    /// Signals all prewarm tasks to stop execution.
837    #[inline]
838    pub fn stop(&self) {
839        self.terminate_execution.store(true, Ordering::Relaxed);
840    }
841
842    /// Hashes and streams a single BAL account's state to the state-root job's hashed-update
843    /// stream.
844    ///
845    /// For each changed account, storage slots are hashed and sent immediately, then the account
846    /// is sent as a separate update. The parent account is read only when the BAL did not provide
847    /// all account leaf fields needed for state-root computation.
848    ///
849    /// The `provider` is lazily initialized on first call and reused across accounts on the same
850    /// thread.
851    fn send_bal_hashed_state(
852        &self,
853        parent_span: &Span,
854        provider: &mut Option<EvmStateProviderBox>,
855        account_changes: &alloy_eip7928::AccountChanges,
856        hashed_update_stream: &StateRootUpdateStream,
857    ) {
858        if self.disable_bal_parallel_state_root {
859            return;
860        }
861        let address = account_changes.address;
862        let mut hashed_address = None;
863        let account_info = account_changes.account_info();
864
865        if !account_info.changes_state_root(account_changes) {
866            return;
867        }
868
869        // If there are any storage changes we can assume that the resulting account info will be
870        // non-empty, so the account will exist, and therefore we can pre-emptively send out storage
871        // changes to start processing them before potentially hitting the db in the next step.
872        if account_changes.has_storage_changes() {
873            let hashed_address = *hashed_address.get_or_insert_with(|| keccak256(address));
874            let storage_map = reth_trie::HashedStorage::from_iter(
875                account_changes
876                    .storage_post_states()
877                    .map(|(slot, value)| (keccak256(slot.to_be_bytes::<32>()), value)),
878            );
879
880            let mut hashed_state = reth_trie::HashedPostState::default();
881            hashed_state.storages.insert(hashed_address, storage_map);
882            hashed_update_stream.on_hashed_state_update(hashed_state);
883        }
884
885        let existing_account = if account_info.is_complete() {
886            None
887        } else {
888            if provider.is_none() {
889                let _span = debug_span!(
890                    target: "engine::tree::payload_processor::prewarm",
891                    parent: parent_span,
892                    "bal_hashed_state_provider_init",
893                    has_saved_cache = !self.disable_bal_batch_io && self.saved_cache.is_some(),
894                )
895                .entered();
896
897                let inner = match self.provider.database_provider_ro() {
898                    Ok(p) => p.into_evm_state_provider(),
899                    Err(err) => {
900                        warn!(
901                            target: "engine::tree::payload_processor::prewarm",
902                            ?err,
903                            "Failed to build provider for BAL account reads"
904                        );
905                        return;
906                    }
907                };
908                let boxed: EvmStateProviderBox =
909                    match (self.disable_bal_batch_io, &self.saved_cache) {
910                        (false, Some(saved)) => {
911                            let caches = saved.cache().clone();
912                            Box::new(
913                                CachedStateProvider::new_prewarm(inner, caches)
914                                    .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
915                            )
916                        }
917                        _ => Box::new(inner),
918                    };
919                *provider = Some(boxed);
920            }
921            let account_reader = provider.as_ref().expect("provider just initialized");
922            account_reader.basic_account(&address).ok().flatten()
923        };
924
925        let mut account = existing_account.unwrap_or_default();
926        account.apply_bal_info(account_info);
927        let hashed_address = hashed_address.unwrap_or_else(|| keccak256(address));
928
929        // It is possible for the resulting account info to be empty. This can happen when, in the
930        // same block:
931        // * tx1: A new account is funded
932        // * tx2: CREATE2 is called on the new account, SELFDESTRUCT is called within the init code
933        //
934        // In this case the account will have only balance_changes, one for funding and the second
935        // setting balance back to zero. The resulting account is fully empty, we mark it as None
936        // with no storage changes to indicate that it should be deleted if nothing else.
937        //
938        // We assume that if the account info is all zero then it can't have storage, so we don't
939        // have to explicitly check for empty storage.
940        let account = (!account.is_empty()).then_some(account);
941
942        let mut hashed_state = reth_trie::HashedPostState::default();
943        hashed_state.accounts.insert(hashed_address, account);
944        hashed_update_stream.on_hashed_state_update(hashed_state);
945    }
946}
947
948/// Returns [`MultiProofTargetsV2`] for withdrawal addresses.
949///
950/// Withdrawals only modify account balances (no storage), so the targets contain
951/// only account-level entries with empty storage sets.
952fn multiproof_targets_from_withdrawals(withdrawals: &[Withdrawal]) -> MultiProofTargetsV2 {
953    MultiProofTargetsV2 {
954        account_targets: withdrawals.iter().map(|w| keccak256(w.address).into()).collect(),
955        ..Default::default()
956    }
957}
958
959/// The events the pre-warm task can handle.
960///
961/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the main
962/// execution path without cloning the expensive `BundleState`.
963#[derive(Debug)]
964pub enum PrewarmTaskEvent<R> {
965    /// Signals the prewarm workers to stop executing further transactions.
966    ///
967    /// This only sets the termination flag the workers poll; the task keeps running to save the
968    /// cache. Sent once the authoritative execution no longer needs prewarming, so the workers do
969    /// not race ahead on transactions that will never be used.
970    TerminateTransactionExecution,
971    /// Tears the whole task down: stops the workers, optionally saves the warmed cache from the
972    /// final output, and exits.
973    ///
974    /// Sent when execution completed successfully (carrying the output to save) or when the task
975    /// handle is dropped (carrying no output, e.g. after an execution error). Handling this event
976    /// also stops the workers, since a teardown may arrive without a preceding
977    /// [`TerminateTransactionExecution`](Self::TerminateTransactionExecution).
978    Terminate {
979        /// The final execution outcome, or `None` when the task is torn down without one (e.g. a
980        /// dropped handle). Using `Arc` allows sharing with the main execution path without
981        /// cloning the expensive `BundleState`.
982        execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
983        /// Receiver for the block validation result.
984        ///
985        /// Cache saving is racing the state root validation. We optimistically construct the
986        /// updated cache but only save it once we know the block is valid.
987        valid_block_rx: mpsc::Receiver<()>,
988    },
989    /// Emitted by the worker-dispatch side once every dispatched transaction has finished or been
990    /// cancelled, reporting how many were executed.
991    FinishedTxExecution {
992        /// Number of transactions executed
993        executed_transactions: usize,
994    },
995}
996
997/// Metrics for transactions prewarming.
998#[derive(Metrics, Clone)]
999#[metrics(scope = "sync.prewarm")]
1000pub struct PrewarmMetrics {
1001    /// The number of transactions to prewarm
1002    pub(crate) transactions: Gauge,
1003    /// A histogram of the number of transactions to prewarm
1004    pub(crate) transactions_histogram: Histogram,
1005    /// A histogram of duration per transaction prewarming
1006    pub(crate) total_runtime: Histogram,
1007    /// A histogram of EVM execution duration per transaction prewarming
1008    pub(crate) execution_duration: Histogram,
1009    /// A histogram for prefetch targets per transaction prewarming
1010    pub(crate) prefetch_storage_targets: Histogram,
1011    /// Time spent in `save_cache`, including dropping its removed `SavedCache` values.
1012    /// Excludes any later freeing of cache contents by other `ExecutionCache` clones.
1013    pub(crate) cache_saving_duration: Gauge,
1014    /// Counter for transaction execution errors during prewarming
1015    pub(crate) transaction_errors: Counter,
1016    /// Counter for transactions seeded with results of pool transactions instead of being
1017    /// executed.
1018    pub(crate) seeded_transactions: Counter,
1019    /// A histogram of BAL slot iteration duration during prefetching
1020    pub(crate) bal_slot_iteration_duration: Histogram,
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025
1026    use super::*;
1027    use alloy_consensus::{transaction::Recovered, TxLegacy};
1028    use alloy_eips::eip7702::constants::EIP7702_CLEARED_DELEGATION;
1029    use alloy_primitives::{Address, Signature, TxKind, B256, U256};
1030    use reth_chainspec::ChainSpec;
1031    use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
1032    use reth_evm::{execute::WithTxEnv, TxEnvFor};
1033    use reth_evm_ethereum::EthEvmConfig;
1034    use reth_provider::test_utils::MockEthProvider;
1035    use reth_stages_api::{StageCheckpoint, StageId};
1036    use reth_storage_overlay::OverlayManager;
1037    use revm::database::EmptyDB;
1038
1039    #[test]
1040    fn terminate_event_stops_transaction_execution() {
1041        let terminate_execution = Arc::new(AtomicBool::new(false));
1042        let ctx = PrewarmContext {
1043            env: ExecutionEnv::test_default(),
1044            evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1045            saved_cache: None,
1046            provider: OverlayStateProviderFactory::new(
1047                MockEthProvider::default(),
1048                OverlayManager::default().overlay_builder(B256::ZERO),
1049            ),
1050            bal_prewarm_pool: None,
1051            metrics: PrewarmMetrics::default(),
1052            cache_metrics: None,
1053            cache_state_metrics: None,
1054            terminate_execution: Arc::clone(&terminate_execution),
1055            executed_tx_index: Arc::new(AtomicUsize::new(0)),
1056            precompile_cache_disabled: false,
1057            precompile_cache_map: PrecompileCacheMap::default(),
1058            disable_bal_parallel_state_root: false,
1059            disable_bal_batch_io: false,
1060            prewarm_results: None,
1061        };
1062        let (task, actions_tx) =
1063            PrewarmCacheTask::new(Runtime::test(), PayloadExecutionCache::default(), ctx);
1064        actions_tx
1065            .send(PrewarmTaskEvent::Terminate {
1066                execution_outcome: None,
1067                valid_block_rx: mpsc::channel().1,
1068            })
1069            .unwrap();
1070
1071        task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
1072            PrewarmMode::Skipped,
1073            actions_tx,
1074        );
1075
1076        assert!(terminate_execution.load(Ordering::Relaxed));
1077    }
1078
1079    #[test]
1080    fn rebuilds_evm_for_each_prewarm_run() {
1081        let caller = Address::repeat_byte(1);
1082        let provider = MockEthProvider::default();
1083        provider.enable_database_provider();
1084        provider.add_header(B256::ZERO, Default::default());
1085        provider.add_stage_checkpoint(StageId::Finish, StageCheckpoint::new(0));
1086        let tx = Recovered::new_unchecked(
1087            TransactionSigned::new_unhashed(
1088                // Free, so that the empty caller can pay for it.
1089                TxLegacy {
1090                    gas_limit: 21_000,
1091                    to: TxKind::Call(Address::repeat_byte(2)),
1092                    ..Default::default()
1093                }
1094                .into(),
1095                Signature::test_signature(),
1096            ),
1097            caller,
1098        );
1099        let pool = WorkerPool::new(1, "prewarm-test");
1100        // Payloads that claim the same block hash may still use different block environments.
1101        let prewarm = |beneficiary| {
1102            let mut env = ExecutionEnv::<EthEvmConfig>::test_default();
1103            env.evm_env.block_env.beneficiary = beneficiary;
1104            let results = Arc::new(PrewarmResults::new(1));
1105            let ctx = PrewarmContext {
1106                env,
1107                evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1108                saved_cache: None,
1109                provider: OverlayStateProviderFactory::new(
1110                    provider.clone(),
1111                    OverlayManager::default().overlay_builder(B256::ZERO),
1112                ),
1113                bal_prewarm_pool: None,
1114                metrics: PrewarmMetrics::default(),
1115                cache_metrics: None,
1116                cache_state_metrics: None,
1117                terminate_execution: Arc::new(AtomicBool::new(false)),
1118                executed_tx_index: Arc::new(AtomicUsize::new(0)),
1119                precompile_cache_disabled: false,
1120                precompile_cache_map: PrecompileCacheMap::default(),
1121                disable_bal_parallel_state_root: false,
1122                disable_bal_batch_io: false,
1123                prewarm_results: Some(Arc::clone(&results)),
1124            };
1125            let tx = WithTxEnv { tx_env: ctx.evm_config.tx_env(&tx), tx: Arc::new(tx.clone()) };
1126            pool.install_fn(|| {
1127                PrewarmCacheTask::<EthPrimitives, MockEthProvider, EthEvmConfig>::transact_worker(
1128                    &ctx, 0, tx, None, None,
1129                )
1130            });
1131            results.take_valid(0, &mut EmptyDB::default()).unwrap().unwrap().state
1132        };
1133
1134        assert!(prewarm(Address::repeat_byte(0xa)).contains_key(&Address::repeat_byte(0xa)));
1135        let state = prewarm(Address::repeat_byte(0xb));
1136        assert!(state.contains_key(&Address::repeat_byte(0xb)));
1137        assert!(!state.contains_key(&Address::repeat_byte(0xa)));
1138    }
1139
1140    fn test_prewarm_context(
1141        saved_cache: SavedCache,
1142        saving_duration: Gauge,
1143    ) -> PrewarmContext<EthPrimitives, MockEthProvider, EthEvmConfig> {
1144        PrewarmContext {
1145            env: ExecutionEnv { hash: B256::repeat_byte(2), ..ExecutionEnv::test_default() },
1146            evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1147            saved_cache: Some(saved_cache),
1148            provider: OverlayStateProviderFactory::new(
1149                MockEthProvider::default(),
1150                OverlayManager::default().overlay_builder(B256::ZERO),
1151            ),
1152            bal_prewarm_pool: None,
1153            metrics: PrewarmMetrics {
1154                cache_saving_duration: saving_duration,
1155                ..Default::default()
1156            },
1157            cache_metrics: None,
1158            cache_state_metrics: None,
1159            terminate_execution: Arc::new(AtomicBool::new(false)),
1160            executed_tx_index: Arc::new(AtomicUsize::new(0)),
1161            precompile_cache_disabled: false,
1162            precompile_cache_map: PrecompileCacheMap::default(),
1163            disable_bal_parallel_state_root: false,
1164            disable_bal_batch_io: false,
1165            prewarm_results: None,
1166        }
1167    }
1168
1169    fn save_test_cache(
1170        runtime: &Runtime,
1171        execution_cache: &PayloadExecutionCache,
1172        saved_cache: SavedCache,
1173        state: reth_revm::db::BundleState,
1174        valid: bool,
1175        saving_duration: Gauge,
1176    ) {
1177        let ctx = test_prewarm_context(saved_cache, saving_duration);
1178        let (task, _) = PrewarmCacheTask::new(runtime.clone(), execution_cache.clone(), ctx);
1179        let (valid_tx, valid_rx) = mpsc::channel();
1180        if valid {
1181            valid_tx.send(()).unwrap();
1182        }
1183        drop(valid_tx);
1184        task.save_cache(
1185            Arc::new(BlockExecutionOutput { state, result: Default::default() }),
1186            valid_rx,
1187        );
1188    }
1189
1190    // Observe the handoff before save_cache returns and drops its local variables. A check after
1191    // return would miss the window where the saving task still owns an extra reference.
1192    struct CacheSaveObserver {
1193        cache: PayloadExecutionCache,
1194        observed: Arc<AtomicBool>,
1195    }
1196
1197    impl metrics::GaugeFn for CacheSaveObserver {
1198        fn increment(&self, _: f64) {}
1199        fn decrement(&self, _: f64) {}
1200        fn set(&self, _: f64) {
1201            assert!(
1202                self.cache.get_cache_for(B256::repeat_byte(2)).is_some(),
1203                "saved cache must be available before the saving task returns"
1204            );
1205            self.observed.store(true, Ordering::Relaxed);
1206        }
1207    }
1208
1209    #[test]
1210    fn save_cache_releases_warm_cache_before_duration_metric() {
1211        let runtime = Runtime::test();
1212        let execution_cache = PayloadExecutionCache::default();
1213        let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1214        let address = Address::with_last_byte(1);
1215        saved.cache().insert_storage(address, B256::ZERO, Some(U256::from(7)));
1216        execution_cache.update_with_guard(|slot| *slot = Some(saved.clone()));
1217        // Keep the drop worker occupied: a queued SavedCache would delay cache reuse.
1218        let (release_tx, release_rx) = mpsc::channel::<()>();
1219        runtime.spawn_blocking_named("drop", move || {
1220            let _ = release_rx.recv();
1221        });
1222        let observed = Arc::new(AtomicBool::new(false));
1223        let observer = Arc::new(CacheSaveObserver {
1224            cache: execution_cache.clone(),
1225            observed: observed.clone(),
1226        });
1227        save_test_cache(
1228            &runtime,
1229            &execution_cache,
1230            saved,
1231            Default::default(),
1232            true,
1233            Gauge::from_arc(observer),
1234        );
1235        assert!(observed.load(Ordering::Relaxed), "save duration was not recorded");
1236        let saved = execution_cache.get_cache_for(B256::repeat_byte(2)).unwrap();
1237        assert_eq!(
1238            saved.cache().get_or_try_insert_storage_with(address, B256::ZERO, || Err(())),
1239            Ok(reth_execution_cache::CachedStatus::Cached(U256::from(7))),
1240            "handoff must preserve the warmed contents",
1241        );
1242        drop(release_tx);
1243    }
1244
1245    #[test]
1246    fn save_cache_blocks_reuse_while_execution_cache_is_cloned() {
1247        let runtime = Runtime::test();
1248        let execution_cache = PayloadExecutionCache::default();
1249        let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1250        execution_cache.update_with_guard(|slot| *slot = Some(saved.clone()));
1251        let other_cache_clone = saved.cache().clone();
1252
1253        save_test_cache(&runtime, &execution_cache, saved, Default::default(), true, Gauge::noop());
1254
1255        assert!(execution_cache.get_cache_for(B256::repeat_byte(2)).is_none());
1256        drop(other_cache_clone);
1257        assert!(execution_cache.get_cache_for(B256::repeat_byte(2)).is_some());
1258    }
1259
1260    #[derive(Clone, Copy, Debug)]
1261    enum CacheSlot {
1262        Empty,
1263        Shared,
1264        Distinct,
1265    }
1266
1267    // EIP-7702 bytecode preserves its owned buffer, letting us observe actual cache destruction.
1268    struct CacheDropProbe {
1269        started: Sender<std::thread::ThreadId>,
1270        inspected: Receiver<()>,
1271        result: Sender<bool>,
1272    }
1273
1274    impl AsRef<[u8]> for CacheDropProbe {
1275        fn as_ref(&self) -> &[u8] {
1276            &EIP7702_CLEARED_DELEGATION
1277        }
1278    }
1279
1280    impl Drop for CacheDropProbe {
1281        fn drop(&mut self) {
1282            let _ = self.started.send(std::thread::current().id());
1283            // A timeout turns destruction under the mutex into a failure instead of a deadlock.
1284            let unlocked = self.inspected.recv_timeout(std::time::Duration::from_secs(5)).is_ok();
1285            let _ = self.result.send(unlocked);
1286        }
1287    }
1288
1289    fn observe_cache_drop(
1290        saved: &SavedCache,
1291        execution_cache: &PayloadExecutionCache,
1292        expect_saved_cache: bool,
1293    ) -> (Receiver<bool>, std::thread::JoinHandle<std::thread::ThreadId>) {
1294        let (started_tx, started_rx) = mpsc::channel();
1295        let (inspected_tx, inspected_rx) = mpsc::channel();
1296        let (result_tx, result_rx) = mpsc::channel();
1297        let cache = execution_cache.clone();
1298        let reader = std::thread::spawn(move || {
1299            let drop_thread = started_rx.recv().unwrap();
1300            // Waiting for the mutex must return while the removed cache is still being dropped.
1301            cache.wait_for_availability();
1302            assert_eq!(cache.get_cache_for(B256::repeat_byte(2)).is_some(), expect_saved_cache);
1303            cache.update_with_guard(|slot| assert_eq!(slot.is_some(), expect_saved_cache));
1304            let _ = inspected_tx.send(());
1305            drop_thread
1306        });
1307        let bytes = alloy_primitives::bytes::Bytes::from_owner(CacheDropProbe {
1308            started: started_tx,
1309            inspected: inspected_rx,
1310            result: result_tx,
1311        });
1312        let code = reth_revm::bytecode::Bytecode::new_eip7702_raw(bytes.into()).unwrap();
1313        saved
1314            .cache()
1315            .insert_code(B256::repeat_byte(3), Some(reth_primitives_traits::Bytecode(code)));
1316        (result_rx, reader)
1317    }
1318
1319    #[test]
1320    fn save_cache_freeing_waits_for_validator_cache_references() {
1321        use crate::tree::payload_processor::{CacheTaskHandle, PayloadHandle};
1322
1323        let runtime = Runtime::test();
1324        let execution_cache = PayloadExecutionCache::default();
1325        let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1326        execution_cache.update_with_guard(|cached| *cached = Some(saved.clone()));
1327        let (dropped, reader) = observe_cache_drop(&saved, &execution_cache, false);
1328        let ctx = test_prewarm_context(saved, Gauge::noop());
1329        let (task, actions_tx) =
1330            PrewarmCacheTask::new(runtime.clone(), execution_cache.clone(), ctx);
1331        let mut payload = PayloadHandle {
1332            prewarm_handle: CacheTaskHandle::<_, ()> {
1333                saved_cache: task.ctx.saved_cache.clone(),
1334                to_prewarm_task: Some(actions_tx.clone()),
1335                executed_tx_index: task.ctx.executed_tx_index.clone(),
1336                prewarm_results: None,
1337                cache_metrics: None,
1338            },
1339            transactions: crossbeam_channel::never::<(usize, Result<(), ()>)>(),
1340            _span: Span::none(),
1341        };
1342        // The validator keeps this ExecutionCache clone after calling terminate_caching.
1343        let validator_cache = payload.caches().unwrap();
1344        let valid_block_tx = payload.terminate_caching(Some(Arc::new(BlockExecutionOutput {
1345            state: Default::default(),
1346            result: Default::default(),
1347        })));
1348        drop(valid_block_tx);
1349        let prewarm = runtime.spawn_blocking_named("prewarm", move || {
1350            task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
1351                PrewarmMode::Skipped,
1352                actions_tx,
1353            );
1354            std::thread::current().id()
1355        });
1356        let prewarm_thread = *prewarm.get();
1357
1358        execution_cache.update_with_guard(|cached| assert!(cached.is_none()));
1359        assert_eq!(payload.prewarm_handle.saved_cache.as_ref().unwrap().usage_count(), 2);
1360        assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Empty)));
1361        drop(payload);
1362        assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Empty)));
1363
1364        drop(validator_cache);
1365        assert!(dropped.try_recv().expect("last ExecutionCache drop must free the cache"));
1366        let drop_thread = reader.join().unwrap();
1367        assert_eq!(drop_thread, std::thread::current().id());
1368        assert_ne!(drop_thread, prewarm_thread);
1369    }
1370
1371    fn assert_save_cache_drops_removed_caches(slot: CacheSlot, valid: bool, insert_error: bool) {
1372        use reth_revm::db::{AccountStatus, BundleAccount, BundleState};
1373
1374        let runtime = Runtime::test();
1375        let execution_cache = PayloadExecutionCache::default();
1376        let cache_to_save =
1377            SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1378        let distinct_previous = matches!(slot, CacheSlot::Distinct).then(|| {
1379            // The same block hash does not imply the same allocation.
1380            SavedCache::new(B256::repeat_byte(2), crate::tree::ExecutionCache::new(1_000))
1381        });
1382        execution_cache.update_with_guard(|cached| {
1383            *cached = match slot {
1384                CacheSlot::Empty => None,
1385                CacheSlot::Shared => Some(cache_to_save.clone()),
1386                CacheSlot::Distinct => distinct_previous.clone(),
1387            };
1388        });
1389        let expect_saved_cache = valid && !insert_error;
1390        let mut drops = Vec::new();
1391        if let Some(previous) = &distinct_previous {
1392            drops.push(observe_cache_drop(previous, &execution_cache, expect_saved_cache));
1393        }
1394        if !expect_saved_cache {
1395            drops.push(observe_cache_drop(&cache_to_save, &execution_cache, expect_saved_cache));
1396        }
1397        // Retaining this SavedCache would prevent save_cache from freeing the old cache.
1398        drop(distinct_previous);
1399
1400        // Cleanup must finish even when the shared background drop worker is occupied.
1401        let (release_tx, release_rx) = mpsc::channel::<()>();
1402        runtime.spawn_blocking_named("drop", move || {
1403            let _ = release_rx.recv();
1404        });
1405
1406        let mut state = BundleState::default();
1407        if insert_error {
1408            // Modified accounts without current info are rejected by insert_state.
1409            state.state.insert(
1410                Address::with_last_byte(1),
1411                BundleAccount::new(None, None, Default::default(), AccountStatus::Changed),
1412            );
1413        }
1414        save_test_cache(&runtime, &execution_cache, cache_to_save, state, valid, Gauge::noop());
1415
1416        for (result, reader) in drops {
1417            assert!(
1418                result.try_recv().expect("removed cache must be destroyed before save returns"),
1419                "cache mutex must be unlocked during destruction"
1420            );
1421            assert_eq!(reader.join().unwrap(), std::thread::current().id());
1422        }
1423        execution_cache.update_with_guard(|slot| {
1424            if expect_saved_cache {
1425                let saved = slot.as_ref().expect("valid cache saved");
1426                assert_eq!(saved.executed_block_hash(), B256::repeat_byte(2));
1427            } else {
1428                assert!(slot.is_none(), "polluted cache must be removed");
1429            }
1430        });
1431        assert_eq!(
1432            execution_cache.get_cache_for(B256::repeat_byte(2)).is_some(),
1433            expect_saved_cache
1434        );
1435        drop(release_tx);
1436    }
1437
1438    #[test]
1439    fn save_cache_drops_replaced_allocation_after_unlock() {
1440        assert_save_cache_drops_removed_caches(CacheSlot::Distinct, true, false);
1441    }
1442
1443    #[test]
1444    fn save_cache_drops_invalid_allocations_after_unlock() {
1445        assert_save_cache_drops_removed_caches(CacheSlot::Distinct, false, false);
1446    }
1447
1448    #[test]
1449    fn save_cache_drops_allocations_after_unlock_on_insert_error() {
1450        assert_save_cache_drops_removed_caches(CacheSlot::Distinct, true, true);
1451    }
1452
1453    #[test]
1454    fn save_cache_drops_shared_allocation_after_unlock_on_invalid_block() {
1455        assert_save_cache_drops_removed_caches(CacheSlot::Shared, false, false);
1456    }
1457
1458    #[test]
1459    fn save_cache_drops_shared_allocation_after_unlock_on_insert_error() {
1460        assert_save_cache_drops_removed_caches(CacheSlot::Shared, true, true);
1461    }
1462
1463    #[test]
1464    fn save_cache_handles_empty_slot() {
1465        for (valid, insert_error) in [(true, false), (false, false), (true, true)] {
1466            assert_save_cache_drops_removed_caches(CacheSlot::Empty, valid, insert_error);
1467        }
1468    }
1469}