1use super::precompile_cache::PrecompileCacheMap;
4use crate::tree::{
5 payload_processor::prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
6 CachedStateCacheMetrics, CachedStateMetrics, CachedStateMetricsSource, ExecutionCache,
7 ExecutionEnv, PayloadExecutionCache, SavedCache, StateProviderBuilder, TreeConfig,
8};
9use alloy_eips::eip1898::BlockWithParent;
10use alloy_primitives::B256;
11use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
12use prewarm::PrewarmMetrics;
13use rayon::prelude::*;
14use reth_evm::{
15 block::ExecutableTxParts,
16 execute::{ExecutableTxFor, WithTxEnv},
17 ConfigureEvm, ConvertTx, ExecutableTxIterator, ExecutableTxTuple, SpecFor, TxEnvFor,
18};
19use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
20use reth_provider::{BlockExecutionOutput, BlockReader, StateProviderFactory, StateReader};
21use reth_revm::db::BundleState;
22use reth_tasks::Runtime;
23pub use reth_trie_parallel::{
24 error::StateRootTaskError,
25 state_root_task::{
26 evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
27 StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
28 StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
29 },
30};
31use std::{
32 ops::Not,
33 sync::{
34 atomic::{AtomicBool, AtomicUsize},
35 mpsc, Arc, OnceLock,
36 },
37};
38use tracing::{debug, instrument, trace, warn, Span};
39
40pub mod bal;
41pub(crate) mod bal_prewarm_pool;
42pub mod prewarm;
43pub mod receipt_root_task;
44
45pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
48
49type IteratorTx<Evm, I> = RecoveredTx<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
51
52type IteratorPayloadHandle<Evm, I> = PayloadHandle<
53 IteratorTx<Evm, I>,
54 <I as ExecutableTxTuple>::Error,
55 <<Evm as ConfigureEvm>::Primitives as NodePrimitives>::Receipt,
56>;
57
58type IteratorPrewarmTxReceiver<Evm, I> =
59 PrewarmTxReceiver<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
60
61type IteratorExecuteTxReceiver<Evm, I> = ExecuteTxReceiver<
62 TxEnvFor<Evm>,
63 <I as ExecutableTxIterator<Evm>>::Recovered,
64 <I as ExecutableTxTuple>::Error,
65>;
66
67type RecoveredTx<TxEnv, Recovered> = WithTxEnv<TxEnv, Recovered>;
68type IndexedTxResult<Tx, Err> = (usize, Result<Tx, Err>);
69type IndexedTxReceiver<Tx, Err> = CrossbeamReceiver<IndexedTxResult<Tx, Err>>;
70type IndexedTxSender<Tx, Err> = CrossbeamSender<IndexedTxResult<Tx, Err>>;
71type PrewarmTxReceiver<TxEnv, Recovered> = mpsc::Receiver<(usize, RecoveredTx<TxEnv, Recovered>)>;
72type ExecuteTxReceiver<TxEnv, Recovered, Err> =
73 IndexedTxReceiver<RecoveredTx<TxEnv, Recovered>, Err>;
74type ExecuteTxSender<TxEnv, Recovered, Err> = IndexedTxSender<RecoveredTx<TxEnv, Recovered>, Err>;
75
76#[derive(Debug)]
78pub struct PayloadProcessor<Evm>
79where
80 Evm: ConfigureEvm,
81{
82 executor: Runtime,
84 execution_cache: PayloadExecutionCache,
86 cache_metrics: Option<CachedStateMetrics>,
88 cache_state_metrics: Option<CachedStateCacheMetrics>,
90 cross_block_cache_size: usize,
92 disable_transaction_prewarming: bool,
94 disable_state_cache: bool,
96 evm_config: Evm,
98 precompile_cache_disabled: bool,
100 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
102 disable_bal_parallel_state_root: bool,
105 disable_bal_batch_io: bool,
107 bal_prewarm_pool: OnceLock<Arc<bal_prewarm_pool::BalPrewarmPool>>,
110}
111
112impl<Evm> PayloadProcessor<Evm>
113where
114 Evm: ConfigureEvm,
115{
116 pub fn new(
118 executor: Runtime,
119 evm_config: Evm,
120 config: &TreeConfig,
121 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
122 ) -> Self {
123 Self {
124 executor,
125 execution_cache: Default::default(),
126 cross_block_cache_size: config.cross_block_cache_size(),
127 disable_transaction_prewarming: config.disable_prewarming(),
128 evm_config,
129 disable_state_cache: config.disable_state_cache(),
130 precompile_cache_disabled: config.precompile_cache_disabled(),
131 precompile_cache_map,
132 cache_metrics: (!config.disable_cache_metrics())
133 .then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
134 cache_state_metrics: (!config.disable_cache_metrics())
135 .then(CachedStateCacheMetrics::default),
136 disable_bal_parallel_state_root: config.disable_bal_parallel_state_root(),
137 disable_bal_batch_io: config.disable_bal_batch_io(),
138 bal_prewarm_pool: OnceLock::new(),
139 }
140 }
141
142 fn bal_prewarm_pool(&self) -> Arc<bal_prewarm_pool::BalPrewarmPool> {
145 self.bal_prewarm_pool
146 .get_or_init(|| {
147 bal_prewarm_pool::BalPrewarmPool::new(bal_prewarm_pool::DEFAULT_BAL_PREWARM_THREADS)
148 })
149 .clone()
150 }
151
152 pub(crate) fn execution_cache(&self) -> PayloadExecutionCache {
154 self.execution_cache.clone()
155 }
156}
157
158impl<Evm> PayloadProcessor<Evm>
159where
160 Evm: ConfigureEvm + 'static,
161{
162 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
165 pub fn spawn_with_state_root_streams<P, I: ExecutableTxIterator<Evm>>(
166 &self,
167 env: ExecutionEnv<Evm>,
168 transactions: I,
169 provider_builder: StateProviderBuilder<Evm::Primitives, P>,
170 hint_stream: Option<StateRootHintStream>,
171 hashed_update_stream: Option<StateRootUpdateStream>,
172 parallel_bal_execution: bool,
173 ) -> IteratorPayloadHandle<Evm, I>
174 where
175 P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
176 {
177 let (prewarm_rx, execution_rx) =
178 self.spawn_tx_iterator(transactions, env.transaction_count, parallel_bal_execution);
179 let prewarm_handle = self.spawn_caching_with(
180 env,
181 prewarm_rx,
182 provider_builder,
183 hint_stream,
184 hashed_update_stream,
185 parallel_bal_execution,
186 );
187 PayloadHandle { prewarm_handle, transactions: execution_rx, _span: Span::current() }
188 }
189
190 const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
197
198 const PARALLEL_PREFETCH_COUNT: usize = 4;
206
207 const FIRST_PARALLEL_TX_WINDOW_SIZE: usize = 64;
209
210 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
219 fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
220 &self,
221 transactions: I,
222 transaction_count: usize,
223 parallel_bal_execution: bool,
224 ) -> (IteratorPrewarmTxReceiver<Evm, I>, IteratorExecuteTxReceiver<Evm, I>) {
225 let (prewarm_tx, prewarm_rx) = mpsc::sync_channel(transaction_count);
226 let (execute_tx, execute_rx) = crossbeam_channel::bounded(transaction_count);
227
228 if transaction_count == 0 {
229 } else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
231 debug!(
234 target: "engine::tree::payload_processor",
235 transaction_count,
236 "using sequential sig recovery for small block"
237 );
238 self.executor.spawn_blocking_named("tx-iterator", move || {
239 let (transactions, convert) = transactions.into_parts();
240 convert_serial(transactions.into_iter(), &convert, &prewarm_tx, &execute_tx);
241 });
242 } else {
243 let executor = self.executor.clone();
246 self.executor.spawn_blocking_named("tx-iterator", move || {
247 let (transactions, convert) = transactions.into_parts();
248 if parallel_bal_execution {
249 executor.cpu_pool().install(|| {
252 transactions
253 .into_par_iter()
254 .enumerate()
255 .map(|(i, tx)| {
256 let tx = convert.convert(tx);
257 (i, tx)
258 })
259 .for_each(|(idx, tx)| {
260 let tx = tx.map(|tx| {
261 let tx = WithTxEnv::new(tx);
262 let _ = prewarm_tx.send((idx, tx.clone()));
263 tx
264 });
265 let _ = execute_tx.send((idx, tx));
266 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
267 });
268 });
269 } else {
270 let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
274 let mut iter = transactions.into_iter();
275
276 convert_serial(iter.by_ref().take(prefetch), &convert, &prewarm_tx, &execute_tx);
279
280 let mut iter = iter.enumerate();
281
282 let mut batch_size = Self::FIRST_PARALLEL_TX_WINDOW_SIZE;
283
284 executor.cpu_pool().install(move || {
287 loop {
288 let chunk = iter
289 .by_ref()
290 .take(batch_size)
291 .collect::<Vec<_>>();
292 if chunk.is_empty() {
293 break;
294 }
295
296 batch_size = batch_size.saturating_mul(2);
297
298 let chunk = chunk
299 .into_par_iter()
300 .map(|(i, tx)| {
301 let idx = i + prefetch;
302 let tx = convert.convert(tx).map(WithTxEnv::new);
303 (idx, tx)
304 })
305 .collect::<Vec<_>>();
306
307 for (idx, tx) in chunk {
308 if let Ok(tx) = &tx {
309 let _ = prewarm_tx.send((idx, tx.clone()));
310 }
311 let _ = execute_tx.send((idx, tx));
312 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
313 }
314 }
315 });
316 }
317 });
318 }
319
320 (prewarm_rx, execute_rx)
321 }
322
323 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
329 fn spawn_caching_with<P>(
330 &self,
331 env: ExecutionEnv<Evm>,
332 transactions: mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + 'static)>,
333 provider_builder: StateProviderBuilder<Evm::Primitives, P>,
334 hint_stream: Option<StateRootHintStream>,
335 hashed_update_stream: Option<StateRootUpdateStream>,
336 parallel_bal_execution: bool,
337 ) -> CacheTaskHandle<<Evm::Primitives as NodePrimitives>::Receipt>
338 where
339 P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
340 {
341 let mode = if parallel_bal_execution {
344 PrewarmMode::BlockAccessList {
345 bal: env.decoded_bal.clone().expect("BAL dispatch implies decoded BAL"),
346 updates: hashed_update_stream,
347 }
348 } else if self.disable_transaction_prewarming ||
349 env.transaction_count < SMALL_BLOCK_TX_THRESHOLD
350 {
351 PrewarmMode::Skipped
352 } else {
353 PrewarmMode::Transactions { pending: transactions, hints: hint_stream }
354 };
355 let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
356
357 let executed_tx_index = Arc::new(AtomicUsize::new(0));
358 let prewarm_ctx = PrewarmContext {
360 env,
361 evm_config: self.evm_config.clone(),
362 saved_cache: saved_cache.clone(),
363 provider: provider_builder,
364 bal_prewarm_pool: parallel_bal_execution.then(|| self.bal_prewarm_pool()),
365 metrics: PrewarmMetrics::default(),
366 cache_metrics: self.cache_metrics.clone(),
367 cache_state_metrics: self.cache_state_metrics.clone(),
368 terminate_execution: Arc::new(AtomicBool::new(false)),
369 executed_tx_index: Arc::clone(&executed_tx_index),
370 precompile_cache_disabled: self.precompile_cache_disabled,
371 precompile_cache_map: self.precompile_cache_map.clone(),
372 disable_bal_parallel_state_root: self.disable_bal_parallel_state_root,
373 disable_bal_batch_io: self.disable_bal_batch_io,
374 };
375
376 let (prewarm_task, to_prewarm_task) =
377 PrewarmCacheTask::new(self.executor.clone(), self.execution_cache.clone(), prewarm_ctx);
378 {
379 let to_prewarm_task = to_prewarm_task.clone();
380 self.executor.spawn_blocking_named("prewarm", move || {
381 prewarm_task.run(mode, to_prewarm_task);
382 });
383 }
384
385 CacheTaskHandle {
386 saved_cache,
387 to_prewarm_task: Some(to_prewarm_task),
388 executed_tx_index,
389 cache_metrics: self.cache_metrics.clone(),
390 }
391 }
392
393 #[instrument(level = "debug", target = "engine::caching", skip(self))]
398 pub fn cache_for(&self, parent_hash: B256) -> SavedCache {
399 if let Some(cache) = self.execution_cache.get_cache_for(parent_hash) {
400 debug!("reusing execution cache");
401 cache
402 } else {
403 debug!("creating new execution cache on cache miss");
404 let start = Instant::now();
405 let cache = ExecutionCache::new(self.cross_block_cache_size);
406 if let Some(metrics) = &self.cache_metrics {
407 metrics.record_cache_creation(start.elapsed());
408 }
409 SavedCache::new(parent_hash, cache)
410 }
411 }
412
413 pub fn on_inserted_executed_block(
421 &self,
422 block_with_parent: BlockWithParent,
423 bundle_state: &BundleState,
424 ) {
425 let cache_state_metrics = self.cache_state_metrics.clone();
426 self.execution_cache.update_with_guard(|cached| {
427 if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
428 debug!(
429 target: "engine::caching",
430 parent_hash = %block_with_parent.parent,
431 "Cannot find cache for parent hash, skip updating cache with new state for inserted executed block",
432 );
433 return
434 }
435
436 if let Some(cache) = cached.as_ref().filter(|cache| !cache.is_available()) {
437 debug!(
438 target: "engine::caching",
439 parent_hash = %block_with_parent.parent,
440 usage_count = cache.usage_count(),
441 "Execution cache is in use, skip updating cache with new state for inserted executed block",
442 );
443 return
444 }
445
446 let caches = match cached.take() {
448 Some(existing) => existing.cache().clone(),
449 None => ExecutionCache::new(self.cross_block_cache_size),
450 };
451
452 let new_cache = SavedCache::new(block_with_parent.block.hash, caches);
454 if new_cache.cache().insert_state(bundle_state).is_err() {
455 *cached = None;
456 debug!(target: "engine::caching", "cleared execution cache on update error");
457 return
458 }
459 new_cache.update_metrics(cache_state_metrics.as_ref());
460
461 *cached = Some(new_cache);
463 debug!(target: "engine::caching", ?block_with_parent, "Updated execution cache for inserted block");
464 });
465 }
466}
467
468fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
470 iter: impl Iterator<Item = RawTx>,
471 convert: &C,
472 prewarm_tx: &mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>,
473 execute_tx: &ExecuteTxSender<TxEnv, Recovered, Err>,
474) where
475 Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
476 TxEnv: Clone,
477 C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
478{
479 for (idx, raw_tx) in iter.enumerate() {
480 let tx = convert.convert(raw_tx);
481 let tx = tx.map(|tx| WithTxEnv::new(tx));
482 if let Ok(tx) = &tx {
483 let _ = prewarm_tx.send((idx, tx.clone()));
484 }
485 let _ = execute_tx.send((idx, tx));
486 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
487 }
488}
489
490#[derive(Debug)]
495pub struct PayloadHandle<Tx, Err, R> {
496 prewarm_handle: CacheTaskHandle<R>,
497 transactions: IndexedTxReceiver<Tx, Err>,
499 _span: Span,
501}
502
503impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
504 pub fn caches(&self) -> Option<ExecutionCache> {
506 self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
507 }
508
509 pub fn cache_metrics(&self) -> Option<CachedStateMetrics> {
511 self.prewarm_handle.cache_metrics.clone()
512 }
513
514 pub const fn executed_tx_index(&self) -> &Arc<AtomicUsize> {
519 &self.prewarm_handle.executed_tx_index
520 }
521
522 pub fn stop_prewarming_execution(&self) {
526 self.prewarm_handle.stop_prewarming_execution()
527 }
528
529 pub fn terminate_caching(
537 &mut self,
538 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
539 ) -> Option<mpsc::Sender<()>> {
540 self.prewarm_handle.terminate_caching(execution_outcome)
541 }
542
543 pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
545 self.transactions.iter().map(|(_, tx)| tx)
546 }
547
548 pub fn clone_transaction_receiver(&self) -> IndexedTxReceiver<Tx, Err> {
550 self.transactions.clone()
551 }
552}
553
554#[derive(Debug)]
559pub struct CacheTaskHandle<R> {
560 saved_cache: Option<SavedCache>,
562 to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
564 executed_tx_index: Arc<AtomicUsize>,
567 cache_metrics: Option<CachedStateMetrics>,
569}
570
571impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
572 pub fn stop_prewarming_execution(&self) {
576 self.to_prewarm_task
577 .as_ref()
578 .map(|tx| tx.send(PrewarmTaskEvent::TerminateTransactionExecution).ok());
579 }
580
581 #[must_use = "sender must be used and notified on block validation success"]
586 pub fn terminate_caching(
587 &mut self,
588 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
589 ) -> Option<mpsc::Sender<()>> {
590 if let Some(tx) = self.to_prewarm_task.take() {
591 let (valid_block_tx, valid_block_rx) = mpsc::channel();
592 let event = PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx };
593 let _ = tx.send(event);
594
595 Some(valid_block_tx)
596 } else {
597 None
598 }
599 }
600}
601
602impl<R> Drop for CacheTaskHandle<R> {
603 fn drop(&mut self) {
604 if let Some(tx) = self.to_prewarm_task.take() {
606 let _ = tx.send(PrewarmTaskEvent::Terminate {
607 execution_outcome: None,
608 valid_block_rx: mpsc::channel().1,
609 });
610 }
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use crate::tree::{
617 payload_processor::PayloadProcessor, precompile_cache::PrecompileCacheMap, ExecutionCache,
618 PayloadExecutionCache, SavedCache, TreeConfig,
619 };
620 use alloy_consensus::constants::KECCAK_EMPTY;
621 use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
622 use alloy_primitives::{Address, B256, U256};
623 use reth_chainspec::ChainSpec;
624 use reth_evm_ethereum::EthEvmConfig;
625 use reth_execution_cache::CachedStatus;
626 use reth_revm::db::BundleState;
627 use revm::state::AccountInfo;
628 use std::sync::Arc;
629
630 fn make_saved_cache(hash: B256) -> SavedCache {
631 let execution_cache = ExecutionCache::new(1_000);
632 SavedCache::new(hash, execution_cache)
633 }
634
635 #[test]
636 fn execution_cache_allows_single_checkout() {
637 let execution_cache = PayloadExecutionCache::default();
638 let hash = B256::from([1u8; 32]);
639
640 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
641
642 let first = execution_cache.get_cache_for(hash);
643 assert!(first.is_some(), "expected initial checkout to succeed");
644
645 let second = execution_cache.get_cache_for(hash);
646 assert!(second.is_none(), "second checkout should be blocked while guard is active");
647
648 drop(first);
649
650 let third = execution_cache.get_cache_for(hash);
651 assert!(third.is_some(), "third checkout should succeed after guard is dropped");
652 }
653
654 #[test]
655 fn execution_cache_checkout_releases_on_drop() {
656 let execution_cache = PayloadExecutionCache::default();
657 let hash = B256::from([2u8; 32]);
658
659 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
660
661 {
662 let guard = execution_cache.get_cache_for(hash);
663 assert!(guard.is_some(), "expected checkout to succeed");
664 }
666
667 let retry = execution_cache.get_cache_for(hash);
668 assert!(retry.is_some(), "checkout should succeed after guard drop");
669 }
670
671 #[test]
672 fn execution_cache_mismatch_parent_clears_and_returns() {
673 let execution_cache = PayloadExecutionCache::default();
674 let hash = B256::from([3u8; 32]);
675
676 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
677
678 let different_hash = B256::from([4u8; 32]);
681 let cache = execution_cache.get_cache_for(different_hash);
682 assert!(cache.is_some(), "cache should be returned for reuse after clearing");
683
684 drop(cache);
685
686 let original = execution_cache.get_cache_for(hash);
689 assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
690 }
691
692 #[test]
693 fn execution_cache_update_after_release_succeeds() {
694 let execution_cache = PayloadExecutionCache::default();
695 let initial = B256::from([5u8; 32]);
696
697 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(initial)));
698
699 let guard =
700 execution_cache.get_cache_for(initial).expect("expected initial checkout to succeed");
701
702 drop(guard);
703
704 let updated = B256::from([6u8; 32]);
705 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(updated)));
706
707 let new_checkout = execution_cache.get_cache_for(updated);
708 assert!(new_checkout.is_some(), "new checkout should succeed after release and update");
709 }
710
711 #[test]
712 fn on_inserted_executed_block_populates_cache() {
713 let payload_processor = PayloadProcessor::new(
714 reth_tasks::Runtime::test(),
715 EthEvmConfig::new(Arc::new(ChainSpec::default())),
716 &TreeConfig::default(),
717 PrecompileCacheMap::default(),
718 );
719
720 let parent_hash = B256::from([1u8; 32]);
721 let block_hash = B256::from([10u8; 32]);
722 let block_with_parent = BlockWithParent {
723 block: BlockNumHash { hash: block_hash, number: 1 },
724 parent: parent_hash,
725 };
726 let bundle_state = BundleState::default();
727
728 assert!(payload_processor.execution_cache.get_cache_for(block_hash).is_none());
730
731 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
733
734 let cached = payload_processor.execution_cache.get_cache_for(block_hash);
736 assert!(cached.is_some());
737 assert_eq!(cached.unwrap().executed_block_hash(), block_hash);
738 }
739
740 #[test]
741 fn on_inserted_executed_block_skips_on_parent_mismatch() {
742 let payload_processor = PayloadProcessor::new(
743 reth_tasks::Runtime::test(),
744 EthEvmConfig::new(Arc::new(ChainSpec::default())),
745 &TreeConfig::default(),
746 PrecompileCacheMap::default(),
747 );
748
749 let block1_hash = B256::from([1u8; 32]);
751 payload_processor
752 .execution_cache
753 .update_with_guard(|slot| *slot = Some(make_saved_cache(block1_hash)));
754
755 let wrong_parent = B256::from([99u8; 32]);
757 let block3_hash = B256::from([3u8; 32]);
758 let block_with_parent = BlockWithParent {
759 block: BlockNumHash { hash: block3_hash, number: 3 },
760 parent: wrong_parent,
761 };
762 let bundle_state = BundleState::default();
763
764 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
765
766 let cached = payload_processor.execution_cache.get_cache_for(block1_hash);
768 assert!(cached.is_some(), "Original cache should be preserved");
769
770 let cached3 = payload_processor.execution_cache.get_cache_for(block3_hash);
772 assert!(cached3.is_none(), "New block cache should not be created on mismatch");
773 }
774
775 #[test]
776 fn on_inserted_executed_block_does_not_mutate_checked_out_parent_cache() {
777 let payload_processor = PayloadProcessor::new(
778 reth_tasks::Runtime::test(),
779 EthEvmConfig::new(Arc::new(ChainSpec::default())),
780 &TreeConfig::default(),
781 PrecompileCacheMap::default(),
782 );
783
784 let parent_hash = B256::from([1u8; 32]);
785 payload_processor
786 .execution_cache
787 .update_with_guard(|slot| *slot = Some(make_saved_cache(parent_hash)));
788
789 let checked_out = payload_processor
793 .execution_cache
794 .get_cache_for(parent_hash)
795 .expect("expected parent cache checkout to succeed");
796
797 let polluted_address = Address::random();
798 let bundle_state = BundleState::builder(2..=2)
799 .state_present_account_info(
800 polluted_address,
801 AccountInfo {
802 balance: U256::from(1337),
803 nonce: 7,
804 code_hash: KECCAK_EMPTY,
805 code: None,
806 account_id: None,
807 },
808 )
809 .build();
810
811 let block_with_parent = BlockWithParent {
814 block: BlockNumHash { hash: B256::from([2u8; 32]), number: 2 },
815 parent: parent_hash,
816 };
817
818 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
819
820 let account = checked_out
823 .cache()
824 .get_or_try_insert_account_with(polluted_address, || Ok::<_, ()>(None))
825 .expect("cache read should succeed");
826
827 assert_eq!(
828 account,
829 CachedStatus::NotCached(None),
830 "checked-out parent cache should not observe state from inserted local block"
831 );
832 }
833
834 #[test]
845 fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
846 let execution_cache = PayloadExecutionCache::default();
847
848 let block4_hash = B256::from([4u8; 32]);
850 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
851
852 let fork_parent = B256::from([2u8; 32]);
855 let prewarm_cache = execution_cache.get_cache_for(fork_parent);
856 assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
857 let prewarm_cache = prewarm_cache.unwrap();
858 assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
859
860 let fork_addr = Address::from([0xBB; 20]);
863 let fork_key = B256::from([0xCC; 32]);
864 prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
865
866 let during_prewarm = execution_cache.get_cache_for(block4_hash);
868 assert!(
869 during_prewarm.is_none(),
870 "cache must be unavailable while prewarm holds a reference"
871 );
872
873 drop(prewarm_cache);
875
876 let block5_cache = execution_cache.get_cache_for(block4_hash);
880 assert!(
881 block5_cache.is_some(),
882 "canonical chain must get cache after fork prewarm is dropped"
883 );
884 assert_eq!(
885 block5_cache.as_ref().unwrap().executed_block_hash(),
886 block4_hash,
887 "cache must carry the canonical parent hash, not the fork parent"
888 );
889 }
890}