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