1use super::{
17 bal_prewarm_pool::BalPrewarmPool,
18 reuse::{BalanceTracker, PoolTx, PrewarmResults, RecordingDb, SpeculativeTx},
19 StateRootHintStream, StateRootUpdateStream,
20};
21use crate::tree::{
22 precompile_cache::{CachedPrecompile, PrecompileCacheMap},
23 CachedStateCacheMetrics, CachedStateMetrics, CachedStateProvider, ExecutionEnv,
24 PayloadExecutionCache, SavedCache,
25};
26use alloy_consensus::transaction::TxHashRef;
27use alloy_eip7928::bal::DecodedBal;
28use alloy_eips::eip4895::Withdrawal;
29use alloy_primitives::keccak256;
30use metrics::{Counter, Gauge, Histogram};
31use rayon::prelude::*;
32use reth_evm::{
33 execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, HaltReasonFor, RecoveredTx, SpecFor,
34};
35use reth_metrics::Metrics;
36use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
37use reth_provider::{
38 BlockExecutionOutput, BlockNumReader, ChangeSetReader, DatabaseProviderFactory,
39 DatabaseProviderROFactory, EvmStateProvider, EvmStateProviderBox, HistoryReader,
40 PruneCheckpointReader, StageCheckpointReader, StateProvider, StorageChangeSetReader,
41 StorageSettingsCache,
42};
43use reth_revm::database::StateProviderDatabase;
44use reth_storage_overlay::OverlayStateProviderFactory;
45use reth_tasks::{pool::WorkerPool, Runtime};
46use reth_trie_common::MultiProofTargetsV2;
47use revm::{context::Transaction as _, state::EvmState};
48use std::{
49 sync::{
50 atomic::{AtomicBool, AtomicUsize, Ordering},
51 mpsc::{self, channel, Receiver, Sender},
52 Arc,
53 },
54 time::Duration,
55};
56use tokio::sync::oneshot;
57use tracing::{debug, debug_span, instrument, trace, trace_span, warn, Span};
58
59const MAX_REFRESH_ROUNDS: usize = 4;
61
62const REFRESH_WINDOW: usize = 64;
65
66const REFRESH_POLL_INTERVAL: Duration = Duration::from_micros(50);
68
69#[derive(Debug)]
74pub enum PrewarmMode<Tx> {
75 Transactions {
77 pending: Receiver<(usize, Tx)>,
79 hints: Option<StateRootHintStream>,
81 },
82 BlockAccessList {
84 bal: Arc<DecodedBal>,
86 updates: Option<StateRootUpdateStream>,
88 },
89 Skipped,
92}
93
94#[derive(Debug)]
99pub struct PrewarmCacheTask<N, P, Evm>
100where
101 N: NodePrimitives,
102 Evm: ConfigureEvm<Primitives = N>,
103{
104 executor: Runtime,
106 execution_cache: PayloadExecutionCache,
108 ctx: PrewarmContext<N, P, Evm>,
110 actions_rx: Receiver<PrewarmTaskEvent<N::Receipt>>,
112 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 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 fn spawn_txs_prewarm<Tx>(
158 &self,
159 pending: mpsc::Receiver<(usize, Tx)>,
160 actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
161 state_root_hint_stream: Option<StateRootHintStream>,
162 ) where
163 Tx: ExecutableTxFor<Evm> + Clone + Send + Sync + 'static,
164 {
165 let executor = self.executor.clone();
166 let ctx = self.ctx.clone();
167 let span = Span::current();
168
169 self.executor.spawn_blocking_named("prewarm-txs", move || {
170 let _enter = debug_span!(
171 target: "engine::tree::payload_processor::prewarm",
172 parent: &span,
173 "prewarm_txs"
174 )
175 .entered();
176
177 let ctx = &ctx;
178 let pool = executor.prewarming_pool();
179
180 let mut tx_count = 0usize;
181 let state_root_hint_stream = state_root_hint_stream.as_ref();
182 let mut refresh_txs = Vec::new();
184 pool.in_place_scope(|s| {
185 s.spawn(|_| {
186 pool.init::<PrewarmEvmState<Evm>>(|_| ctx.evm_for_ctx());
187 });
188
189 while let Ok((index, tx)) = pending.recv() {
190 if ctx.should_stop() {
191 trace!(
192 target: "engine::tree::payload_processor::prewarm",
193 "Termination requested, stopping transaction distribution"
194 );
195 break;
196 }
197
198 if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
200 continue;
201 }
202
203 tx_count += 1;
204 if ctx.prewarm_results.is_some() {
205 refresh_txs.push((index, tx.clone()));
206 }
207 let seeded = if let Some(results) = &ctx.prewarm_results &&
209 let Some(seeds) = &ctx.env.pool_seeds
210 {
211 results.seed(index, tx.tx().tx_hash(), seeds)
212 } else {
213 None
214 };
215 if seeded.is_some() {
216 ctx.metrics.seeded_transactions.increment(1);
217 }
218 let parent_span = Span::current();
219 s.spawn(move |_| {
220 let _enter = trace_span!(
221 target: "engine::tree::payload_processor::prewarm",
222 parent: parent_span,
223 "prewarm_tx",
224 i = index,
225 )
226 .entered();
227 Self::transact_worker(ctx, index, tx, seeded, state_root_hint_stream);
228 });
229 }
230
231 if let Some(state_root_hint_stream) = state_root_hint_stream &&
233 let Some(withdrawals) = &ctx.env.withdrawals &&
234 !withdrawals.is_empty()
235 {
236 let targets = multiproof_targets_from_withdrawals(withdrawals);
237 state_root_hint_stream.on_access_hint(targets.into());
238 }
239 });
240
241 if let Some(results) = &ctx.prewarm_results {
242 Self::refresh_results(ctx, pool, results, &refresh_txs);
243 }
244
245 pool.clear();
247
248 let _ = actions_tx
249 .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: tx_count });
250 });
251 }
252
253 fn refresh_results<Tx>(
261 ctx: &PrewarmContext<N, P, Evm>,
262 pool: &WorkerPool,
263 results: &PrewarmResults<HaltReasonFor<Evm>>,
264 txs: &[(usize, Tx)],
265 ) where
266 Tx: ExecutableTxFor<Evm> + Clone + Send + Sync,
267 {
268 let mut rounds = 0;
269 let mut window_len = usize::MAX;
272 while !ctx.should_stop() {
273 let generation = results.predicted().generation();
274 let next = ctx.executed_tx_index.load(Ordering::Relaxed) + 1;
276 let start = txs.partition_point(|(index, _)| *index <= next);
277 if start == txs.len() {
278 break
279 }
280 let window = &txs[start..txs.len().min(start.saturating_add(window_len))];
281 let stale = window
283 .iter()
284 .filter(|(index, _)| results.needs_refresh(*index))
285 .collect::<Vec<_>>();
286 let refreshed = !stale.is_empty();
287 if refreshed {
288 let cursor = AtomicUsize::new(0);
289 pool.in_place_scope(|s| {
290 for _ in 0..pool.current_num_threads().min(stale.len()) {
291 s.spawn(|_| {
292 while let Some((index, tx)) =
293 stale.get(cursor.fetch_add(1, Ordering::Relaxed))
294 {
295 if ctx.should_stop() {
296 break
297 }
298 Self::transact_worker(ctx, *index, tx.clone(), None, None);
299 }
300 });
301 }
302 });
303 }
304
305 rounds += 1;
306 if refreshed && rounds < MAX_REFRESH_ROUNDS {
307 continue
308 }
309 rounds = 0;
310 window_len = REFRESH_WINDOW;
311 while results.predicted().generation() == generation && !ctx.should_stop() {
312 std::thread::sleep(REFRESH_POLL_INTERVAL);
313 }
314 }
315 }
316
317 fn transact_worker<Tx>(
323 ctx: &PrewarmContext<N, P, Evm>,
324 index: usize,
325 tx: Tx,
326 seeded: Option<Arc<PoolTx<HaltReasonFor<Evm>>>>,
327 state_root_hint_stream: Option<&StateRootHintStream>,
328 ) where
329 Tx: ExecutableTxFor<Evm>,
330 {
331 WorkerPool::with_worker_mut(|worker| {
332 if ctx.should_stop() {
333 return;
334 }
335
336 if index < ctx.executed_tx_index.load(Ordering::Relaxed) {
338 return;
339 }
340
341 let state = worker.get_or_init::<PrewarmEvmState<Evm>>(|| ctx.evm_for_ctx());
342 if !Arc::ptr_eq(&state.run, &ctx.terminate_execution) {
346 *state = ctx.evm_for_ctx();
347 }
348 let Some(evm) = state.evm.as_mut() else { return };
349
350 let start = Instant::now();
351
352 let (tx_env, tx) = tx.into_parts();
353 let send_hints = |state: &EvmState| {
354 if index > 0 &&
355 let Some(state_root_hint_stream) = state_root_hint_stream
356 {
357 let (targets, storage_targets) = MultiProofTargetsV2::from_state(state);
358 ctx.metrics.prefetch_storage_targets.record(storage_targets as f64);
359 state_root_hint_stream.on_access_hint(targets.into());
360 }
361 };
362
363 if let Some(seeded) = seeded {
364 if let Err(err) = seeded.load_reads(evm.db_mut()) {
366 trace!(
367 target: "engine::tree::payload_processor::prewarm",
368 %err,
369 tx_hash=%tx.tx().tx_hash(),
370 "Failed to load reads of seeded pool transaction",
371 );
372 }
373 send_hints(seeded.state());
374 return;
375 }
376
377 let speculative = SpeculativeTx {
380 caller: *tx.signer(),
381 nonce: tx_env.nonce(),
382 max_balance_spending: tx_env.max_balance_spending(),
383 };
384 if let Some(results) = &ctx.prewarm_results {
385 evm.db_mut().start_recording(Arc::clone(results.predicted()), index);
386 evm.inspector_mut().clear();
387 }
388 let res = evm.transact(tx_env);
389 let reads = evm.db_mut().take_reads();
390 let res = match res {
391 Ok(res) => res,
392 Err(err) => {
393 trace!(
394 target: "engine::tree::payload_processor::prewarm",
395 %err,
396 tx_hash=%tx.tx().tx_hash(),
397 sender=%tx.signer(),
398 "Error when executing prewarm transaction",
399 );
400 ctx.metrics.transaction_errors.increment(1);
401 if let Some(results) = &ctx.prewarm_results &&
402 let Some(reads) = reads
403 {
404 results.reject(index, reads);
405 }
406 return;
407 }
408 };
409 ctx.metrics.execution_duration.record(start.elapsed());
410
411 if ctx.should_stop() {
412 return;
413 }
414
415 send_hints(&res.state);
416
417 if let Some(results) = &ctx.prewarm_results &&
418 let Some(reads) = reads &&
419 index >= ctx.executed_tx_index.load(Ordering::Relaxed)
420 {
421 results.store(index, res, reads, evm.inspector_mut(), speculative);
422 }
423
424 ctx.metrics.total_runtime.record(start.elapsed());
425 });
426 }
427
428 #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
444 fn save_cache(
445 self,
446 execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
447 valid_block_rx: mpsc::Receiver<()>,
448 ) {
449 let start = Instant::now();
450
451 let Self {
452 execution_cache,
453 ctx: PrewarmContext { env, metrics, cache_state_metrics, saved_cache, .. },
454 ..
455 } = self;
456 let hash = env.hash;
457
458 if let Some(saved_cache) = saved_cache {
459 debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
460 let (previous, rejected) = execution_cache.update_with_guard(|cached| {
461 let new_cache = SavedCache::new(hash, saved_cache.into_cache());
462
463 if new_cache.cache().insert_state(&execution_outcome.state).is_err() {
465 debug!(target: "engine::caching", "cleared execution cache on update error");
466 return (cached.take(), Some(new_cache));
467 }
468
469 new_cache.update_metrics(cache_state_metrics.as_ref());
470
471 if valid_block_rx.recv().is_err() {
475 debug!(target: "engine::caching", "cleared execution cache on invalid block");
476 return (cached.take(), Some(new_cache));
477 }
478
479 let reused =
480 cached.as_ref().is_some_and(|previous| previous.shares_cache_with(&new_cache));
481 let previous = cached.replace(new_cache);
482 if reused {
483 drop(previous);
486 (None, None)
487 } else {
488 (previous, None)
490 }
491 });
492
493 drop((previous, rejected));
498
499 let elapsed = start.elapsed();
500 debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
501
502 metrics.cache_saving_duration.set(elapsed.as_secs_f64());
503 }
504 }
505
506 #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
514 fn run_bal_prewarm(
515 &self,
516 decoded_bal: Arc<DecodedBal>,
517 actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
518 hashed_update_stream: Option<StateRootUpdateStream>,
519 ) {
520 let bal = decoded_bal.as_bal();
521 if bal.is_empty() {
522 if let Some(hashed_update_stream) = hashed_update_stream {
523 hashed_update_stream.finish();
524 }
525 let _ =
526 actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
527 return;
528 }
529
530 trace!(
531 target: "engine::tree::payload_processor::prewarm",
532 accounts = bal.len(),
533 "Starting BAL prewarm"
534 );
535
536 let ctx = self.ctx.clone();
537 let executor = self.executor.clone();
538 let parent_span = Span::current();
539 let stream_parent_span = parent_span;
540 let prefetch_bal = Arc::clone(&decoded_bal);
541 let stream_bal = Arc::clone(&decoded_bal);
542 let (stream_tx, stream_rx) = oneshot::channel();
543
544 if let Some(hashed_update_stream) = hashed_update_stream {
545 let ctx = ctx.clone();
546 executor.bal_streaming_pool().spawn(move || {
547 let branch_span = debug_span!(
548 target: "engine::tree::payload_processor::prewarm",
549 parent: &stream_parent_span,
550 "bal_hashed_state_stream",
551 bal_accounts = stream_bal.as_bal().len(),
552 );
553 let parent_span = branch_span.clone();
554 let _span = branch_span.entered();
555
556 stream_bal.as_bal().par_iter().for_each(|account_changes| {
557 WorkerPool::with_worker_mut(|worker| {
558 let provider = worker.get_or_init::<Option<EvmStateProviderBox>>(|| None);
559 ctx.send_bal_hashed_state(
560 &parent_span,
561 provider,
562 account_changes,
563 &hashed_update_stream,
564 );
565 });
566 });
567
568 hashed_update_stream.finish();
569 let _ = stream_tx.send(());
570 });
571 } else {
572 let _ = stream_tx.send(());
573 }
574
575 if let Some(saved_cache) = ctx.saved_cache &&
576 !ctx.disable_bal_batch_io &&
577 let Some(pool) = ctx.bal_prewarm_pool.as_ref()
578 {
579 let caches = saved_cache.cache().clone();
591 let state_provider_factory = ctx.provider.clone();
592 let build = Arc::new(move || {
593 state_provider_factory.database_provider_ro().map(|provider| {
594 Box::new(provider.into_evm_state_provider()) as EvmStateProviderBox
595 })
596 });
597
598 pool.begin_block(build, caches, ctx.env.txpool_snapshot.clone());
599 let dispatch_start = Instant::now();
600 for account in prefetch_bal.as_bal() {
601 pool.warm_account(account.address, account.storage_slots().map(Into::into));
602 }
603 ctx.metrics.bal_slot_iteration_duration.record(dispatch_start.elapsed());
604 pool.end_block();
605 }
606
607 stream_rx
608 .blocking_recv()
609 .expect("BAL hashed-state streaming task dropped without signaling completion");
610
611 executor.bal_streaming_pool().clear();
613
614 let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
615 }
616
617 #[instrument(
622 parent = &self.parent_span,
623 level = "debug",
624 target = "engine::tree::payload_processor::prewarm",
625 name = "prewarm and caching",
626 skip_all
627 )]
628 pub fn run<Tx>(self, mode: PrewarmMode<Tx>, actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>)
629 where
630 Tx: ExecutableTxFor<Evm> + Clone + Send + Sync + 'static,
631 {
632 match mode {
636 PrewarmMode::Transactions { pending, hints } => {
637 self.spawn_txs_prewarm(pending, actions_tx, hints);
638 }
639 PrewarmMode::BlockAccessList { bal, updates } => {
640 self.run_bal_prewarm(bal, actions_tx, updates);
641 }
642 PrewarmMode::Skipped => {
643 let _ = actions_tx
644 .send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
645 }
646 }
647
648 let mut final_execution_outcome = None;
649 let mut finished_execution = false;
650 while let Ok(event) = self.actions_rx.recv() {
651 match event {
652 PrewarmTaskEvent::TerminateTransactionExecution => {
653 debug!(target: "engine::tree::prewarm", "Terminating prewarm execution");
655 self.ctx.stop();
656 }
657 PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx } => {
658 trace!(target: "engine::tree::payload_processor::prewarm", "Received termination signal");
659 self.ctx.stop();
662 final_execution_outcome =
663 Some(execution_outcome.map(|outcome| (outcome, valid_block_rx)));
664
665 if finished_execution {
666 break
668 }
669 }
670 PrewarmTaskEvent::FinishedTxExecution { executed_transactions } => {
671 trace!(target: "engine::tree::payload_processor::prewarm", "Finished prewarm execution signal");
672 self.ctx.metrics.transactions.set(executed_transactions as f64);
673 self.ctx.metrics.transactions_histogram.record(executed_transactions as f64);
674
675 finished_execution = true;
676
677 if final_execution_outcome.is_some() {
678 break
680 }
681 }
682 }
683 }
684
685 debug!(target: "engine::tree::payload_processor::prewarm", "Completed prewarm execution");
686
687 if let Some(Some((execution_outcome, valid_block_rx))) = final_execution_outcome {
689 self.save_cache(execution_outcome, valid_block_rx);
690 }
691 }
692}
693
694#[derive(Debug, Clone)]
696pub struct PrewarmContext<N, P, Evm>
697where
698 N: NodePrimitives,
699 Evm: ConfigureEvm<Primitives = N>,
700{
701 pub env: ExecutionEnv<Evm>,
703 pub evm_config: Evm,
705 pub saved_cache: Option<SavedCache>,
707 pub provider: OverlayStateProviderFactory<P, N>,
709 pub(crate) bal_prewarm_pool: Option<Arc<BalPrewarmPool>>,
712 pub metrics: PrewarmMetrics,
714 pub cache_metrics: Option<CachedStateMetrics>,
717 pub cache_state_metrics: Option<CachedStateCacheMetrics>,
719 pub terminate_execution: Arc<AtomicBool>,
721 pub executed_tx_index: Arc<AtomicUsize>,
725 pub precompile_cache_disabled: bool,
727 pub precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
729 pub disable_bal_parallel_state_root: bool,
732 pub disable_bal_batch_io: bool,
734 pub(crate) prewarm_results: Option<Arc<PrewarmResults<HaltReasonFor<Evm>>>>,
736}
737
738struct PrewarmEvmState<Evm: ConfigureEvm> {
741 run: Arc<AtomicBool>,
743 evm: Option<
745 EvmFor<Evm, RecordingDb<StateProviderDatabase<EvmStateProviderBox>>, BalanceTracker>,
746 >,
747}
748
749impl<N, P, Evm> PrewarmContext<N, P, Evm>
750where
751 N: NodePrimitives,
752 P: DatabaseProviderFactory,
753 P::Provider: BlockNumReader
754 + PruneCheckpointReader
755 + StageCheckpointReader
756 + ChangeSetReader
757 + StorageChangeSetReader
758 + StorageSettingsCache
759 + HistoryReader
760 + 'static,
761 Evm: ConfigureEvm<Primitives = N> + 'static,
762{
763 #[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
765 fn evm_for_ctx(&self) -> PrewarmEvmState<Evm> {
766 let mut state_provider = match self.provider.database_provider_ro() {
767 Ok(provider) => Box::new(provider.into_evm_state_provider()) as EvmStateProviderBox,
768 Err(err) => {
769 trace!(
770 target: "engine::tree::payload_processor::prewarm",
771 %err,
772 "Failed to build state provider in prewarm thread"
773 );
774 return PrewarmEvmState { run: Arc::clone(&self.terminate_execution), evm: None }
775 }
776 };
777
778 if let Some(saved_cache) = &self.saved_cache {
780 let caches = saved_cache.cache().clone();
781 state_provider = Box::new(
782 CachedStateProvider::new_prewarm(state_provider, caches)
783 .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
784 );
785 }
786
787 let state_provider = StateProviderDatabase::new(state_provider);
788
789 let mut evm_env = self.env.evm_env.clone();
790
791 evm_env.cfg_env.disable_nonce_check = true;
794
795 evm_env.cfg_env.disable_balance_check = true;
798
799 let spec_id = *evm_env.spec_id();
801 let reuse = self.prewarm_results.is_some();
804 let mut evm_config = self.evm_config.clone();
805 if reuse {
806 evm_config = evm_config.with_jit_support_enabled(false);
807 }
808 let mut evm = evm_config.evm_with_env_and_inspector(
809 RecordingDb::new(state_provider),
810 evm_env,
811 BalanceTracker::default(),
812 );
813 evm.set_inspector_enabled(reuse);
814
815 if !self.precompile_cache_disabled {
816 evm.precompiles_mut().map_cacheable_precompiles(|address, precompile| {
818 CachedPrecompile::wrap(
819 precompile,
820 self.precompile_cache_map.cache_for_address(*address),
821 spec_id,
822 None, )
824 });
825 }
826
827 PrewarmEvmState { run: Arc::clone(&self.terminate_execution), evm: Some(evm) }
828 }
829
830 #[inline]
832 pub fn should_stop(&self) -> bool {
833 self.terminate_execution.load(Ordering::Relaxed)
834 }
835
836 #[inline]
838 pub fn stop(&self) {
839 self.terminate_execution.store(true, Ordering::Relaxed);
840 }
841
842 fn send_bal_hashed_state(
852 &self,
853 parent_span: &Span,
854 provider: &mut Option<EvmStateProviderBox>,
855 account_changes: &alloy_eip7928::AccountChanges,
856 hashed_update_stream: &StateRootUpdateStream,
857 ) {
858 if self.disable_bal_parallel_state_root {
859 return;
860 }
861 let address = account_changes.address;
862 let mut hashed_address = None;
863 let account_info = account_changes.account_info();
864
865 if !account_info.changes_state_root(account_changes) {
866 return;
867 }
868
869 if account_changes.has_storage_changes() {
873 let hashed_address = *hashed_address.get_or_insert_with(|| keccak256(address));
874 let storage_map = reth_trie::HashedStorage::from_iter(
875 account_changes
876 .storage_post_states()
877 .map(|(slot, value)| (keccak256(slot.to_be_bytes::<32>()), value)),
878 );
879
880 let mut hashed_state = reth_trie::HashedPostState::default();
881 hashed_state.storages.insert(hashed_address, storage_map);
882 hashed_update_stream.on_hashed_state_update(hashed_state);
883 }
884
885 let existing_account = if account_info.is_complete() {
886 None
887 } else {
888 if provider.is_none() {
889 let _span = debug_span!(
890 target: "engine::tree::payload_processor::prewarm",
891 parent: parent_span,
892 "bal_hashed_state_provider_init",
893 has_saved_cache = !self.disable_bal_batch_io && self.saved_cache.is_some(),
894 )
895 .entered();
896
897 let inner = match self.provider.database_provider_ro() {
898 Ok(p) => p.into_evm_state_provider(),
899 Err(err) => {
900 warn!(
901 target: "engine::tree::payload_processor::prewarm",
902 ?err,
903 "Failed to build provider for BAL account reads"
904 );
905 return;
906 }
907 };
908 let boxed: EvmStateProviderBox =
909 match (self.disable_bal_batch_io, &self.saved_cache) {
910 (false, Some(saved)) => {
911 let caches = saved.cache().clone();
912 Box::new(
913 CachedStateProvider::new_prewarm(inner, caches)
914 .with_txpool_snapshot(self.env.txpool_snapshot.clone()),
915 )
916 }
917 _ => Box::new(inner),
918 };
919 *provider = Some(boxed);
920 }
921 let account_reader = provider.as_ref().expect("provider just initialized");
922 account_reader.basic_account(&address).ok().flatten()
923 };
924
925 let mut account = existing_account.unwrap_or_default();
926 account.apply_bal_info(account_info);
927 let hashed_address = hashed_address.unwrap_or_else(|| keccak256(address));
928
929 let account = (!account.is_empty()).then_some(account);
941
942 let mut hashed_state = reth_trie::HashedPostState::default();
943 hashed_state.accounts.insert(hashed_address, account);
944 hashed_update_stream.on_hashed_state_update(hashed_state);
945 }
946}
947
948fn multiproof_targets_from_withdrawals(withdrawals: &[Withdrawal]) -> MultiProofTargetsV2 {
953 MultiProofTargetsV2 {
954 account_targets: withdrawals.iter().map(|w| keccak256(w.address).into()).collect(),
955 ..Default::default()
956 }
957}
958
959#[derive(Debug)]
964pub enum PrewarmTaskEvent<R> {
965 TerminateTransactionExecution,
971 Terminate {
979 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
983 valid_block_rx: mpsc::Receiver<()>,
988 },
989 FinishedTxExecution {
992 executed_transactions: usize,
994 },
995}
996
997#[derive(Metrics, Clone)]
999#[metrics(scope = "sync.prewarm")]
1000pub struct PrewarmMetrics {
1001 pub(crate) transactions: Gauge,
1003 pub(crate) transactions_histogram: Histogram,
1005 pub(crate) total_runtime: Histogram,
1007 pub(crate) execution_duration: Histogram,
1009 pub(crate) prefetch_storage_targets: Histogram,
1011 pub(crate) cache_saving_duration: Gauge,
1014 pub(crate) transaction_errors: Counter,
1016 pub(crate) seeded_transactions: Counter,
1019 pub(crate) bal_slot_iteration_duration: Histogram,
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025
1026 use super::*;
1027 use alloy_consensus::{transaction::Recovered, TxLegacy};
1028 use alloy_eips::eip7702::constants::EIP7702_CLEARED_DELEGATION;
1029 use alloy_primitives::{Address, Signature, TxKind, B256, U256};
1030 use reth_chainspec::ChainSpec;
1031 use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
1032 use reth_evm::{execute::WithTxEnv, TxEnvFor};
1033 use reth_evm_ethereum::EthEvmConfig;
1034 use reth_provider::test_utils::MockEthProvider;
1035 use reth_stages_api::{StageCheckpoint, StageId};
1036 use reth_storage_overlay::OverlayManager;
1037 use revm::database::EmptyDB;
1038
1039 #[test]
1040 fn terminate_event_stops_transaction_execution() {
1041 let terminate_execution = Arc::new(AtomicBool::new(false));
1042 let ctx = PrewarmContext {
1043 env: ExecutionEnv::test_default(),
1044 evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1045 saved_cache: None,
1046 provider: OverlayStateProviderFactory::new(
1047 MockEthProvider::default(),
1048 OverlayManager::default().overlay_builder(B256::ZERO),
1049 ),
1050 bal_prewarm_pool: None,
1051 metrics: PrewarmMetrics::default(),
1052 cache_metrics: None,
1053 cache_state_metrics: None,
1054 terminate_execution: Arc::clone(&terminate_execution),
1055 executed_tx_index: Arc::new(AtomicUsize::new(0)),
1056 precompile_cache_disabled: false,
1057 precompile_cache_map: PrecompileCacheMap::default(),
1058 disable_bal_parallel_state_root: false,
1059 disable_bal_batch_io: false,
1060 prewarm_results: None,
1061 };
1062 let (task, actions_tx) =
1063 PrewarmCacheTask::new(Runtime::test(), PayloadExecutionCache::default(), ctx);
1064 actions_tx
1065 .send(PrewarmTaskEvent::Terminate {
1066 execution_outcome: None,
1067 valid_block_rx: mpsc::channel().1,
1068 })
1069 .unwrap();
1070
1071 task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
1072 PrewarmMode::Skipped,
1073 actions_tx,
1074 );
1075
1076 assert!(terminate_execution.load(Ordering::Relaxed));
1077 }
1078
1079 #[test]
1080 fn rebuilds_evm_for_each_prewarm_run() {
1081 let caller = Address::repeat_byte(1);
1082 let provider = MockEthProvider::default();
1083 provider.enable_database_provider();
1084 provider.add_header(B256::ZERO, Default::default());
1085 provider.add_stage_checkpoint(StageId::Finish, StageCheckpoint::new(0));
1086 let tx = Recovered::new_unchecked(
1087 TransactionSigned::new_unhashed(
1088 TxLegacy {
1090 gas_limit: 21_000,
1091 to: TxKind::Call(Address::repeat_byte(2)),
1092 ..Default::default()
1093 }
1094 .into(),
1095 Signature::test_signature(),
1096 ),
1097 caller,
1098 );
1099 let pool = WorkerPool::new(1, "prewarm-test");
1100 let prewarm = |beneficiary| {
1102 let mut env = ExecutionEnv::<EthEvmConfig>::test_default();
1103 env.evm_env.block_env.beneficiary = beneficiary;
1104 let results = Arc::new(PrewarmResults::new(1));
1105 let ctx = PrewarmContext {
1106 env,
1107 evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1108 saved_cache: None,
1109 provider: OverlayStateProviderFactory::new(
1110 provider.clone(),
1111 OverlayManager::default().overlay_builder(B256::ZERO),
1112 ),
1113 bal_prewarm_pool: None,
1114 metrics: PrewarmMetrics::default(),
1115 cache_metrics: None,
1116 cache_state_metrics: None,
1117 terminate_execution: Arc::new(AtomicBool::new(false)),
1118 executed_tx_index: Arc::new(AtomicUsize::new(0)),
1119 precompile_cache_disabled: false,
1120 precompile_cache_map: PrecompileCacheMap::default(),
1121 disable_bal_parallel_state_root: false,
1122 disable_bal_batch_io: false,
1123 prewarm_results: Some(Arc::clone(&results)),
1124 };
1125 let tx = WithTxEnv { tx_env: ctx.evm_config.tx_env(&tx), tx: Arc::new(tx.clone()) };
1126 pool.install_fn(|| {
1127 PrewarmCacheTask::<EthPrimitives, MockEthProvider, EthEvmConfig>::transact_worker(
1128 &ctx, 0, tx, None, None,
1129 )
1130 });
1131 results.take_valid(0, &mut EmptyDB::default()).unwrap().unwrap().state
1132 };
1133
1134 assert!(prewarm(Address::repeat_byte(0xa)).contains_key(&Address::repeat_byte(0xa)));
1135 let state = prewarm(Address::repeat_byte(0xb));
1136 assert!(state.contains_key(&Address::repeat_byte(0xb)));
1137 assert!(!state.contains_key(&Address::repeat_byte(0xa)));
1138 }
1139
1140 fn test_prewarm_context(
1141 saved_cache: SavedCache,
1142 saving_duration: Gauge,
1143 ) -> PrewarmContext<EthPrimitives, MockEthProvider, EthEvmConfig> {
1144 PrewarmContext {
1145 env: ExecutionEnv { hash: B256::repeat_byte(2), ..ExecutionEnv::test_default() },
1146 evm_config: EthEvmConfig::new(Arc::new(ChainSpec::default())),
1147 saved_cache: Some(saved_cache),
1148 provider: OverlayStateProviderFactory::new(
1149 MockEthProvider::default(),
1150 OverlayManager::default().overlay_builder(B256::ZERO),
1151 ),
1152 bal_prewarm_pool: None,
1153 metrics: PrewarmMetrics {
1154 cache_saving_duration: saving_duration,
1155 ..Default::default()
1156 },
1157 cache_metrics: None,
1158 cache_state_metrics: None,
1159 terminate_execution: Arc::new(AtomicBool::new(false)),
1160 executed_tx_index: Arc::new(AtomicUsize::new(0)),
1161 precompile_cache_disabled: false,
1162 precompile_cache_map: PrecompileCacheMap::default(),
1163 disable_bal_parallel_state_root: false,
1164 disable_bal_batch_io: false,
1165 prewarm_results: None,
1166 }
1167 }
1168
1169 fn save_test_cache(
1170 runtime: &Runtime,
1171 execution_cache: &PayloadExecutionCache,
1172 saved_cache: SavedCache,
1173 state: reth_revm::db::BundleState,
1174 valid: bool,
1175 saving_duration: Gauge,
1176 ) {
1177 let ctx = test_prewarm_context(saved_cache, saving_duration);
1178 let (task, _) = PrewarmCacheTask::new(runtime.clone(), execution_cache.clone(), ctx);
1179 let (valid_tx, valid_rx) = mpsc::channel();
1180 if valid {
1181 valid_tx.send(()).unwrap();
1182 }
1183 drop(valid_tx);
1184 task.save_cache(
1185 Arc::new(BlockExecutionOutput { state, result: Default::default() }),
1186 valid_rx,
1187 );
1188 }
1189
1190 struct CacheSaveObserver {
1193 cache: PayloadExecutionCache,
1194 observed: Arc<AtomicBool>,
1195 }
1196
1197 impl metrics::GaugeFn for CacheSaveObserver {
1198 fn increment(&self, _: f64) {}
1199 fn decrement(&self, _: f64) {}
1200 fn set(&self, _: f64) {
1201 assert!(
1202 self.cache.get_cache_for(B256::repeat_byte(2)).is_some(),
1203 "saved cache must be available before the saving task returns"
1204 );
1205 self.observed.store(true, Ordering::Relaxed);
1206 }
1207 }
1208
1209 #[test]
1210 fn save_cache_releases_warm_cache_before_duration_metric() {
1211 let runtime = Runtime::test();
1212 let execution_cache = PayloadExecutionCache::default();
1213 let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1214 let address = Address::with_last_byte(1);
1215 saved.cache().insert_storage(address, B256::ZERO, Some(U256::from(7)));
1216 execution_cache.update_with_guard(|slot| *slot = Some(saved.clone()));
1217 let (release_tx, release_rx) = mpsc::channel::<()>();
1219 runtime.spawn_blocking_named("drop", move || {
1220 let _ = release_rx.recv();
1221 });
1222 let observed = Arc::new(AtomicBool::new(false));
1223 let observer = Arc::new(CacheSaveObserver {
1224 cache: execution_cache.clone(),
1225 observed: observed.clone(),
1226 });
1227 save_test_cache(
1228 &runtime,
1229 &execution_cache,
1230 saved,
1231 Default::default(),
1232 true,
1233 Gauge::from_arc(observer),
1234 );
1235 assert!(observed.load(Ordering::Relaxed), "save duration was not recorded");
1236 let saved = execution_cache.get_cache_for(B256::repeat_byte(2)).unwrap();
1237 assert_eq!(
1238 saved.cache().get_or_try_insert_storage_with(address, B256::ZERO, || Err(())),
1239 Ok(reth_execution_cache::CachedStatus::Cached(U256::from(7))),
1240 "handoff must preserve the warmed contents",
1241 );
1242 drop(release_tx);
1243 }
1244
1245 #[test]
1246 fn save_cache_blocks_reuse_while_execution_cache_is_cloned() {
1247 let runtime = Runtime::test();
1248 let execution_cache = PayloadExecutionCache::default();
1249 let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1250 execution_cache.update_with_guard(|slot| *slot = Some(saved.clone()));
1251 let other_cache_clone = saved.cache().clone();
1252
1253 save_test_cache(&runtime, &execution_cache, saved, Default::default(), true, Gauge::noop());
1254
1255 assert!(execution_cache.get_cache_for(B256::repeat_byte(2)).is_none());
1256 drop(other_cache_clone);
1257 assert!(execution_cache.get_cache_for(B256::repeat_byte(2)).is_some());
1258 }
1259
1260 #[derive(Clone, Copy, Debug)]
1261 enum CacheSlot {
1262 Empty,
1263 Shared,
1264 Distinct,
1265 }
1266
1267 struct CacheDropProbe {
1269 started: Sender<std::thread::ThreadId>,
1270 inspected: Receiver<()>,
1271 result: Sender<bool>,
1272 }
1273
1274 impl AsRef<[u8]> for CacheDropProbe {
1275 fn as_ref(&self) -> &[u8] {
1276 &EIP7702_CLEARED_DELEGATION
1277 }
1278 }
1279
1280 impl Drop for CacheDropProbe {
1281 fn drop(&mut self) {
1282 let _ = self.started.send(std::thread::current().id());
1283 let unlocked = self.inspected.recv_timeout(std::time::Duration::from_secs(5)).is_ok();
1285 let _ = self.result.send(unlocked);
1286 }
1287 }
1288
1289 fn observe_cache_drop(
1290 saved: &SavedCache,
1291 execution_cache: &PayloadExecutionCache,
1292 expect_saved_cache: bool,
1293 ) -> (Receiver<bool>, std::thread::JoinHandle<std::thread::ThreadId>) {
1294 let (started_tx, started_rx) = mpsc::channel();
1295 let (inspected_tx, inspected_rx) = mpsc::channel();
1296 let (result_tx, result_rx) = mpsc::channel();
1297 let cache = execution_cache.clone();
1298 let reader = std::thread::spawn(move || {
1299 let drop_thread = started_rx.recv().unwrap();
1300 cache.wait_for_availability();
1302 assert_eq!(cache.get_cache_for(B256::repeat_byte(2)).is_some(), expect_saved_cache);
1303 cache.update_with_guard(|slot| assert_eq!(slot.is_some(), expect_saved_cache));
1304 let _ = inspected_tx.send(());
1305 drop_thread
1306 });
1307 let bytes = alloy_primitives::bytes::Bytes::from_owner(CacheDropProbe {
1308 started: started_tx,
1309 inspected: inspected_rx,
1310 result: result_tx,
1311 });
1312 let code = reth_revm::bytecode::Bytecode::new_eip7702_raw(bytes.into()).unwrap();
1313 saved
1314 .cache()
1315 .insert_code(B256::repeat_byte(3), Some(reth_primitives_traits::Bytecode(code)));
1316 (result_rx, reader)
1317 }
1318
1319 #[test]
1320 fn save_cache_freeing_waits_for_validator_cache_references() {
1321 use crate::tree::payload_processor::{CacheTaskHandle, PayloadHandle};
1322
1323 let runtime = Runtime::test();
1324 let execution_cache = PayloadExecutionCache::default();
1325 let saved = SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1326 execution_cache.update_with_guard(|cached| *cached = Some(saved.clone()));
1327 let (dropped, reader) = observe_cache_drop(&saved, &execution_cache, false);
1328 let ctx = test_prewarm_context(saved, Gauge::noop());
1329 let (task, actions_tx) =
1330 PrewarmCacheTask::new(runtime.clone(), execution_cache.clone(), ctx);
1331 let mut payload = PayloadHandle {
1332 prewarm_handle: CacheTaskHandle::<_, ()> {
1333 saved_cache: task.ctx.saved_cache.clone(),
1334 to_prewarm_task: Some(actions_tx.clone()),
1335 executed_tx_index: task.ctx.executed_tx_index.clone(),
1336 prewarm_results: None,
1337 cache_metrics: None,
1338 },
1339 transactions: crossbeam_channel::never::<(usize, Result<(), ()>)>(),
1340 _span: Span::none(),
1341 };
1342 let validator_cache = payload.caches().unwrap();
1344 let valid_block_tx = payload.terminate_caching(Some(Arc::new(BlockExecutionOutput {
1345 state: Default::default(),
1346 result: Default::default(),
1347 })));
1348 drop(valid_block_tx);
1349 let prewarm = runtime.spawn_blocking_named("prewarm", move || {
1350 task.run::<WithTxEnv<TxEnvFor<EthEvmConfig>, Recovered<TransactionSigned>>>(
1351 PrewarmMode::Skipped,
1352 actions_tx,
1353 );
1354 std::thread::current().id()
1355 });
1356 let prewarm_thread = *prewarm.get();
1357
1358 execution_cache.update_with_guard(|cached| assert!(cached.is_none()));
1359 assert_eq!(payload.prewarm_handle.saved_cache.as_ref().unwrap().usage_count(), 2);
1360 assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Empty)));
1361 drop(payload);
1362 assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Empty)));
1363
1364 drop(validator_cache);
1365 assert!(dropped.try_recv().expect("last ExecutionCache drop must free the cache"));
1366 let drop_thread = reader.join().unwrap();
1367 assert_eq!(drop_thread, std::thread::current().id());
1368 assert_ne!(drop_thread, prewarm_thread);
1369 }
1370
1371 fn assert_save_cache_drops_removed_caches(slot: CacheSlot, valid: bool, insert_error: bool) {
1372 use reth_revm::db::{AccountStatus, BundleAccount, BundleState};
1373
1374 let runtime = Runtime::test();
1375 let execution_cache = PayloadExecutionCache::default();
1376 let cache_to_save =
1377 SavedCache::new(B256::repeat_byte(1), crate::tree::ExecutionCache::new(1_000));
1378 let distinct_previous = matches!(slot, CacheSlot::Distinct).then(|| {
1379 SavedCache::new(B256::repeat_byte(2), crate::tree::ExecutionCache::new(1_000))
1381 });
1382 execution_cache.update_with_guard(|cached| {
1383 *cached = match slot {
1384 CacheSlot::Empty => None,
1385 CacheSlot::Shared => Some(cache_to_save.clone()),
1386 CacheSlot::Distinct => distinct_previous.clone(),
1387 };
1388 });
1389 let expect_saved_cache = valid && !insert_error;
1390 let mut drops = Vec::new();
1391 if let Some(previous) = &distinct_previous {
1392 drops.push(observe_cache_drop(previous, &execution_cache, expect_saved_cache));
1393 }
1394 if !expect_saved_cache {
1395 drops.push(observe_cache_drop(&cache_to_save, &execution_cache, expect_saved_cache));
1396 }
1397 drop(distinct_previous);
1399
1400 let (release_tx, release_rx) = mpsc::channel::<()>();
1402 runtime.spawn_blocking_named("drop", move || {
1403 let _ = release_rx.recv();
1404 });
1405
1406 let mut state = BundleState::default();
1407 if insert_error {
1408 state.state.insert(
1410 Address::with_last_byte(1),
1411 BundleAccount::new(None, None, Default::default(), AccountStatus::Changed),
1412 );
1413 }
1414 save_test_cache(&runtime, &execution_cache, cache_to_save, state, valid, Gauge::noop());
1415
1416 for (result, reader) in drops {
1417 assert!(
1418 result.try_recv().expect("removed cache must be destroyed before save returns"),
1419 "cache mutex must be unlocked during destruction"
1420 );
1421 assert_eq!(reader.join().unwrap(), std::thread::current().id());
1422 }
1423 execution_cache.update_with_guard(|slot| {
1424 if expect_saved_cache {
1425 let saved = slot.as_ref().expect("valid cache saved");
1426 assert_eq!(saved.executed_block_hash(), B256::repeat_byte(2));
1427 } else {
1428 assert!(slot.is_none(), "polluted cache must be removed");
1429 }
1430 });
1431 assert_eq!(
1432 execution_cache.get_cache_for(B256::repeat_byte(2)).is_some(),
1433 expect_saved_cache
1434 );
1435 drop(release_tx);
1436 }
1437
1438 #[test]
1439 fn save_cache_drops_replaced_allocation_after_unlock() {
1440 assert_save_cache_drops_removed_caches(CacheSlot::Distinct, true, false);
1441 }
1442
1443 #[test]
1444 fn save_cache_drops_invalid_allocations_after_unlock() {
1445 assert_save_cache_drops_removed_caches(CacheSlot::Distinct, false, false);
1446 }
1447
1448 #[test]
1449 fn save_cache_drops_allocations_after_unlock_on_insert_error() {
1450 assert_save_cache_drops_removed_caches(CacheSlot::Distinct, true, true);
1451 }
1452
1453 #[test]
1454 fn save_cache_drops_shared_allocation_after_unlock_on_invalid_block() {
1455 assert_save_cache_drops_removed_caches(CacheSlot::Shared, false, false);
1456 }
1457
1458 #[test]
1459 fn save_cache_drops_shared_allocation_after_unlock_on_insert_error() {
1460 assert_save_cache_drops_removed_caches(CacheSlot::Shared, true, true);
1461 }
1462
1463 #[test]
1464 fn save_cache_handles_empty_slot() {
1465 for (valid, insert_error) in [(true, false), (false, false), (true, true)] {
1466 assert_save_cache_drops_removed_caches(CacheSlot::Empty, valid, insert_error);
1467 }
1468 }
1469}