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
14use super::{bal_prewarm_pool::BalPrewarmPool, StateRootHintStream, StateRootUpdateStream};
15use crate::tree::{
16    precompile_cache::{CachedPrecompile, PrecompileCacheMap},
17    CachedStateCacheMetrics, CachedStateMetrics, CachedStateProvider, ExecutionEnv,
18    PayloadExecutionCache, SavedCache, StateProviderBuilder,
19};
20use alloy_consensus::transaction::TxHashRef;
21use alloy_eip7928::bal::DecodedBal;
22use alloy_eips::eip4895::Withdrawal;
23use alloy_primitives::{keccak256, B256, U256};
24use metrics::{Counter, Gauge, Histogram};
25use rayon::prelude::*;
26use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, RecoveredTx, SpecFor};
27use reth_metrics::Metrics;
28use reth_primitives_traits::{Account, FastInstant as Instant, NodePrimitives};
29use reth_provider::{
30    AccountReader, BlockExecutionOutput, BlockNumReader, DatabaseProviderFactory,
31    PruneCheckpointReader, StageCheckpointReader, StorageSettingsCache,
32    TryIntoHistoricalStateProvider,
33};
34use reth_revm::database::StateProviderDatabase;
35use reth_tasks::{pool::WorkerPool, Runtime};
36use reth_trie_common::MultiProofTargetsV2;
37use std::sync::{
38    atomic::{AtomicBool, AtomicUsize, Ordering},
39    mpsc::{self, channel, Receiver, Sender},
40    Arc,
41};
42use tokio::sync::oneshot;
43use tracing::{debug, debug_span, instrument, trace, trace_span, warn, Span};
44
45/// Determines the prewarming mode: transaction-based, BAL-based, or skipped.
46///
47/// Each variant carries the state-root capability its producers use, so the capability dies
48/// with the workers instead of outliving them.
49#[derive(Debug)]
50pub enum PrewarmMode<Tx> {
51    /// Prewarm by executing transactions from a stream, each paired with its block index.
52    Transactions {
53        /// Stream of transactions pending prewarm execution.
54        pending: Receiver<(usize, Tx)>,
55        /// Best-effort access hints emitted by the prewarm workers.
56        hints: Option<StateRootHintStream>,
57    },
58    /// Prewarm by prefetching slots from a Block Access List.
59    BlockAccessList {
60        /// The decoded block access list.
61        bal: Arc<DecodedBal>,
62        /// Authoritative pre-hashed updates derived from the BAL.
63        updates: Option<StateRootUpdateStream>,
64    },
65    /// Transaction prewarming is skipped (e.g. small blocks where the overhead exceeds the
66    /// benefit). No workers are spawned.
67    Skipped,
68}
69
70/// A task that is responsible for caching and prewarming the cache by executing transactions
71/// individually in parallel.
72///
73/// Note: This task runs until cancelled externally.
74#[derive(Debug)]
75pub struct PrewarmCacheTask<N, P, Evm>
76where
77    N: NodePrimitives,
78    Evm: ConfigureEvm<Primitives = N>,
79{
80    /// The executor used to spawn execution tasks.
81    executor: Runtime,
82    /// Shared execution cache.
83    execution_cache: PayloadExecutionCache,
84    /// Context provided to execution tasks
85    ctx: PrewarmContext<N, P, Evm>,
86    /// Receiver for events produced by tx execution
87    actions_rx: Receiver<PrewarmTaskEvent<N::Receipt>>,
88    /// Parent span for tracing
89    parent_span: Span,
90}
91
92impl<N, P, Evm> PrewarmCacheTask<N, P, Evm>
93where
94    N: NodePrimitives,
95    P: DatabaseProviderFactory + Clone + 'static,
96    P::Provider: BlockNumReader
97        + PruneCheckpointReader
98        + StageCheckpointReader
99        + StorageSettingsCache
100        + TryIntoHistoricalStateProvider
101        + 'static,
102    Evm: ConfigureEvm<Primitives = N> + 'static,
103{
104    /// Initializes the task with the given transactions pending execution
105    pub fn new(
106        executor: Runtime,
107        execution_cache: PayloadExecutionCache,
108        ctx: PrewarmContext<N, P, Evm>,
109    ) -> (Self, Sender<PrewarmTaskEvent<N::Receipt>>) {
110        let (actions_tx, actions_rx) = channel();
111
112        trace!(
113            target: "engine::tree::payload_processor::prewarm",
114            prewarming_threads = executor.prewarming_pool().current_num_threads(),
115            transaction_count = ctx.env.transaction_count,
116            "Initialized prewarm task"
117        );
118
119        (
120            Self { executor, execution_cache, ctx, actions_rx, parent_span: Span::current() },
121            actions_tx,
122        )
123    }
124
125    /// Streams pending transactions and executes them in parallel on the prewarming pool.
126    ///
127    /// Kicks off EVM init on every pool thread, then uses `in_place_scope` to dispatch
128    /// transactions as they arrive and wait for all spawned tasks to complete before
129    /// clearing per-thread state. Workers that start via work-stealing lazily initialise
130    /// their EVM state on first access via [`get_or_init`](reth_tasks::pool::Worker::get_or_init).
131    fn spawn_txs_prewarm<Tx>(
132        &self,
133        pending: mpsc::Receiver<(usize, Tx)>,
134        actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
135        state_root_hint_stream: Option<StateRootHintStream>,
136    ) where
137        Tx: ExecutableTxFor<Evm> + Send + 'static,
138    {
139        let executor = self.executor.clone();
140        let ctx = self.ctx.clone();
141        let span = Span::current();
142
143        self.executor.spawn_blocking_named("prewarm-txs", move || {
144            let _enter = debug_span!(
145                target: "engine::tree::payload_processor::prewarm",
146                parent: &span,
147                "prewarm_txs"
148            )
149            .entered();
150
151            let ctx = &ctx;
152            let pool = executor.prewarming_pool();
153
154            let mut tx_count = 0usize;
155            let state_root_hint_stream = state_root_hint_stream.as_ref();
156            pool.in_place_scope(|s| {
157                s.spawn(|_| {
158                    pool.init::<PrewarmEvmState<Evm>>(|_| ctx.evm_for_ctx());
159                });
160
161                while let Ok((index, tx)) = pending.recv() {
162                    if ctx.should_stop() {
163                        trace!(
164                            target: "engine::tree::payload_processor::prewarm",
165                            "Termination requested, stopping transaction distribution"
166                        );
167                        break;
168                    }
169
170                    // skip transactions already executed by the main loop
171                    if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
172                        continue;
173                    }
174
175                    tx_count += 1;
176                    let parent_span = Span::current();
177                    s.spawn(move |_| {
178                        let _enter = trace_span!(
179                            target: "engine::tree::payload_processor::prewarm",
180                            parent: parent_span,
181                            "prewarm_tx",
182                            i = index,
183                        )
184                        .entered();
185                        Self::transact_worker(ctx, index, tx, state_root_hint_stream);
186                    });
187                }
188
189                // Send withdrawal prefetch targets after all transactions dispatched
190                if let Some(state_root_hint_stream) = state_root_hint_stream &&
191                    let Some(withdrawals) = &ctx.env.withdrawals &&
192                    !withdrawals.is_empty()
193                {
194                    let targets = multiproof_targets_from_withdrawals(withdrawals);
195                    state_root_hint_stream.on_access_hint(targets.into());
196                }
197            });
198
199            // All tasks are done — clear per-thread EVM state for the next block.
200            pool.clear();
201
202            let _ = actions_tx
203                .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: tx_count });
204        });
205    }
206
207    /// Executes a single prewarm transaction on the current pool thread's EVM.
208    ///
209    /// Lazily initialises per-thread [`PrewarmEvmState`] via
210    /// [`get_or_init`](reth_tasks::pool::Worker::get_or_init) on first access.
211    fn transact_worker<Tx>(
212        ctx: &PrewarmContext<N, P, Evm>,
213        index: usize,
214        tx: Tx,
215        state_root_hint_stream: Option<&StateRootHintStream>,
216    ) where
217        Tx: ExecutableTxFor<Evm>,
218    {
219        WorkerPool::with_worker_mut(|worker| {
220            let Some(evm) =
221                worker.get_or_init::<PrewarmEvmState<Evm>>(|| ctx.evm_for_ctx()).as_mut()
222            else {
223                return;
224            };
225
226            if ctx.should_stop() {
227                return;
228            }
229
230            // skip if main execution has already processed this transaction
231            if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
232                return;
233            }
234
235            let start = Instant::now();
236
237            let (tx_env, tx) = tx.into_parts();
238            let res = match evm.transact(tx_env) {
239                Ok(res) => res,
240                Err(err) => {
241                    trace!(
242                        target: "engine::tree::payload_processor::prewarm",
243                        %err,
244                        tx_hash=%tx.tx().tx_hash(),
245                        sender=%tx.signer(),
246                        "Error when executing prewarm transaction",
247                    );
248                    ctx.metrics.transaction_errors.increment(1);
249                    return;
250                }
251            };
252            ctx.metrics.execution_duration.record(start.elapsed());
253
254            if ctx.should_stop() {
255                return;
256            }
257
258            if index > 0 {
259                let (targets, storage_targets) = MultiProofTargetsV2::from_state(res.state);
260                ctx.metrics.prefetch_storage_targets.record(storage_targets as f64);
261                if let Some(state_root_hint_stream) = state_root_hint_stream {
262                    state_root_hint_stream.on_access_hint(targets.into());
263                }
264            }
265
266            ctx.metrics.total_runtime.record(start.elapsed());
267        });
268    }
269
270    /// This method calls `ExecutionCache::update_with_guard` which requires exclusive access.
271    /// It should only be called after ensuring that:
272    /// 1. All prewarming tasks have completed execution
273    /// 2. No other concurrent operations are accessing the cache
274    ///
275    /// Saves the warmed caches back into the shared slot after prewarming completes.
276    ///
277    /// This consumes the `SavedCache` held by the task, which releases its cache handle and allows
278    /// the new, warmed cache to be inserted.
279    ///
280    /// This method is called from `run()` only after all execution tasks are complete.
281    #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
282    fn save_cache(
283        self,
284        execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
285        valid_block_rx: mpsc::Receiver<()>,
286    ) {
287        let start = Instant::now();
288
289        let Self {
290            execution_cache,
291            ctx: PrewarmContext { env, metrics, cache_state_metrics, saved_cache, .. },
292            ..
293        } = self;
294        let hash = env.hash;
295
296        if let Some(saved_cache) = saved_cache {
297            debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
298            execution_cache.update_with_guard(|cached| {
299                // consumes the `SavedCache` held by the prewarming task, which releases its cache
300                // handle
301                let caches = saved_cache.cache().clone();
302                let new_cache = SavedCache::new(hash, caches);
303
304                // Insert state into cache while holding the lock
305                // Access the BundleState through the shared ExecutionOutcome
306                if new_cache.cache().insert_state(&execution_outcome.state).is_err() {
307                    // Clear the cache on error to prevent having a polluted cache
308                    *cached = None;
309                    debug!(target: "engine::caching", "cleared execution cache on update error");
310                    return;
311                }
312
313                new_cache.update_metrics(cache_state_metrics.as_ref());
314
315                if valid_block_rx.recv().is_ok() {
316                    // Replace the shared cache with the new one; the previous cache (if any) is
317                    // dropped.
318                    *cached = Some(new_cache);
319                } else {
320                    // Block was invalid; caches were already mutated by insert_state above,
321                    // so we must clear to prevent using polluted state
322                    *cached = None;
323                    debug!(target: "engine::caching", "cleared execution cache on invalid block");
324                }
325            });
326
327            let elapsed = start.elapsed();
328            debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
329
330            metrics.cache_saving_duration.set(elapsed.as_secs_f64());
331        }
332    }
333
334    /// Runs BAL-based prewarming and state-root streaming inline.
335    ///
336    /// Spawns two halves concurrently on separate pools, then waits for both to complete:
337    /// 1. Hashed state streaming on the BAL streaming pool so storage updates can reach the
338    ///    state-root job before account reads finish.
339    /// 2. Storage prefetch on the prewarming pool to populate the execution cache, unless BAL batch
340    ///    I/O is disabled.
341    #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
342    fn run_bal_prewarm(
343        &self,
344        decoded_bal: Arc<DecodedBal>,
345        actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
346        hashed_update_stream: Option<StateRootUpdateStream>,
347    ) {
348        let bal = decoded_bal.as_bal();
349        if bal.is_empty() {
350            if let Some(hashed_update_stream) = hashed_update_stream {
351                hashed_update_stream.finish();
352            }
353            let _ =
354                actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
355            return;
356        }
357
358        trace!(
359            target: "engine::tree::payload_processor::prewarm",
360            accounts = bal.len(),
361            "Starting BAL prewarm"
362        );
363
364        let ctx = self.ctx.clone();
365        let executor = self.executor.clone();
366        let parent_span = Span::current();
367        let stream_parent_span = parent_span;
368        let prefetch_bal = Arc::clone(&decoded_bal);
369        let stream_bal = Arc::clone(&decoded_bal);
370        let (stream_tx, stream_rx) = oneshot::channel();
371
372        if let Some(hashed_update_stream) = hashed_update_stream {
373            let ctx = ctx.clone();
374            executor.bal_streaming_pool().spawn(move || {
375                let branch_span = debug_span!(
376                    target: "engine::tree::payload_processor::prewarm",
377                    parent: &stream_parent_span,
378                    "bal_hashed_state_stream",
379                    bal_accounts = stream_bal.as_bal().len(),
380                );
381                let parent_span = branch_span.clone();
382                let _span = branch_span.entered();
383
384                stream_bal.as_bal().par_iter().for_each(|account_changes| {
385                    WorkerPool::with_worker_mut(|worker| {
386                        let provider =
387                            worker.get_or_init::<Option<Box<dyn AccountReader>>>(|| None);
388                        ctx.send_bal_hashed_state(
389                            &parent_span,
390                            provider,
391                            account_changes,
392                            &hashed_update_stream,
393                        );
394                    });
395                });
396
397                hashed_update_stream.finish();
398                let _ = stream_tx.send(());
399            });
400        } else {
401            let _ = stream_tx.send(());
402        }
403
404        if let Some(saved_cache) = ctx.saved_cache &&
405            !ctx.disable_bal_batch_io &&
406            let Some(pool) = ctx.bal_prewarm_pool.as_ref()
407        {
408            // If
409            //
410            // - BAL path is enabled (and so bal_prewarm_pool is present),
411            // - dispatch_bal_batch_io is false
412            // - execution cache is not disabled
413            //
414            // we launch prewarming sequence of the BAL read set here. The BAL read-set consists
415            // of the accounts, their code if present, and declared storages (both storage_reads
416            // and storage_changes).
417            //
418            // This runs side-by-side with the parallel transaction execution reducing the time it
419            // spends blocking on the data.
420            let caches = saved_cache.cache().clone();
421            let provider_builder = ctx.provider.clone();
422            let build = Arc::new(move || provider_builder.build());
423
424            pool.begin_block(build, caches, ctx.env.txpool_snapshot.clone());
425            for account in prefetch_bal.as_bal() {
426                pool.warm_account(account.address);
427                for change in &account.storage_changes {
428                    pool.warm_storage(account.address, change.slot.into());
429                }
430                for &slot in &account.storage_reads {
431                    pool.warm_storage(account.address, slot.into());
432                }
433            }
434            pool.end_block();
435        }
436
437        stream_rx
438            .blocking_recv()
439            .expect("BAL hashed-state streaming task dropped without signaling completion");
440
441        // Drop the per-thread providers
442        executor.bal_streaming_pool().clear();
443        executor.prewarming_pool().clear();
444
445        let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
446    }
447
448    /// Executes the task.
449    ///
450    /// This will execute the transactions until all transactions have been processed or the task
451    /// was cancelled.
452    #[instrument(
453        parent = &self.parent_span,
454        level = "debug",
455        target = "engine::tree::payload_processor::prewarm",
456        name = "prewarm and caching",
457        skip_all
458    )]
459    pub fn run<Tx>(self, mode: PrewarmMode<Tx>, actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>)
460    where
461        Tx: ExecutableTxFor<Evm> + Send + 'static,
462    {
463        // Spawn execution tasks based on mode. The state-root capabilities arrive inside the
464        // mode and move into the spawned producers, so they die with the producers instead of
465        // living for the full lifetime of this task.
466        match mode {
467            PrewarmMode::Transactions { pending, hints } => {
468                self.spawn_txs_prewarm(pending, actions_tx, hints);
469            }
470            PrewarmMode::BlockAccessList { bal, updates } => {
471                self.run_bal_prewarm(bal, actions_tx, updates);
472            }
473            PrewarmMode::Skipped => {
474                let _ = actions_tx
475                    .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
476            }
477        }
478
479        let mut final_execution_outcome = None;
480        let mut finished_execution = false;
481        while let Ok(event) = self.actions_rx.recv() {
482            match event {
483                PrewarmTaskEvent::TerminateTransactionExecution => {
484                    // stop tx processing
485                    debug!(target: "engine::tree::prewarm", "Terminating prewarm execution");
486                    self.ctx.stop();
487                }
488                PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx } => {
489                    trace!(target: "engine::tree::payload_processor::prewarm", "Received termination signal");
490                    // `Terminate` can arrive without `TerminateTransactionExecution` when the
491                    // handle is dropped on an execution error, so stop workers before waiting.
492                    self.ctx.stop();
493                    final_execution_outcome =
494                        Some(execution_outcome.map(|outcome| (outcome, valid_block_rx)));
495
496                    if finished_execution {
497                        // all tasks are done, we can exit, which will save caches and exit
498                        break
499                    }
500                }
501                PrewarmTaskEvent::FinishedTxExecution { executed_transactions } => {
502                    trace!(target: "engine::tree::payload_processor::prewarm", "Finished prewarm execution signal");
503                    self.ctx.metrics.transactions.set(executed_transactions as f64);
504                    self.ctx.metrics.transactions_histogram.record(executed_transactions as f64);
505
506                    finished_execution = true;
507
508                    if final_execution_outcome.is_some() {
509                        // all tasks are done, we can exit, which will save caches and exit
510                        break
511                    }
512                }
513            }
514        }
515
516        debug!(target: "engine::tree::payload_processor::prewarm", "Completed prewarm execution");
517
518        // save caches and finish using the shared ExecutionOutcome
519        if let Some(Some((execution_outcome, valid_block_rx))) = final_execution_outcome {
520            self.save_cache(execution_outcome, valid_block_rx);
521        }
522    }
523}
524
525/// Context required by tx execution tasks.
526#[derive(Debug, Clone)]
527pub struct PrewarmContext<N, P, Evm>
528where
529    N: NodePrimitives,
530    Evm: ConfigureEvm<Primitives = N>,
531{
532    /// The execution environment.
533    pub env: ExecutionEnv<Evm>,
534    /// The EVM configuration.
535    pub evm_config: Evm,
536    /// The saved cache.
537    pub saved_cache: Option<SavedCache>,
538    /// Provider to obtain the state
539    pub provider: StateProviderBuilder<N, P>,
540    /// Dedicated blocking pool for warming the BAL read-set. `Some` only on the BAL parallel
541    /// execution path; the pool is owned by the [`PayloadProcessor`](super::PayloadProcessor).
542    pub(crate) bal_prewarm_pool: Option<Arc<BalPrewarmPool>>,
543    /// The metrics for the prewarm task.
544    pub metrics: PrewarmMetrics,
545    /// Metrics for the execution cache.
546    /// Metrics for the execution cache. `None` disables metrics recording.
547    pub cache_metrics: Option<CachedStateMetrics>,
548    /// Metrics for shared execution cache state. `None` disables metrics recording.
549    pub cache_state_metrics: Option<CachedStateCacheMetrics>,
550    /// An atomic bool that tells prewarm tasks to not start any more execution.
551    pub terminate_execution: Arc<AtomicBool>,
552    /// Shared counter tracking the next transaction index to be executed by the main execution
553    /// loop. Prewarm workers skip transactions with `index < counter` since those have already
554    /// been executed.
555    pub executed_tx_index: Arc<AtomicUsize>,
556    /// Whether the precompile cache is disabled.
557    pub precompile_cache_disabled: bool,
558    /// The precompile cache map.
559    pub precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
560    /// Whether to disable BAL-driven parallel state root computation.
561    /// Only valid when BAL parallel execution is also disabled.
562    pub disable_bal_parallel_state_root: bool,
563    /// Whether BAL state prefetching during prewarm is disabled.
564    pub disable_bal_batch_io: bool,
565}
566
567/// Per-thread EVM state initialised by [`PrewarmContext::evm_for_ctx`] and stored in
568/// [`WorkerPool`] workers via [`Worker::get_or_init`](reth_tasks::pool::Worker::get_or_init).
569type PrewarmEvmState<Evm> =
570    Option<EvmFor<Evm, StateProviderDatabase<reth_provider::StateProviderBox>>>;
571
572impl<N, P, Evm> PrewarmContext<N, P, Evm>
573where
574    N: NodePrimitives,
575    P: DatabaseProviderFactory,
576    P::Provider: BlockNumReader
577        + PruneCheckpointReader
578        + StageCheckpointReader
579        + StorageSettingsCache
580        + TryIntoHistoricalStateProvider
581        + 'static,
582    Evm: ConfigureEvm<Primitives = N> + 'static,
583{
584    /// Creates a per-thread EVM for prewarming.
585    #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
586    fn evm_for_ctx(&self) -> PrewarmEvmState<Evm> {
587        let mut state_provider = match self.provider.build() {
588            Ok(provider) => provider,
589            Err(err) => {
590                trace!(
591                    target: "engine::tree::payload_processor::prewarm",
592                    %err,
593                    "Failed to build state provider in prewarm thread"
594                );
595                return None
596            }
597        };
598
599        // Use the caches to create a new provider with caching
600        if let Some(saved_cache) = &self.saved_cache {
601            let caches = saved_cache.cache().clone();
602            state_provider = Box::new(
603                CachedStateProvider::new_prewarm(state_provider, caches)
604                    .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
605            );
606        }
607
608        let state_provider = StateProviderDatabase::new(state_provider);
609
610        let mut evm_env = self.env.evm_env.clone();
611
612        // we must disable the nonce check so that we can execute the transaction even if the nonce
613        // doesn't match what's on chain.
614        evm_env.cfg_env.disable_nonce_check = true;
615
616        // disable the balance check so that transactions from senders who were funded by earlier
617        // transactions in the block can still be prewarmed
618        evm_env.cfg_env.disable_balance_check = true;
619
620        // create a new executor and disable nonce checks in the env
621        let spec_id = *evm_env.spec_id();
622        let mut evm = self.evm_config.evm_with_env(state_provider, evm_env);
623
624        if !self.precompile_cache_disabled {
625            // Only cache pure precompiles to avoid issues with stateful precompiles
626            evm.precompiles_mut().map_cacheable_precompiles(|address, precompile| {
627                CachedPrecompile::wrap(
628                    precompile,
629                    self.precompile_cache_map.cache_for_address(*address),
630                    spec_id,
631                    None, // No metrics for prewarm
632                )
633            });
634        }
635
636        Some(evm)
637    }
638
639    /// Returns `true` if prewarming should stop.
640    #[inline]
641    pub fn should_stop(&self) -> bool {
642        self.terminate_execution.load(Ordering::Relaxed)
643    }
644
645    /// Signals all prewarm tasks to stop execution.
646    #[inline]
647    pub fn stop(&self) {
648        self.terminate_execution.store(true, Ordering::Relaxed);
649    }
650
651    /// Hashes and streams a single BAL account's state to the state-root job's hashed-update
652    /// stream.
653    ///
654    /// For each changed account, storage slots are hashed and sent immediately, then the account
655    /// is sent as a separate update. The parent account is read only when the BAL did not provide
656    /// all account leaf fields needed for state-root computation.
657    ///
658    /// The `provider` is lazily initialized on first call and reused across accounts on the same
659    /// thread.
660    fn send_bal_hashed_state(
661        &self,
662        parent_span: &Span,
663        provider: &mut Option<Box<dyn AccountReader>>,
664        account_changes: &alloy_eip7928::AccountChanges,
665        hashed_update_stream: &StateRootUpdateStream,
666    ) {
667        if self.disable_bal_parallel_state_root {
668            return;
669        }
670        let address = account_changes.address;
671        let mut hashed_address = None;
672        let account_fields = BalAccountStateFields::from_changes(account_changes);
673
674        if !bal_account_changes_state_root(account_changes, account_fields) {
675            return;
676        }
677
678        // If there are any storage changes we can assume that the resulting account info will be
679        // non-empty, so the account will exist, and therefore we can pre-emptively send out storage
680        // changes to start processing them before potentially hitting the db in the next step.
681        if !account_changes.storage_changes.is_empty() {
682            let hashed_address = *hashed_address.get_or_insert_with(|| keccak256(address));
683            let mut storage_map = reth_trie::HashedStorage::new(false);
684
685            for slot_changes in &account_changes.storage_changes {
686                let hashed_slot = keccak256(slot_changes.slot.to_be_bytes::<32>());
687                if let Some(last_change) = slot_changes.changes.last() {
688                    storage_map.storage.insert(hashed_slot, last_change.new_value);
689                }
690            }
691
692            let mut hashed_state = reth_trie::HashedPostState::default();
693            hashed_state.storages.insert(hashed_address, storage_map);
694            hashed_update_stream.on_hashed_state_update(hashed_state);
695        }
696
697        let existing_account = if account_fields.needs_parent_account() {
698            if provider.is_none() {
699                let _span = debug_span!(
700                    target: "engine::tree::payload_processor::prewarm",
701                    parent: parent_span,
702                    "bal_hashed_state_provider_init",
703                    has_saved_cache = !self.disable_bal_batch_io && self.saved_cache.is_some(),
704                )
705                .entered();
706
707                let inner = match self.provider.build() {
708                    Ok(p) => p,
709                    Err(err) => {
710                        warn!(
711                            target: "engine::tree::payload_processor::prewarm",
712                            ?err,
713                            "Failed to build provider for BAL account reads"
714                        );
715                        return;
716                    }
717                };
718                let boxed: Box<dyn AccountReader> =
719                    match (self.disable_bal_batch_io, &self.saved_cache) {
720                        (false, Some(saved)) => {
721                            let caches = saved.cache().clone();
722                            Box::new(
723                                CachedStateProvider::new_prewarm(inner, caches)
724                                    .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
725                            )
726                        }
727                        _ => Box::new(inner),
728                    };
729                *provider = Some(boxed);
730            }
731            let account_reader = provider.as_ref().expect("provider just initialized");
732            account_reader.basic_account(&address).ok().flatten()
733        } else {
734            None
735        };
736
737        let account = account_fields.into_account(existing_account);
738        let hashed_address = hashed_address.unwrap_or_else(|| keccak256(address));
739
740        // It is possible for the resulting account info to be empty. This can happen when, in the
741        // same block:
742        // * tx1: A new account is funded
743        // * tx2: CREATE2 is called on the new account, SELFDESTRUCT is called within the init code
744        //
745        // In this case the account will have only balance_changes, one for funding and the second
746        // setting balance back to zero. The resulting account is fully empty, we mark it as None
747        // with no storage changes to indicate that it should be deleted if nothing else.
748        //
749        // We assume that if the account info is all zero then it can't have storage, so we don't
750        // have to explicitly check for empty storage.
751        let account = (!account.is_empty()).then_some(account);
752
753        let mut hashed_state = reth_trie::HashedPostState::default();
754        hashed_state.accounts.insert(hashed_address, account);
755        hashed_update_stream.on_hashed_state_update(hashed_state);
756    }
757}
758
759#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
760struct BalAccountStateFields {
761    balance: Option<U256>,
762    nonce: Option<u64>,
763    code_hash: Option<B256>,
764}
765
766impl BalAccountStateFields {
767    fn from_changes(account_changes: &alloy_eip7928::AccountChanges) -> Self {
768        Self {
769            balance: account_changes.balance_changes.last().map(|change| change.post_balance),
770            nonce: account_changes.nonce_changes.last().map(|change| change.new_nonce),
771            code_hash: account_changes.code_changes.last().map(|code_change| {
772                if code_change.new_code.is_empty() {
773                    alloy_consensus::constants::KECCAK_EMPTY
774                } else {
775                    keccak256(&code_change.new_code)
776                }
777            }),
778        }
779    }
780
781    const fn is_empty(self) -> bool {
782        self.balance.is_none() && self.nonce.is_none() && self.code_hash.is_none()
783    }
784
785    const fn needs_parent_account(self) -> bool {
786        self.balance.is_none() || self.nonce.is_none() || self.code_hash.is_none()
787    }
788
789    fn into_account(self, existing_account: Option<Account>) -> Account {
790        let existing_account = existing_account.as_ref();
791        Account {
792            balance: self.balance.unwrap_or_else(|| {
793                existing_account
794                    .map(|account| account.balance)
795                    .unwrap_or(alloy_primitives::U256::ZERO)
796            }),
797            nonce: self
798                .nonce
799                .unwrap_or_else(|| existing_account.map(|account| account.nonce).unwrap_or(0)),
800            bytecode_hash: self.code_hash.or_else(|| {
801                existing_account
802                    .and_then(|account| account.bytecode_hash)
803                    .or(Some(alloy_consensus::constants::KECCAK_EMPTY))
804            }),
805        }
806    }
807}
808
809const fn bal_account_changes_state_root(
810    account_changes: &alloy_eip7928::AccountChanges,
811    account_fields: BalAccountStateFields,
812) -> bool {
813    !account_fields.is_empty() || !account_changes.storage_changes.is_empty()
814}
815
816/// Returns [`MultiProofTargetsV2`] for withdrawal addresses.
817///
818/// Withdrawals only modify account balances (no storage), so the targets contain
819/// only account-level entries with empty storage sets.
820fn multiproof_targets_from_withdrawals(withdrawals: &[Withdrawal]) -> MultiProofTargetsV2 {
821    MultiProofTargetsV2 {
822        account_targets: withdrawals.iter().map(|w| keccak256(w.address).into()).collect(),
823        ..Default::default()
824    }
825}
826
827#[cfg(test)]
828mod tests {
829    use super::*;
830    use alloy_consensus::transaction::Recovered;
831    use alloy_eip7928::{
832        AccountChanges, BalanceChange, BlockAccessIndex, CodeChange, NonceChange, SlotChanges,
833        StorageChange,
834    };
835    use alloy_primitives::{address, bytes};
836    use reth_chainspec::ChainSpec;
837    use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
838    use reth_evm::{execute::WithTxEnv, TxEnvFor};
839    use reth_evm_ethereum::EthEvmConfig;
840    use reth_provider::test_utils::MockEthProvider;
841    use reth_storage_overlay::OverlayManager;
842
843    #[test]
844    fn terminate_event_stops_transaction_execution() {
845        let terminate_execution = Arc::new(AtomicBool::new(false));
846        let ctx = PrewarmContext {
847            env: ExecutionEnv::test_default(),
848            evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
849            saved_cache: None,
850            provider: StateProviderBuilder::<EthPrimitives, _>::new(
851                MockEthProvider::default(),
852                B256::ZERO,
853                OverlayManager::default(),
854            ),
855            bal_prewarm_pool: None,
856            metrics: PrewarmMetrics::default(),
857            cache_metrics: None,
858            cache_state_metrics: None,
859            terminate_execution: Arc::clone(&terminate_execution),
860            executed_tx_index: Arc::new(AtomicUsize::new(0)),
861            precompile_cache_disabled: false,
862            precompile_cache_map: PrecompileCacheMap::default(),
863            disable_bal_parallel_state_root: false,
864            disable_bal_batch_io: false,
865        };
866        let (task, actions_tx) =
867            PrewarmCacheTask::new(Runtime::test(), PayloadExecutionCache::default(), ctx);
868        actions_tx
869            .send(PrewarmTaskEvent::Terminate {
870                execution_outcome: None,
871                valid_block_rx: mpsc::channel().1,
872            })
873            .unwrap();
874
875        task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
876            PrewarmMode::Skipped,
877            actions_tx,
878        );
879
880        assert!(terminate_execution.load(Ordering::Relaxed));
881    }
882
883    #[test]
884    fn bal_read_only_account_does_not_change_state_root() {
885        let changes = AccountChanges::new(address!("0000000000000000000000000000000000000001"))
886            .with_storage_read(U256::from(1));
887        let fields = BalAccountStateFields::from_changes(&changes);
888
889        assert!(fields.is_empty());
890        assert!(!bal_account_changes_state_root(&changes, fields));
891    }
892
893    #[test]
894    fn bal_account_with_all_leaf_fields_does_not_need_parent_account() {
895        let changes = AccountChanges::new(address!("0000000000000000000000000000000000000001"))
896            .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::from(10)))
897            .with_nonce_change(NonceChange::new(BlockAccessIndex::new(1), 7))
898            .with_code_change(CodeChange::new(BlockAccessIndex::new(1), bytes!("6001600155")));
899        let fields = BalAccountStateFields::from_changes(&changes);
900
901        assert!(bal_account_changes_state_root(&changes, fields));
902        assert!(!fields.needs_parent_account());
903    }
904
905    #[test]
906    fn bal_storage_change_needs_parent_account_when_leaf_fields_missing() {
907        let changes = AccountChanges::new(address!("0000000000000000000000000000000000000001"))
908            .with_storage_change(SlotChanges::new(
909                U256::from(1),
910                vec![StorageChange::new(BlockAccessIndex::new(1), U256::from(2))],
911            ));
912        let fields = BalAccountStateFields::from_changes(&changes);
913
914        assert!(bal_account_changes_state_root(&changes, fields));
915        assert!(fields.needs_parent_account());
916    }
917
918    #[test]
919    fn bal_account_uses_existing_fields_only_when_missing() {
920        let changes = AccountChanges::new(address!("0000000000000000000000000000000000000001"))
921            .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::from(10)));
922        let fields = BalAccountStateFields::from_changes(&changes);
923        let account = fields.into_account(Some(Account {
924            balance: U256::from(1),
925            nonce: 3,
926            bytecode_hash: Some(B256::repeat_byte(0xaa)),
927        }));
928
929        assert_eq!(account.balance, U256::from(10));
930        assert_eq!(account.nonce, 3);
931        assert_eq!(account.bytecode_hash, Some(B256::repeat_byte(0xaa)));
932    }
933}
934
935/// The events the pre-warm task can handle.
936///
937/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the main
938/// execution path without cloning the expensive `BundleState`.
939#[derive(Debug)]
940pub enum PrewarmTaskEvent<R> {
941    /// Signals the prewarm workers to stop executing further transactions.
942    ///
943    /// This only sets the termination flag the workers poll; the task keeps running to save the
944    /// cache. Sent once the authoritative execution no longer needs prewarming, so the workers do
945    /// not race ahead on transactions that will never be used.
946    TerminateTransactionExecution,
947    /// Tears the whole task down: stops the workers, optionally saves the warmed cache from the
948    /// final output, and exits.
949    ///
950    /// Sent when execution completed successfully (carrying the output to save) or when the task
951    /// handle is dropped (carrying no output, e.g. after an execution error). Handling this event
952    /// also stops the workers, since a teardown may arrive without a preceding
953    /// [`TerminateTransactionExecution`](Self::TerminateTransactionExecution).
954    Terminate {
955        /// The final execution outcome, or `None` when the task is torn down without one (e.g. a
956        /// dropped handle). Using `Arc` allows sharing with the main execution path without
957        /// cloning the expensive `BundleState`.
958        execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
959        /// Receiver for the block validation result.
960        ///
961        /// Cache saving is racing the state root validation. We optimistically construct the
962        /// updated cache but only save it once we know the block is valid.
963        valid_block_rx: mpsc::Receiver<()>,
964    },
965    /// Emitted by the worker-dispatch side once every dispatched transaction has finished or been
966    /// cancelled, reporting how many were executed.
967    FinishedTxExecution {
968        /// Number of transactions executed
969        executed_transactions: usize,
970    },
971}
972
973/// Metrics for transactions prewarming.
974#[derive(Metrics, Clone)]
975#[metrics(scope = "sync.prewarm")]
976pub struct PrewarmMetrics {
977    /// The number of transactions to prewarm
978    pub(crate) transactions: Gauge,
979    /// A histogram of the number of transactions to prewarm
980    pub(crate) transactions_histogram: Histogram,
981    /// A histogram of duration per transaction prewarming
982    pub(crate) total_runtime: Histogram,
983    /// A histogram of EVM execution duration per transaction prewarming
984    pub(crate) execution_duration: Histogram,
985    /// A histogram for prefetch targets per transaction prewarming
986    pub(crate) prefetch_storage_targets: Histogram,
987    /// A histogram of duration for cache saving
988    pub(crate) cache_saving_duration: Gauge,
989    /// Counter for transaction execution errors during prewarming
990    pub(crate) transaction_errors: Counter,
991    /// A histogram of BAL slot iteration duration during prefetching
992    pub(crate) bal_slot_iteration_duration: Histogram,
993}