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::{
21 BlockExecutionOutput, BlockNumReader, DatabaseProviderFactory, PruneCheckpointReader,
22 StageCheckpointReader, StorageSettingsCache, TryIntoHistoricalStateProvider,
23};
24use reth_revm::db::BundleState;
25use reth_tasks::Runtime;
26pub use reth_trie_parallel::{
27 error::StateRootTaskError,
28 state_root_task::{
29 evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
30 StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
31 StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
32 },
33};
34use std::{
35 ops::Not,
36 sync::{
37 atomic::{AtomicBool, AtomicUsize},
38 mpsc, Arc, OnceLock,
39 },
40};
41use tracing::{debug, instrument, trace, warn, Span};
42
43pub mod bal;
44pub(crate) mod bal_prewarm_pool;
45pub mod prewarm;
46pub mod receipt_root_task;
47
48pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
51
52type IteratorTx<Evm, I> = RecoveredTx<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
54
55type IteratorPayloadHandle<Evm, I> = PayloadHandle<
56 IteratorTx<Evm, I>,
57 <I as ExecutableTxTuple>::Error,
58 <<Evm as ConfigureEvm>::Primitives as NodePrimitives>::Receipt,
59>;
60
61type IteratorPrewarmTxReceiver<Evm, I> =
62 PrewarmTxReceiver<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
63
64type IteratorExecuteTxReceiver<Evm, I> = ExecuteTxReceiver<
65 TxEnvFor<Evm>,
66 <I as ExecutableTxIterator<Evm>>::Recovered,
67 <I as ExecutableTxTuple>::Error,
68>;
69
70type RecoveredTx<TxEnv, Recovered> = WithTxEnv<TxEnv, Recovered>;
71type IndexedTxResult<Tx, Err> = (usize, Result<Tx, Err>);
72type IndexedTxReceiver<Tx, Err> = CrossbeamReceiver<IndexedTxResult<Tx, Err>>;
73type IndexedTxSender<Tx, Err> = CrossbeamSender<IndexedTxResult<Tx, Err>>;
74type PrewarmTxReceiver<TxEnv, Recovered> = mpsc::Receiver<(usize, RecoveredTx<TxEnv, Recovered>)>;
75type ExecuteTxReceiver<TxEnv, Recovered, Err> =
76 IndexedTxReceiver<RecoveredTx<TxEnv, Recovered>, Err>;
77type ExecuteTxSender<TxEnv, Recovered, Err> = IndexedTxSender<RecoveredTx<TxEnv, Recovered>, Err>;
78
79#[derive(Debug)]
81pub struct PayloadProcessor<Evm>
82where
83 Evm: ConfigureEvm,
84{
85 executor: Runtime,
87 execution_cache: PayloadExecutionCache,
89 cache_metrics: Option<CachedStateMetrics>,
91 cache_state_metrics: Option<CachedStateCacheMetrics>,
93 cross_block_cache_size: usize,
95 disable_transaction_prewarming: bool,
97 disable_state_cache: bool,
99 evm_config: Evm,
101 precompile_cache_disabled: bool,
103 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
105 disable_bal_parallel_state_root: bool,
108 disable_bal_batch_io: bool,
110 bal_prewarm_pool: OnceLock<Arc<bal_prewarm_pool::BalPrewarmPool>>,
113}
114
115impl<Evm> PayloadProcessor<Evm>
116where
117 Evm: ConfigureEvm,
118{
119 pub fn new(
121 executor: Runtime,
122 evm_config: Evm,
123 config: &TreeConfig,
124 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
125 ) -> Self {
126 Self {
127 executor,
128 execution_cache: Default::default(),
129 cross_block_cache_size: config.cross_block_cache_size(),
130 disable_transaction_prewarming: config.disable_prewarming(),
131 evm_config,
132 disable_state_cache: config.disable_state_cache(),
133 precompile_cache_disabled: config.precompile_cache_disabled(),
134 precompile_cache_map,
135 cache_metrics: (!config.disable_cache_metrics())
136 .then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
137 cache_state_metrics: (!config.disable_cache_metrics())
138 .then(CachedStateCacheMetrics::default),
139 disable_bal_parallel_state_root: config.disable_bal_parallel_state_root(),
140 disable_bal_batch_io: config.disable_bal_batch_io(),
141 bal_prewarm_pool: OnceLock::new(),
142 }
143 }
144
145 fn bal_prewarm_pool(&self) -> Arc<bal_prewarm_pool::BalPrewarmPool> {
148 self.bal_prewarm_pool
149 .get_or_init(|| {
150 bal_prewarm_pool::BalPrewarmPool::new(bal_prewarm_pool::DEFAULT_BAL_PREWARM_THREADS)
151 })
152 .clone()
153 }
154
155 pub(crate) fn execution_cache(&self) -> PayloadExecutionCache {
157 self.execution_cache.clone()
158 }
159}
160
161impl<Evm> PayloadProcessor<Evm>
162where
163 Evm: ConfigureEvm + 'static,
164{
165 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
168 pub fn spawn_with_state_root_streams<P, I: ExecutableTxIterator<Evm>>(
169 &self,
170 env: ExecutionEnv<Evm>,
171 transactions: I,
172 provider_builder: StateProviderBuilder<Evm::Primitives, P>,
173 hint_stream: Option<StateRootHintStream>,
174 hashed_update_stream: Option<StateRootUpdateStream>,
175 parallel_bal_execution: bool,
176 ) -> IteratorPayloadHandle<Evm, I>
177 where
178 P: DatabaseProviderFactory + Clone + 'static,
179 P::Provider: BlockNumReader
180 + PruneCheckpointReader
181 + StageCheckpointReader
182 + StorageSettingsCache
183 + TryIntoHistoricalStateProvider
184 + 'static,
185 {
186 let (prewarm_rx, execution_rx) =
187 self.spawn_tx_iterator(transactions, env.transaction_count, parallel_bal_execution);
188 let prewarm_handle = self.spawn_caching_with(
189 env,
190 prewarm_rx,
191 provider_builder,
192 hint_stream,
193 hashed_update_stream,
194 parallel_bal_execution,
195 );
196 PayloadHandle { prewarm_handle, transactions: execution_rx, _span: Span::current() }
197 }
198
199 const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
206
207 const PARALLEL_PREFETCH_COUNT: usize = 4;
215
216 const FIRST_PARALLEL_TX_WINDOW_SIZE: usize = 64;
218
219 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
228 fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
229 &self,
230 transactions: I,
231 transaction_count: usize,
232 parallel_bal_execution: bool,
233 ) -> (IteratorPrewarmTxReceiver<Evm, I>, IteratorExecuteTxReceiver<Evm, I>) {
234 let (prewarm_tx, prewarm_rx) = mpsc::sync_channel(transaction_count);
235 let (execute_tx, execute_rx) = crossbeam_channel::bounded(transaction_count);
236
237 if transaction_count == 0 {
238 } else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
240 debug!(
243 target: "engine::tree::payload_processor",
244 transaction_count,
245 "using sequential sig recovery for small block"
246 );
247 self.executor.spawn_blocking_named("tx-iterator", move || {
248 let (transactions, convert) = transactions.into_parts();
249 convert_serial(transactions.into_iter(), &convert, &prewarm_tx, &execute_tx);
250 });
251 } else {
252 let executor = self.executor.clone();
255 self.executor.spawn_blocking_named("tx-iterator", move || {
256 let (transactions, convert) = transactions.into_parts();
257 if parallel_bal_execution {
258 executor.cpu_pool().install(|| {
261 transactions
262 .into_par_iter()
263 .enumerate()
264 .map(|(i, tx)| {
265 let tx = convert.convert(tx);
266 (i, tx)
267 })
268 .for_each(|(idx, tx)| {
269 let tx = tx.map(|tx| {
270 let tx = WithTxEnv::new(tx);
271 let _ = prewarm_tx.send((idx, tx.clone()));
272 tx
273 });
274 let _ = execute_tx.send((idx, tx));
275 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
276 });
277 });
278 } else {
279 let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
283 let mut iter = transactions.into_iter();
284
285 convert_serial(iter.by_ref().take(prefetch), &convert, &prewarm_tx, &execute_tx);
288
289 let mut iter = iter.enumerate();
290
291 let mut batch_size = Self::FIRST_PARALLEL_TX_WINDOW_SIZE;
292
293 executor.cpu_pool().install(move || {
296 loop {
297 let chunk = iter
298 .by_ref()
299 .take(batch_size)
300 .collect::<Vec<_>>();
301 if chunk.is_empty() {
302 break;
303 }
304
305 batch_size = batch_size.saturating_mul(2);
306
307 let chunk = chunk
308 .into_par_iter()
309 .map(|(i, tx)| {
310 let idx = i + prefetch;
311 let tx = convert.convert(tx).map(WithTxEnv::new);
312 (idx, tx)
313 })
314 .collect::<Vec<_>>();
315
316 for (idx, tx) in chunk {
317 if let Ok(tx) = &tx {
318 let _ = prewarm_tx.send((idx, tx.clone()));
319 }
320 let _ = execute_tx.send((idx, tx));
321 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
322 }
323 }
324 });
325 }
326 });
327 }
328
329 (prewarm_rx, execute_rx)
330 }
331
332 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
338 fn spawn_caching_with<P>(
339 &self,
340 env: ExecutionEnv<Evm>,
341 transactions: mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + 'static)>,
342 provider_builder: StateProviderBuilder<Evm::Primitives, P>,
343 hint_stream: Option<StateRootHintStream>,
344 hashed_update_stream: Option<StateRootUpdateStream>,
345 parallel_bal_execution: bool,
346 ) -> CacheTaskHandle<<Evm::Primitives as NodePrimitives>::Receipt>
347 where
348 P: DatabaseProviderFactory + Clone + 'static,
349 P::Provider: BlockNumReader
350 + PruneCheckpointReader
351 + StageCheckpointReader
352 + StorageSettingsCache
353 + TryIntoHistoricalStateProvider
354 + 'static,
355 {
356 let mode = if parallel_bal_execution {
359 PrewarmMode::BlockAccessList {
360 bal: env.decoded_bal.clone().expect("BAL dispatch implies decoded BAL"),
361 updates: hashed_update_stream,
362 }
363 } else if self.disable_transaction_prewarming ||
364 env.transaction_count < SMALL_BLOCK_TX_THRESHOLD
365 {
366 PrewarmMode::Skipped
367 } else {
368 PrewarmMode::Transactions { pending: transactions, hints: hint_stream }
369 };
370 let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
371
372 let executed_tx_index = Arc::new(AtomicUsize::new(0));
373 let prewarm_ctx = PrewarmContext {
375 env,
376 evm_config: self.evm_config.clone(),
377 saved_cache: saved_cache.clone(),
378 provider: provider_builder,
379 bal_prewarm_pool: parallel_bal_execution.then(|| self.bal_prewarm_pool()),
380 metrics: PrewarmMetrics::default(),
381 cache_metrics: self.cache_metrics.clone(),
382 cache_state_metrics: self.cache_state_metrics.clone(),
383 terminate_execution: Arc::new(AtomicBool::new(false)),
384 executed_tx_index: Arc::clone(&executed_tx_index),
385 precompile_cache_disabled: self.precompile_cache_disabled,
386 precompile_cache_map: self.precompile_cache_map.clone(),
387 disable_bal_parallel_state_root: self.disable_bal_parallel_state_root,
388 disable_bal_batch_io: self.disable_bal_batch_io,
389 };
390
391 let (prewarm_task, to_prewarm_task) =
392 PrewarmCacheTask::new(self.executor.clone(), self.execution_cache.clone(), prewarm_ctx);
393 {
394 let to_prewarm_task = to_prewarm_task.clone();
395 self.executor.spawn_blocking_named("prewarm", move || {
396 prewarm_task.run(mode, to_prewarm_task);
397 });
398 }
399
400 CacheTaskHandle {
401 saved_cache,
402 to_prewarm_task: Some(to_prewarm_task),
403 executed_tx_index,
404 cache_metrics: self.cache_metrics.clone(),
405 }
406 }
407
408 #[instrument(level = "debug", target = "engine::caching", skip(self))]
413 pub fn cache_for(&self, parent_hash: B256) -> SavedCache {
414 if let Some(cache) = self.execution_cache.get_cache_for(parent_hash) {
415 debug!("reusing execution cache");
416 cache
417 } else {
418 debug!("creating new execution cache on cache miss");
419 let start = Instant::now();
420 let cache = ExecutionCache::new(self.cross_block_cache_size);
421 if let Some(metrics) = &self.cache_metrics {
422 metrics.record_cache_creation(start.elapsed());
423 }
424 SavedCache::new(parent_hash, cache)
425 }
426 }
427
428 pub fn on_inserted_executed_block(
436 &self,
437 block_with_parent: BlockWithParent,
438 bundle_state: &BundleState,
439 ) {
440 let cache_state_metrics = self.cache_state_metrics.clone();
441 self.execution_cache.update_with_guard(|cached| {
442 if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
443 debug!(
444 target: "engine::caching",
445 parent_hash = %block_with_parent.parent,
446 "Cannot find cache for parent hash, skip updating cache with new state for inserted executed block",
447 );
448 return
449 }
450
451 if let Some(cache) = cached.as_ref().filter(|cache| !cache.is_available()) {
452 debug!(
453 target: "engine::caching",
454 parent_hash = %block_with_parent.parent,
455 usage_count = cache.usage_count(),
456 "Execution cache is in use, skip updating cache with new state for inserted executed block",
457 );
458 return
459 }
460
461 let caches = match cached.take() {
463 Some(existing) => existing.cache().clone(),
464 None => ExecutionCache::new(self.cross_block_cache_size),
465 };
466
467 let new_cache = SavedCache::new(block_with_parent.block.hash, caches);
469 if new_cache.cache().insert_state(bundle_state).is_err() {
470 *cached = None;
471 debug!(target: "engine::caching", "cleared execution cache on update error");
472 return
473 }
474 new_cache.update_metrics(cache_state_metrics.as_ref());
475
476 *cached = Some(new_cache);
478 debug!(target: "engine::caching", ?block_with_parent, "Updated execution cache for inserted block");
479 });
480 }
481}
482
483fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
485 iter: impl Iterator<Item = RawTx>,
486 convert: &C,
487 prewarm_tx: &mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>,
488 execute_tx: &ExecuteTxSender<TxEnv, Recovered, Err>,
489) where
490 Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
491 TxEnv: Clone,
492 C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
493{
494 for (idx, raw_tx) in iter.enumerate() {
495 let tx = convert.convert(raw_tx);
496 let tx = tx.map(|tx| WithTxEnv::new(tx));
497 if let Ok(tx) = &tx {
498 let _ = prewarm_tx.send((idx, tx.clone()));
499 }
500 let _ = execute_tx.send((idx, tx));
501 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
502 }
503}
504
505#[derive(Debug)]
510pub struct PayloadHandle<Tx, Err, R> {
511 prewarm_handle: CacheTaskHandle<R>,
512 transactions: IndexedTxReceiver<Tx, Err>,
514 _span: Span,
516}
517
518impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
519 pub fn caches(&self) -> Option<ExecutionCache> {
521 self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
522 }
523
524 pub fn cache_metrics(&self) -> Option<CachedStateMetrics> {
526 self.prewarm_handle.cache_metrics.clone()
527 }
528
529 pub const fn executed_tx_index(&self) -> &Arc<AtomicUsize> {
534 &self.prewarm_handle.executed_tx_index
535 }
536
537 pub fn stop_prewarming_execution(&self) {
541 self.prewarm_handle.stop_prewarming_execution()
542 }
543
544 pub fn terminate_caching(
552 &mut self,
553 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
554 ) -> Option<mpsc::Sender<()>> {
555 self.prewarm_handle.terminate_caching(execution_outcome)
556 }
557
558 pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
560 self.transactions.iter().map(|(_, tx)| tx)
561 }
562
563 pub fn clone_transaction_receiver(&self) -> IndexedTxReceiver<Tx, Err> {
565 self.transactions.clone()
566 }
567}
568
569#[derive(Debug)]
574pub struct CacheTaskHandle<R> {
575 saved_cache: Option<SavedCache>,
577 to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
579 executed_tx_index: Arc<AtomicUsize>,
582 cache_metrics: Option<CachedStateMetrics>,
584}
585
586impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
587 pub fn stop_prewarming_execution(&self) {
591 self.to_prewarm_task
592 .as_ref()
593 .map(|tx| tx.send(PrewarmTaskEvent::TerminateTransactionExecution).ok());
594 }
595
596 #[must_use = "sender must be used and notified on block validation success"]
601 pub fn terminate_caching(
602 &mut self,
603 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
604 ) -> Option<mpsc::Sender<()>> {
605 if let Some(tx) = self.to_prewarm_task.take() {
606 let (valid_block_tx, valid_block_rx) = mpsc::channel();
607 let event = PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx };
608 let _ = tx.send(event);
609
610 Some(valid_block_tx)
611 } else {
612 None
613 }
614 }
615}
616
617impl<R> Drop for CacheTaskHandle<R> {
618 fn drop(&mut self) {
619 if let Some(tx) = self.to_prewarm_task.take() {
621 let _ = tx.send(PrewarmTaskEvent::Terminate {
622 execution_outcome: None,
623 valid_block_rx: mpsc::channel().1,
624 });
625 }
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use crate::tree::{
632 payload_processor::PayloadProcessor, precompile_cache::PrecompileCacheMap, ExecutionCache,
633 PayloadExecutionCache, SavedCache, TreeConfig,
634 };
635 use alloy_consensus::constants::KECCAK_EMPTY;
636 use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
637 use alloy_primitives::{Address, B256, U256};
638 use reth_chainspec::ChainSpec;
639 use reth_evm_ethereum::EthEvmConfig;
640 use reth_execution_cache::CachedStatus;
641 use reth_revm::db::BundleState;
642 use revm::state::AccountInfo;
643 use std::sync::Arc;
644
645 fn make_saved_cache(hash: B256) -> SavedCache {
646 let execution_cache = ExecutionCache::new(1_000);
647 SavedCache::new(hash, execution_cache)
648 }
649
650 #[test]
651 fn execution_cache_allows_single_checkout() {
652 let execution_cache = PayloadExecutionCache::default();
653 let hash = B256::from([1u8; 32]);
654
655 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
656
657 let first = execution_cache.get_cache_for(hash);
658 assert!(first.is_some(), "expected initial checkout to succeed");
659
660 let second = execution_cache.get_cache_for(hash);
661 assert!(second.is_none(), "second checkout should be blocked while guard is active");
662
663 drop(first);
664
665 let third = execution_cache.get_cache_for(hash);
666 assert!(third.is_some(), "third checkout should succeed after guard is dropped");
667 }
668
669 #[test]
670 fn execution_cache_checkout_releases_on_drop() {
671 let execution_cache = PayloadExecutionCache::default();
672 let hash = B256::from([2u8; 32]);
673
674 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
675
676 {
677 let guard = execution_cache.get_cache_for(hash);
678 assert!(guard.is_some(), "expected checkout to succeed");
679 }
681
682 let retry = execution_cache.get_cache_for(hash);
683 assert!(retry.is_some(), "checkout should succeed after guard drop");
684 }
685
686 #[test]
687 fn execution_cache_mismatch_parent_clears_and_returns() {
688 let execution_cache = PayloadExecutionCache::default();
689 let hash = B256::from([3u8; 32]);
690
691 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
692
693 let different_hash = B256::from([4u8; 32]);
696 let cache = execution_cache.get_cache_for(different_hash);
697 assert!(cache.is_some(), "cache should be returned for reuse after clearing");
698
699 drop(cache);
700
701 let original = execution_cache.get_cache_for(hash);
704 assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
705 }
706
707 #[test]
708 fn execution_cache_update_after_release_succeeds() {
709 let execution_cache = PayloadExecutionCache::default();
710 let initial = B256::from([5u8; 32]);
711
712 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(initial)));
713
714 let guard =
715 execution_cache.get_cache_for(initial).expect("expected initial checkout to succeed");
716
717 drop(guard);
718
719 let updated = B256::from([6u8; 32]);
720 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(updated)));
721
722 let new_checkout = execution_cache.get_cache_for(updated);
723 assert!(new_checkout.is_some(), "new checkout should succeed after release and update");
724 }
725
726 #[test]
727 fn on_inserted_executed_block_populates_cache() {
728 let payload_processor = PayloadProcessor::new(
729 reth_tasks::Runtime::test(),
730 EthEvmConfig::new(Arc::new(ChainSpec::default())),
731 &TreeConfig::default(),
732 PrecompileCacheMap::default(),
733 );
734
735 let parent_hash = B256::from([1u8; 32]);
736 let block_hash = B256::from([10u8; 32]);
737 let block_with_parent = BlockWithParent {
738 block: BlockNumHash { hash: block_hash, number: 1 },
739 parent: parent_hash,
740 };
741 let bundle_state = BundleState::default();
742
743 assert!(payload_processor.execution_cache.get_cache_for(block_hash).is_none());
745
746 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
748
749 let cached = payload_processor.execution_cache.get_cache_for(block_hash);
751 assert!(cached.is_some());
752 assert_eq!(cached.unwrap().executed_block_hash(), block_hash);
753 }
754
755 #[test]
756 fn on_inserted_executed_block_skips_on_parent_mismatch() {
757 let payload_processor = PayloadProcessor::new(
758 reth_tasks::Runtime::test(),
759 EthEvmConfig::new(Arc::new(ChainSpec::default())),
760 &TreeConfig::default(),
761 PrecompileCacheMap::default(),
762 );
763
764 let block1_hash = B256::from([1u8; 32]);
766 payload_processor
767 .execution_cache
768 .update_with_guard(|slot| *slot = Some(make_saved_cache(block1_hash)));
769
770 let wrong_parent = B256::from([99u8; 32]);
772 let block3_hash = B256::from([3u8; 32]);
773 let block_with_parent = BlockWithParent {
774 block: BlockNumHash { hash: block3_hash, number: 3 },
775 parent: wrong_parent,
776 };
777 let bundle_state = BundleState::default();
778
779 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
780
781 let cached = payload_processor.execution_cache.get_cache_for(block1_hash);
783 assert!(cached.is_some(), "Original cache should be preserved");
784
785 let cached3 = payload_processor.execution_cache.get_cache_for(block3_hash);
787 assert!(cached3.is_none(), "New block cache should not be created on mismatch");
788 }
789
790 #[test]
791 fn on_inserted_executed_block_does_not_mutate_checked_out_parent_cache() {
792 let payload_processor = PayloadProcessor::new(
793 reth_tasks::Runtime::test(),
794 EthEvmConfig::new(Arc::new(ChainSpec::default())),
795 &TreeConfig::default(),
796 PrecompileCacheMap::default(),
797 );
798
799 let parent_hash = B256::from([1u8; 32]);
800 payload_processor
801 .execution_cache
802 .update_with_guard(|slot| *slot = Some(make_saved_cache(parent_hash)));
803
804 let checked_out = payload_processor
808 .execution_cache
809 .get_cache_for(parent_hash)
810 .expect("expected parent cache checkout to succeed");
811
812 let polluted_address = Address::random();
813 let bundle_state = BundleState::builder(2..=2)
814 .state_present_account_info(
815 polluted_address,
816 AccountInfo {
817 balance: U256::from(1337),
818 nonce: 7,
819 code_hash: KECCAK_EMPTY,
820 code: None,
821 account_id: None,
822 },
823 )
824 .build();
825
826 let block_with_parent = BlockWithParent {
829 block: BlockNumHash { hash: B256::from([2u8; 32]), number: 2 },
830 parent: parent_hash,
831 };
832
833 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
834
835 let account = checked_out
838 .cache()
839 .get_or_try_insert_account_with(polluted_address, || Ok::<_, ()>(None))
840 .expect("cache read should succeed");
841
842 assert_eq!(
843 account,
844 CachedStatus::NotCached(None),
845 "checked-out parent cache should not observe state from inserted local block"
846 );
847 }
848
849 #[test]
860 fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
861 let execution_cache = PayloadExecutionCache::default();
862
863 let block4_hash = B256::from([4u8; 32]);
865 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
866
867 let fork_parent = B256::from([2u8; 32]);
870 let prewarm_cache = execution_cache.get_cache_for(fork_parent);
871 assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
872 let prewarm_cache = prewarm_cache.unwrap();
873 assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
874
875 let fork_addr = Address::from([0xBB; 20]);
878 let fork_key = B256::from([0xCC; 32]);
879 prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
880
881 let during_prewarm = execution_cache.get_cache_for(block4_hash);
883 assert!(
884 during_prewarm.is_none(),
885 "cache must be unavailable while prewarm holds a reference"
886 );
887
888 drop(prewarm_cache);
890
891 let block5_cache = execution_cache.get_cache_for(block4_hash);
895 assert!(
896 block5_cache.is_some(),
897 "canonical chain must get cache after fork prewarm is dropped"
898 );
899 assert_eq!(
900 block5_cache.as_ref().unwrap().executed_block_hash(),
901 block4_hash,
902 "cache must carry the canonical parent hash, not the fork parent"
903 );
904 }
905}