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