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, 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, ChangeSetReader, DatabaseProviderFactory, HistoryReader,
22 PruneCheckpointReader, StageCheckpointReader, StorageChangeSetReader, StorageSettingsCache,
23};
24use reth_revm::db::BundleState;
25use reth_storage_overlay::OverlayStateProviderFactory;
26use reth_tasks::Runtime;
27pub use reth_trie_parallel::{
28 error::StateRootTaskError,
29 state_root_task::{
30 evm_state_to_hashed_post_state, PayloadStateRootHandle, StateAccessHint,
31 StateRootComputeOutcome, StateRootHandle, StateRootHintStream, StateRootMessage,
32 StateRootSink, StateRootTaskCancelGuard, StateRootUpdateHook, StateRootUpdateStream,
33 },
34};
35use std::{
36 ops::Not,
37 sync::{
38 atomic::{AtomicBool, AtomicUsize},
39 mpsc, Arc, OnceLock,
40 },
41};
42use tracing::{debug, instrument, trace, warn, Span};
43
44pub mod bal;
45pub mod bal_prewarm_pool;
46pub mod prewarm;
47pub mod receipt_root_task;
48
49pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
52
53type IteratorTx<Evm, I> = RecoveredTx<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
55
56type IteratorPayloadHandle<Evm, I> = PayloadHandle<
57 IteratorTx<Evm, I>,
58 <I as ExecutableTxTuple>::Error,
59 <<Evm as ConfigureEvm>::Primitives as NodePrimitives>::Receipt,
60>;
61
62type IteratorPrewarmTxReceiver<Evm, I> =
63 PrewarmTxReceiver<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
64
65type IteratorExecuteTxReceiver<Evm, I> = ExecuteTxReceiver<
66 TxEnvFor<Evm>,
67 <I as ExecutableTxIterator<Evm>>::Recovered,
68 <I as ExecutableTxTuple>::Error,
69>;
70
71type RecoveredTx<TxEnv, Recovered> = WithTxEnv<TxEnv, Recovered>;
72type IndexedTxResult<Tx, Err> = (usize, Result<Tx, Err>);
73type IndexedTxReceiver<Tx, Err> = CrossbeamReceiver<IndexedTxResult<Tx, Err>>;
74type IndexedTxSender<Tx, Err> = CrossbeamSender<IndexedTxResult<Tx, Err>>;
75type PrewarmTxReceiver<TxEnv, Recovered> = mpsc::Receiver<(usize, RecoveredTx<TxEnv, Recovered>)>;
76type ExecuteTxReceiver<TxEnv, Recovered, Err> =
77 IndexedTxReceiver<RecoveredTx<TxEnv, Recovered>, Err>;
78type ExecuteTxSender<TxEnv, Recovered, Err> = IndexedTxSender<RecoveredTx<TxEnv, Recovered>, Err>;
79
80#[derive(Debug)]
82pub struct PayloadProcessor<Evm>
83where
84 Evm: ConfigureEvm,
85{
86 executor: Runtime,
88 execution_cache: PayloadExecutionCache,
90 cache_metrics: Option<CachedStateMetrics>,
92 cache_state_metrics: Option<CachedStateCacheMetrics>,
94 cross_block_cache_size: usize,
96 disable_transaction_prewarming: bool,
98 disable_state_cache: bool,
100 evm_config: Evm,
102 precompile_cache_disabled: bool,
104 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
106 disable_bal_parallel_state_root: bool,
109 disable_bal_batch_io: bool,
111 bal_prewarm_pool: OnceLock<Arc<bal_prewarm_pool::BalPrewarmPool>>,
114}
115
116impl<Evm> PayloadProcessor<Evm>
117where
118 Evm: ConfigureEvm,
119{
120 pub fn new(
122 executor: Runtime,
123 evm_config: Evm,
124 config: &TreeConfig,
125 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
126 ) -> Self {
127 Self {
128 executor,
129 execution_cache: Default::default(),
130 cross_block_cache_size: config.cross_block_cache_size(),
131 disable_transaction_prewarming: config.disable_prewarming(),
132 evm_config,
133 disable_state_cache: config.disable_state_cache(),
134 precompile_cache_disabled: config.precompile_cache_disabled(),
135 precompile_cache_map,
136 cache_metrics: (!config.disable_cache_metrics())
137 .then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
138 cache_state_metrics: (!config.disable_cache_metrics())
139 .then(CachedStateCacheMetrics::default),
140 disable_bal_parallel_state_root: config.disable_bal_parallel_state_root(),
141 disable_bal_batch_io: config.disable_bal_batch_io(),
142 bal_prewarm_pool: OnceLock::new(),
143 }
144 }
145
146 fn bal_prewarm_pool(&self) -> Arc<bal_prewarm_pool::BalPrewarmPool> {
149 self.bal_prewarm_pool
150 .get_or_init(|| {
151 bal_prewarm_pool::BalPrewarmPool::new(bal_prewarm_pool::DEFAULT_BAL_PREWARM_THREADS)
152 })
153 .clone()
154 }
155
156 pub(crate) fn execution_cache(&self) -> PayloadExecutionCache {
158 self.execution_cache.clone()
159 }
160}
161
162impl<Evm> PayloadProcessor<Evm>
163where
164 Evm: ConfigureEvm + 'static,
165{
166 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
169 pub fn spawn_with_state_root_streams<P, I: ExecutableTxIterator<Evm>>(
170 &self,
171 env: ExecutionEnv<Evm>,
172 transactions: I,
173 state_provider_factory: OverlayStateProviderFactory<P, Evm::Primitives>,
174 hint_stream: Option<StateRootHintStream>,
175 hashed_update_stream: Option<StateRootUpdateStream>,
176 parallel_bal_execution: bool,
177 ) -> IteratorPayloadHandle<Evm, I>
178 where
179 P: DatabaseProviderFactory + Clone + 'static,
180 P::Provider: BlockNumReader
181 + PruneCheckpointReader
182 + StageCheckpointReader
183 + ChangeSetReader
184 + StorageChangeSetReader
185 + StorageSettingsCache
186 + HistoryReader
187 + 'static,
188 {
189 let prewarm_transactions =
190 self.prewarms_transactions(env.transaction_count, parallel_bal_execution);
191 let (prewarm_rx, execution_rx) = self.spawn_tx_iterator(
192 transactions,
193 env.transaction_count,
194 parallel_bal_execution,
195 prewarm_transactions,
196 );
197 let prewarm_handle = self.spawn_caching_with(
198 env,
199 prewarm_rx,
200 state_provider_factory,
201 hint_stream,
202 hashed_update_stream,
203 parallel_bal_execution,
204 );
205 PayloadHandle { prewarm_handle, transactions: execution_rx, _span: Span::current() }
206 }
207
208 const fn prewarms_transactions(
215 &self,
216 transaction_count: usize,
217 parallel_bal_execution: bool,
218 ) -> bool {
219 !parallel_bal_execution &&
220 !self.disable_transaction_prewarming &&
221 transaction_count >= SMALL_BLOCK_TX_THRESHOLD
222 }
223
224 const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
231
232 const PARALLEL_PREFETCH_COUNT: usize = 4;
240
241 const FIRST_PARALLEL_TX_WINDOW_SIZE: usize = 64;
243
244 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
256 fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
257 &self,
258 transactions: I,
259 transaction_count: usize,
260 parallel_bal_execution: bool,
261 prewarm_transactions: bool,
262 ) -> (Option<IteratorPrewarmTxReceiver<Evm, I>>, IteratorExecuteTxReceiver<Evm, I>) {
263 let (prewarm_tx, prewarm_rx) =
264 prewarm_transactions.then(|| mpsc::sync_channel(transaction_count)).unzip();
265 let (execute_tx, execute_rx) = crossbeam_channel::bounded(transaction_count);
266
267 if transaction_count == 0 {
268 } else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
270 debug!(
273 target: "engine::tree::payload_processor",
274 transaction_count,
275 "using sequential sig recovery for small block"
276 );
277 self.executor.spawn_blocking_named("tx-iterator", move || {
278 let (transactions, convert) = transactions.into_parts();
279 convert_serial(
280 transactions.into_iter(),
281 &convert,
282 prewarm_tx.as_ref(),
283 &execute_tx,
284 );
285 });
286 } else {
287 let executor = self.executor.clone();
290 self.executor.spawn_blocking_named("tx-iterator", move || {
291 let (transactions, convert) = transactions.into_parts();
292 if parallel_bal_execution {
293 executor.cpu_pool().install(|| {
296 let _ = transactions
297 .into_par_iter()
298 .enumerate()
299 .try_for_each(|(idx, tx)| {
300 let tx = convert.convert(tx).map(WithTxEnv::new);
301 let failed = tx.is_err();
302 if let (Some(prewarm_tx), Ok(tx)) = (&prewarm_tx, &tx) {
303 let _ = prewarm_tx.send((idx, tx.clone()));
304 }
305 let disconnected = execute_tx.send((idx, tx)).is_err();
306 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
307 if failed || disconnected {
308 Err(())
309 } else {
310 Ok(())
311 }
312 });
313 });
314 } else {
315 let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
319 let mut iter = transactions.into_iter();
320
321 if !convert_serial(
324 iter.by_ref().take(prefetch),
325 &convert,
326 prewarm_tx.as_ref(),
327 &execute_tx,
328 ) {
329 return
330 }
331
332 let mut iter = iter.enumerate();
333
334 let mut batch_size = Self::FIRST_PARALLEL_TX_WINDOW_SIZE;
335
336 executor.cpu_pool().install(move || {
339 loop {
340 let chunk = iter
341 .by_ref()
342 .take(batch_size)
343 .collect::<Vec<_>>();
344 if chunk.is_empty() {
345 break;
346 }
347
348 batch_size = batch_size.saturating_mul(2);
349
350 let chunk = chunk
351 .into_par_iter()
352 .map(|(i, tx)| {
353 let idx = i + prefetch;
354 let tx = convert.convert(tx).map(WithTxEnv::new);
355 (idx, tx)
356 })
357 .collect::<Vec<_>>();
358
359 for (idx, tx) in chunk {
360 let failed = tx.is_err();
361 if let (Some(prewarm_tx), Ok(tx)) = (&prewarm_tx, &tx) {
362 let _ = prewarm_tx.send((idx, tx.clone()));
363 }
364 if execute_tx.send((idx, tx)).is_err() || failed {
365 return
366 }
367 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
368 }
369 }
370 });
371 }
372 });
373 }
374
375 (prewarm_rx, execute_rx)
376 }
377
378 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
384 fn spawn_caching_with<P>(
385 &self,
386 env: ExecutionEnv<Evm>,
387 transactions: Option<
388 mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + 'static)>,
389 >,
390 state_provider_factory: OverlayStateProviderFactory<P, Evm::Primitives>,
391 hint_stream: Option<StateRootHintStream>,
392 hashed_update_stream: Option<StateRootUpdateStream>,
393 parallel_bal_execution: bool,
394 ) -> CacheTaskHandle<<Evm::Primitives as NodePrimitives>::Receipt>
395 where
396 P: DatabaseProviderFactory + Clone + 'static,
397 P::Provider: BlockNumReader
398 + PruneCheckpointReader
399 + StageCheckpointReader
400 + ChangeSetReader
401 + StorageChangeSetReader
402 + StorageSettingsCache
403 + HistoryReader
404 + 'static,
405 {
406 let mode = if parallel_bal_execution {
409 PrewarmMode::BlockAccessList {
410 bal: env.decoded_bal.clone().expect("BAL dispatch implies decoded BAL"),
411 updates: hashed_update_stream,
412 }
413 } else if let Some(pending) = transactions {
414 PrewarmMode::Transactions { pending, hints: hint_stream }
415 } else {
416 PrewarmMode::Skipped
417 };
418 let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
419
420 let executed_tx_index = Arc::new(AtomicUsize::new(0));
421 let prewarm_ctx = PrewarmContext {
423 env,
424 evm_config: self.evm_config.clone(),
425 saved_cache: saved_cache.clone(),
426 provider: state_provider_factory,
427 bal_prewarm_pool: parallel_bal_execution.then(|| self.bal_prewarm_pool()),
428 metrics: PrewarmMetrics::default(),
429 cache_metrics: self.cache_metrics.clone(),
430 cache_state_metrics: self.cache_state_metrics.clone(),
431 terminate_execution: Arc::new(AtomicBool::new(false)),
432 executed_tx_index: Arc::clone(&executed_tx_index),
433 precompile_cache_disabled: self.precompile_cache_disabled,
434 precompile_cache_map: self.precompile_cache_map.clone(),
435 disable_bal_parallel_state_root: self.disable_bal_parallel_state_root,
436 disable_bal_batch_io: self.disable_bal_batch_io,
437 };
438
439 let (prewarm_task, to_prewarm_task) =
440 PrewarmCacheTask::new(self.executor.clone(), self.execution_cache.clone(), prewarm_ctx);
441 {
442 let to_prewarm_task = to_prewarm_task.clone();
443 self.executor.spawn_blocking_named("prewarm", move || {
444 prewarm_task.run(mode, to_prewarm_task);
445 });
446 }
447
448 CacheTaskHandle {
449 saved_cache,
450 to_prewarm_task: Some(to_prewarm_task),
451 executed_tx_index,
452 cache_metrics: self.cache_metrics.clone(),
453 }
454 }
455
456 #[instrument(level = "debug", target = "engine::caching", skip(self))]
461 pub fn cache_for(&self, parent_hash: B256) -> SavedCache {
462 if let Some(cache) = self.execution_cache.get_cache_for(parent_hash) {
463 debug!("reusing execution cache");
464 cache
465 } else {
466 debug!("creating new execution cache on cache miss");
467 let start = Instant::now();
468 let cache = ExecutionCache::new(self.cross_block_cache_size);
469 if let Some(metrics) = &self.cache_metrics {
470 metrics.record_cache_creation(start.elapsed());
471 }
472 SavedCache::new(parent_hash, cache)
473 }
474 }
475
476 pub fn on_inserted_executed_block(
484 &self,
485 block_with_parent: BlockWithParent,
486 bundle_state: &BundleState,
487 ) {
488 let cache_state_metrics = self.cache_state_metrics.clone();
489 self.execution_cache.update_with_guard(|cached| {
490 if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
491 debug!(
492 target: "engine::caching",
493 parent_hash = %block_with_parent.parent,
494 "Cannot find cache for parent hash, skip updating cache with new state for inserted executed block",
495 );
496 return
497 }
498
499 if let Some(cache) = cached.as_ref().filter(|cache| !cache.is_available()) {
500 debug!(
501 target: "engine::caching",
502 parent_hash = %block_with_parent.parent,
503 usage_count = cache.usage_count(),
504 "Execution cache is in use, skip updating cache with new state for inserted executed block",
505 );
506 return
507 }
508
509 let caches = match cached.take() {
511 Some(existing) => existing.cache().clone(),
512 None => ExecutionCache::new(self.cross_block_cache_size),
513 };
514
515 let new_cache = SavedCache::new(block_with_parent.block.hash, caches);
517 if new_cache.cache().insert_state(bundle_state).is_err() {
518 *cached = None;
519 debug!(target: "engine::caching", "cleared execution cache on update error");
520 return
521 }
522 new_cache.update_metrics(cache_state_metrics.as_ref());
523
524 *cached = Some(new_cache);
526 debug!(target: "engine::caching", ?block_with_parent, "Updated execution cache for inserted block");
527 });
528 }
529}
530
531fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
534 iter: impl Iterator<Item = RawTx>,
535 convert: &C,
536 prewarm_tx: Option<&mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>>,
537 execute_tx: &ExecuteTxSender<TxEnv, Recovered, Err>,
538) -> bool
539where
540 Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
541 TxEnv: Clone,
542 C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
543{
544 for (idx, raw_tx) in iter.enumerate() {
545 let tx = convert.convert(raw_tx);
546 let failed = tx.is_err();
547 let tx = tx.map(WithTxEnv::new);
548 if let (Some(prewarm_tx), Ok(tx)) = (prewarm_tx, &tx) {
549 let _ = prewarm_tx.send((idx, tx.clone()));
550 }
551 if execute_tx.send((idx, tx)).is_err() || failed {
552 return false
553 }
554 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
555 }
556 true
557}
558
559#[derive(Debug)]
564pub struct PayloadHandle<Tx, Err, R> {
565 prewarm_handle: CacheTaskHandle<R>,
566 transactions: IndexedTxReceiver<Tx, Err>,
568 _span: Span,
570}
571
572impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
573 pub fn caches(&self) -> Option<ExecutionCache> {
575 self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
576 }
577
578 pub fn cache_metrics(&self) -> Option<CachedStateMetrics> {
580 self.prewarm_handle.cache_metrics.clone()
581 }
582
583 pub const fn executed_tx_index(&self) -> &Arc<AtomicUsize> {
588 &self.prewarm_handle.executed_tx_index
589 }
590
591 pub fn stop_prewarming_execution(&self) {
595 self.prewarm_handle.stop_prewarming_execution()
596 }
597
598 pub fn terminate_caching(
606 &mut self,
607 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
608 ) -> Option<mpsc::Sender<()>> {
609 self.prewarm_handle.terminate_caching(execution_outcome)
610 }
611
612 pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
614 self.transactions.iter().map(|(_, tx)| tx)
615 }
616
617 pub fn clone_transaction_receiver(&self) -> IndexedTxReceiver<Tx, Err> {
619 self.transactions.clone()
620 }
621}
622
623#[derive(Debug)]
628pub struct CacheTaskHandle<R> {
629 saved_cache: Option<SavedCache>,
631 to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
633 executed_tx_index: Arc<AtomicUsize>,
636 cache_metrics: Option<CachedStateMetrics>,
638}
639
640impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
641 pub fn stop_prewarming_execution(&self) {
645 self.to_prewarm_task
646 .as_ref()
647 .map(|tx| tx.send(PrewarmTaskEvent::TerminateTransactionExecution).ok());
648 }
649
650 #[must_use = "sender must be used and notified on block validation success"]
655 pub fn terminate_caching(
656 &mut self,
657 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
658 ) -> Option<mpsc::Sender<()>> {
659 if let Some(tx) = self.to_prewarm_task.take() {
660 let (valid_block_tx, valid_block_rx) = mpsc::channel();
661 let event = PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx };
662 let _ = tx.send(event);
663
664 Some(valid_block_tx)
665 } else {
666 None
667 }
668 }
669}
670
671impl<R> Drop for CacheTaskHandle<R> {
672 fn drop(&mut self) {
673 if let Some(tx) = self.to_prewarm_task.take() {
675 let _ = tx.send(PrewarmTaskEvent::Terminate {
676 execution_outcome: None,
677 valid_block_rx: mpsc::channel().1,
678 });
679 }
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use crate::tree::{
686 payload_processor::PayloadProcessor, precompile_cache::PrecompileCacheMap, ExecutionCache,
687 PayloadExecutionCache, SavedCache, TreeConfig,
688 };
689 use alloy_consensus::constants::KECCAK_EMPTY;
690 use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
691 use alloy_primitives::{Address, B256, U256};
692 use reth_chainspec::ChainSpec;
693 use reth_evm_ethereum::EthEvmConfig;
694 use reth_execution_cache::CachedStatus;
695 use reth_revm::db::BundleState;
696 use revm::state::AccountInfo;
697 use std::sync::{atomic::Ordering, Arc};
698
699 type TestTx = reth_evm::execute::WithTxEnv<
700 reth_evm::TxEnvFor<EthEvmConfig>,
701 reth_primitives_traits::Recovered<reth_ethereum_primitives::TransactionSigned>,
702 >;
703
704 fn converted_tx() -> TestTx {
705 TestTx {
706 tx_env: Default::default(),
707 tx: Arc::new(reth_primitives_traits::Recovered::new_unchecked(
708 reth_ethereum_primitives::TransactionSigned::Legacy(
709 alloy_consensus::Signed::new_unchecked(
710 alloy_consensus::TxLegacy::default(),
711 alloy_primitives::Signature::test_signature(),
712 B256::ZERO,
713 ),
714 ),
715 Address::ZERO,
716 )),
717 }
718 }
719
720 fn test_processor() -> PayloadProcessor<EthEvmConfig> {
721 PayloadProcessor::new(
722 reth_tasks::Runtime::test(),
723 EthEvmConfig::new(Arc::new(ChainSpec::default())),
724 &TreeConfig::default(),
725 PrecompileCacheMap::default(),
726 )
727 }
728
729 #[test]
730 fn transaction_conversion_preserves_results() {
731 for (count, bal) in [(10, false), (200, false), (200, true)] {
732 let processor = test_processor();
733 let (prewarm, receiver) = processor.spawn_tx_iterator(
734 ((0..count).collect::<Vec<_>>(), |_| Ok::<_, std::io::Error>(converted_tx())),
735 count,
736 bal,
737 true,
738 );
739 let mut indices = Vec::new();
740 for _ in 0..count {
741 let (idx, tx) = receiver.recv_timeout(std::time::Duration::from_secs(10)).unwrap();
742 assert!(tx.is_ok());
743 indices.push(idx);
744 }
745 if bal {
746 indices.sort_unstable();
747 }
748 assert_eq!(indices, (0..count).collect::<Vec<_>>());
749 assert_eq!(prewarm.unwrap().iter().count(), count);
750 }
751 }
752
753 #[test]
754 fn transaction_conversion_stops_on_error() {
755 for (count, bal, fail_at) in
756 [(10, false, 0), (200, false, 0), (200, false, 4), (200, false, 20), (200, true, 0)]
757 {
758 let processor = test_processor();
759 let (_, receiver) = processor.spawn_tx_iterator(
760 ((0..count).collect::<Vec<_>>(), move |idx| {
761 if idx >= fail_at {
762 Err(std::io::Error::other("invalid transaction"))
763 } else {
764 Ok(converted_tx())
765 }
766 }),
767 count,
768 bal,
769 false,
770 );
771 let mut results = Vec::new();
772 loop {
773 match receiver.recv_timeout(std::time::Duration::from_secs(10)) {
774 Ok(tx) => results.push(tx),
775 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
776 Err(err) => panic!("conversion did not terminate: {err}"),
777 }
778 }
779 assert!(results.iter().any(|(_, tx)| tx.is_err()));
780 assert!(results.len() < count);
781 if !bal {
782 assert_eq!(results.len(), fail_at + 1);
783 assert!(results.last().unwrap().1.is_err());
784 }
785 }
786 }
787
788 #[test]
789 fn dropping_payload_handle_stops_transaction_conversion() {
790 for (count, bal, pause_at) in
791 [(10, false, 0), (1000, false, 0), (1000, false, 4), (1000, true, 0)]
792 {
793 let processor = test_processor();
794 let calls = Arc::new(super::AtomicUsize::new(0));
795 let converted = calls.clone();
796 let (started_tx, started_rx) = crossbeam_channel::unbounded();
797 let (release_tx, release_rx) = crossbeam_channel::bounded::<()>(0);
798 let (_, receiver) = processor.spawn_tx_iterator(
799 ((0..count).collect::<Vec<_>>(), move |idx| {
800 converted.fetch_add(1, Ordering::Relaxed);
801 if idx >= pause_at {
802 started_tx.send(()).unwrap();
803 let _ = release_rx.recv_timeout(std::time::Duration::from_secs(10));
804 }
805 Ok::<_, std::io::Error>(converted_tx())
806 }),
807 count,
808 bal,
809 false,
810 );
811 let handle = super::PayloadHandle {
812 prewarm_handle: super::CacheTaskHandle::<()> {
813 saved_cache: None,
814 to_prewarm_task: None,
815 executed_tx_index: Default::default(),
816 cache_metrics: None,
817 },
818 transactions: receiver,
819 _span: tracing::Span::none(),
820 };
821 started_rx.recv_timeout(std::time::Duration::from_secs(10)).unwrap();
822 drop(handle);
823 drop(release_tx);
824
825 loop {
827 match started_rx.recv_timeout(std::time::Duration::from_secs(10)) {
828 Ok(()) => {}
829 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
830 Err(err) => panic!("conversion did not terminate: {err}"),
831 }
832 }
833 assert!(calls.load(Ordering::Relaxed) < count);
834 }
835 }
836
837 fn make_saved_cache(hash: B256) -> SavedCache {
838 let execution_cache = ExecutionCache::new(1_000);
839 SavedCache::new(hash, execution_cache)
840 }
841
842 #[test]
843 fn execution_cache_allows_single_checkout() {
844 let execution_cache = PayloadExecutionCache::default();
845 let hash = B256::from([1u8; 32]);
846
847 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
848
849 let first = execution_cache.get_cache_for(hash);
850 assert!(first.is_some(), "expected initial checkout to succeed");
851
852 let second = execution_cache.get_cache_for(hash);
853 assert!(second.is_none(), "second checkout should be blocked while guard is active");
854
855 drop(first);
856
857 let third = execution_cache.get_cache_for(hash);
858 assert!(third.is_some(), "third checkout should succeed after guard is dropped");
859 }
860
861 #[test]
862 fn execution_cache_checkout_releases_on_drop() {
863 let execution_cache = PayloadExecutionCache::default();
864 let hash = B256::from([2u8; 32]);
865
866 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
867
868 {
869 let guard = execution_cache.get_cache_for(hash);
870 assert!(guard.is_some(), "expected checkout to succeed");
871 }
873
874 let retry = execution_cache.get_cache_for(hash);
875 assert!(retry.is_some(), "checkout should succeed after guard drop");
876 }
877
878 #[test]
879 fn execution_cache_mismatch_parent_clears_and_returns() {
880 let execution_cache = PayloadExecutionCache::default();
881 let hash = B256::from([3u8; 32]);
882
883 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
884
885 let different_hash = B256::from([4u8; 32]);
888 let cache = execution_cache.get_cache_for(different_hash);
889 assert!(cache.is_some(), "cache should be returned for reuse after clearing");
890
891 drop(cache);
892
893 let original = execution_cache.get_cache_for(hash);
896 assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
897 }
898
899 #[test]
900 fn execution_cache_update_after_release_succeeds() {
901 let execution_cache = PayloadExecutionCache::default();
902 let initial = B256::from([5u8; 32]);
903
904 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(initial)));
905
906 let guard =
907 execution_cache.get_cache_for(initial).expect("expected initial checkout to succeed");
908
909 drop(guard);
910
911 let updated = B256::from([6u8; 32]);
912 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(updated)));
913
914 let new_checkout = execution_cache.get_cache_for(updated);
915 assert!(new_checkout.is_some(), "new checkout should succeed after release and update");
916 }
917
918 #[test]
919 fn on_inserted_executed_block_populates_cache() {
920 let payload_processor = PayloadProcessor::new(
921 reth_tasks::Runtime::test(),
922 EthEvmConfig::new(Arc::new(ChainSpec::default())),
923 &TreeConfig::default(),
924 PrecompileCacheMap::default(),
925 );
926
927 let parent_hash = B256::from([1u8; 32]);
928 let block_hash = B256::from([10u8; 32]);
929 let block_with_parent = BlockWithParent {
930 block: BlockNumHash { hash: block_hash, number: 1 },
931 parent: parent_hash,
932 };
933 let bundle_state = BundleState::default();
934
935 assert!(payload_processor.execution_cache.get_cache_for(block_hash).is_none());
937
938 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
940
941 let cached = payload_processor.execution_cache.get_cache_for(block_hash);
943 assert!(cached.is_some());
944 assert_eq!(cached.unwrap().executed_block_hash(), block_hash);
945 }
946
947 #[test]
948 fn on_inserted_executed_block_skips_on_parent_mismatch() {
949 let payload_processor = PayloadProcessor::new(
950 reth_tasks::Runtime::test(),
951 EthEvmConfig::new(Arc::new(ChainSpec::default())),
952 &TreeConfig::default(),
953 PrecompileCacheMap::default(),
954 );
955
956 let block1_hash = B256::from([1u8; 32]);
958 payload_processor
959 .execution_cache
960 .update_with_guard(|slot| *slot = Some(make_saved_cache(block1_hash)));
961
962 let wrong_parent = B256::from([99u8; 32]);
964 let block3_hash = B256::from([3u8; 32]);
965 let block_with_parent = BlockWithParent {
966 block: BlockNumHash { hash: block3_hash, number: 3 },
967 parent: wrong_parent,
968 };
969 let bundle_state = BundleState::default();
970
971 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
972
973 let cached = payload_processor.execution_cache.get_cache_for(block1_hash);
975 assert!(cached.is_some(), "Original cache should be preserved");
976
977 let cached3 = payload_processor.execution_cache.get_cache_for(block3_hash);
979 assert!(cached3.is_none(), "New block cache should not be created on mismatch");
980 }
981
982 #[test]
983 fn on_inserted_executed_block_does_not_mutate_checked_out_parent_cache() {
984 let payload_processor = PayloadProcessor::new(
985 reth_tasks::Runtime::test(),
986 EthEvmConfig::new(Arc::new(ChainSpec::default())),
987 &TreeConfig::default(),
988 PrecompileCacheMap::default(),
989 );
990
991 let parent_hash = B256::from([1u8; 32]);
992 payload_processor
993 .execution_cache
994 .update_with_guard(|slot| *slot = Some(make_saved_cache(parent_hash)));
995
996 let checked_out = payload_processor
1000 .execution_cache
1001 .get_cache_for(parent_hash)
1002 .expect("expected parent cache checkout to succeed");
1003
1004 let polluted_address = Address::random();
1005 let bundle_state = BundleState::builder(2..=2)
1006 .state_present_account_info(
1007 polluted_address,
1008 AccountInfo {
1009 balance: U256::from(1337),
1010 nonce: 7,
1011 code_hash: KECCAK_EMPTY,
1012 code: None,
1013 account_id: None,
1014 },
1015 )
1016 .build();
1017
1018 let block_with_parent = BlockWithParent {
1021 block: BlockNumHash { hash: B256::from([2u8; 32]), number: 2 },
1022 parent: parent_hash,
1023 };
1024
1025 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
1026
1027 let account = checked_out
1030 .cache()
1031 .get_or_try_insert_account_with(polluted_address, || Ok::<_, ()>(None))
1032 .expect("cache read should succeed");
1033
1034 assert_eq!(
1035 account,
1036 CachedStatus::NotCached(None),
1037 "checked-out parent cache should not observe state from inserted local block"
1038 );
1039 }
1040
1041 #[test]
1052 fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
1053 let execution_cache = PayloadExecutionCache::default();
1054
1055 let block4_hash = B256::from([4u8; 32]);
1057 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
1058
1059 let fork_parent = B256::from([2u8; 32]);
1062 let prewarm_cache = execution_cache.get_cache_for(fork_parent);
1063 assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
1064 let prewarm_cache = prewarm_cache.unwrap();
1065 assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
1066
1067 let fork_addr = Address::from([0xBB; 20]);
1070 let fork_key = B256::from([0xCC; 32]);
1071 prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
1072
1073 let during_prewarm = execution_cache.get_cache_for(block4_hash);
1075 assert!(
1076 during_prewarm.is_none(),
1077 "cache must be unavailable while prewarm holds a reference"
1078 );
1079
1080 drop(prewarm_cache);
1082
1083 let block5_cache = execution_cache.get_cache_for(block4_hash);
1087 assert!(
1088 block5_cache.is_some(),
1089 "canonical chain must get cache after fork prewarm is dropped"
1090 );
1091 assert_eq!(
1092 block5_cache.as_ref().unwrap().executed_block_hash(),
1093 block4_hash,
1094 "cache must carry the canonical parent hash, not the fork parent"
1095 );
1096 }
1097}