1use crate::tree::{
4 payload_processor::{
5 prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
6 reuse::{cfg_reuses_prewarm_results, PrewarmResults},
7 },
8 CacheCheckout, CachedStateCacheMetrics, CachedStateMetrics, CachedStateMetricsSource,
9 ExecutionCache, ExecutionEnv, PayloadExecutionCache, SavedCache, TreeConfig,
10};
11use alloy_eips::eip1898::BlockWithParent;
12use alloy_primitives::B256;
13use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
14use prewarm::PrewarmMetrics;
15use rayon::prelude::*;
16use reth_evm::{
17 precompile_cache::PrecompileCacheMap, ConfigureEvm, ConvertTx, EvmEnv, EvmTypesFor,
18 ExecutableTxFor, ExecutableTxIterator, ExecutableTxParts, ExecutableTxTuple, SpecFor, TxEnvFor,
19 WithTxEnv,
20};
21use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
22use reth_provider::{
23 BlockExecutionOutput, BlockNumReader, ChangeSetReader, DatabaseProviderFactory, HistoryReader,
24 PruneCheckpointReader, StageCheckpointReader, StorageChangeSetReader, StorageSettingsCache,
25};
26use reth_storage_overlay::OverlayStateProviderFactory;
27use reth_tasks::Runtime;
28use reth_trie_common::bundle_state::BundleState;
29pub use reth_trie_parallel::{
30 error::StateRootTaskError,
31 state_root_task::{
32 PayloadStateRootHandle, StateAccessHint, StateRootComputeOutcome, StateRootHandle,
33 StateRootHintStream, StateRootMessage, StateRootTaskCancelGuard, StateRootUpdateHook,
34 StateRootUpdateStream,
35 },
36};
37use std::{
38 ops::Not,
39 sync::{
40 atomic::{AtomicBool, AtomicUsize},
41 mpsc, Arc, OnceLock,
42 },
43};
44use tracing::{debug, debug_span, instrument, trace, trace_span, warn, Span};
45
46pub mod bal;
47pub mod bal_prewarm_pool;
48pub mod prewarm;
49pub mod receipt_root_task;
50pub(crate) mod reuse;
51
52pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
55
56type IteratorTx<Evm, I> = RecoveredTx<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
58
59type IteratorPayloadHandle<Evm, I> = PayloadHandle<
60 IteratorTx<Evm, I>,
61 <I as ExecutableTxTuple>::Error,
62 <<Evm as ConfigureEvm>::Primitives as NodePrimitives>::Receipt,
63 EvmTypesFor<Evm>,
64>;
65
66type IteratorPrewarmTxReceiver<Evm, I> =
67 PrewarmTxReceiver<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>;
68
69type IteratorExecuteTxReceiver<Evm, I> = ExecuteTxReceiver<
70 TxEnvFor<Evm>,
71 <I as ExecutableTxIterator<Evm>>::Recovered,
72 <I as ExecutableTxTuple>::Error,
73>;
74
75type RecoveredTx<TxEnv, Recovered> = WithTxEnv<TxEnv, Recovered>;
76type IndexedTxResult<Tx, Err> = (usize, Result<Tx, Err>);
77type IndexedTxReceiver<Tx, Err> = CrossbeamReceiver<IndexedTxResult<Tx, Err>>;
78type IndexedTxSender<Tx, Err> = CrossbeamSender<IndexedTxResult<Tx, Err>>;
79type PrewarmTxReceiver<TxEnv, Recovered> = mpsc::Receiver<(usize, RecoveredTx<TxEnv, Recovered>)>;
80type ExecuteTxReceiver<TxEnv, Recovered, Err> =
81 IndexedTxReceiver<RecoveredTx<TxEnv, Recovered>, Err>;
82type ExecuteTxSender<TxEnv, Recovered, Err> = IndexedTxSender<RecoveredTx<TxEnv, Recovered>, Err>;
83
84#[derive(Debug)]
86pub struct PayloadProcessor<Evm>
87where
88 Evm: ConfigureEvm,
89{
90 executor: Runtime,
92 execution_cache: PayloadExecutionCache,
94 cache_metrics: Option<CachedStateMetrics>,
96 cache_state_metrics: Option<CachedStateCacheMetrics>,
98 cross_block_cache_size: usize,
100 disable_transaction_prewarming: bool,
102 disable_state_cache: bool,
104 evm_config: Evm,
106 precompile_cache_disabled: bool,
108 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
110 disable_bal_parallel_state_root: bool,
113 disable_bal_batch_io: bool,
115 reuse_prewarm_results: bool,
117 bal_prewarm_pool: OnceLock<Arc<bal_prewarm_pool::BalPrewarmPool>>,
120}
121
122impl<Evm> PayloadProcessor<Evm>
123where
124 Evm: ConfigureEvm,
125{
126 pub fn new(
128 executor: Runtime,
129 evm_config: Evm,
130 config: &TreeConfig,
131 precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
132 ) -> Self {
133 Self {
134 executor,
135 execution_cache: Default::default(),
136 cross_block_cache_size: config.cross_block_cache_size(),
137 disable_transaction_prewarming: config.disable_prewarming(),
138 evm_config,
139 disable_state_cache: config.disable_state_cache(),
140 precompile_cache_disabled: config.precompile_cache_disabled(),
141 precompile_cache_map,
142 cache_metrics: (!config.disable_cache_metrics())
143 .then(|| CachedStateMetrics::zeroed(CachedStateMetricsSource::Engine)),
144 cache_state_metrics: (!config.disable_cache_metrics())
145 .then(CachedStateCacheMetrics::default),
146 disable_bal_parallel_state_root: config.disable_bal_parallel_state_root(),
147 disable_bal_batch_io: config.disable_bal_batch_io(),
148 reuse_prewarm_results: config.prewarm_reuse(),
149 bal_prewarm_pool: OnceLock::new(),
150 }
151 }
152
153 fn bal_prewarm_pool(&self) -> Arc<bal_prewarm_pool::BalPrewarmPool> {
156 self.bal_prewarm_pool
157 .get_or_init(|| {
158 bal_prewarm_pool::BalPrewarmPool::new(bal_prewarm_pool::DEFAULT_BAL_PREWARM_THREADS)
159 })
160 .clone()
161 }
162
163 pub(crate) fn execution_cache(&self) -> PayloadExecutionCache {
165 self.execution_cache.clone()
166 }
167}
168
169impl<Evm> PayloadProcessor<Evm>
170where
171 Evm: ConfigureEvm + 'static,
172{
173 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
176 pub fn spawn_with_state_root_streams<P, I: ExecutableTxIterator<Evm>>(
177 &self,
178 env: ExecutionEnv<Evm>,
179 transactions: I,
180 state_provider_factory: OverlayStateProviderFactory<P, Evm::Primitives>,
181 hint_stream: Option<StateRootHintStream>,
182 hashed_update_stream: Option<StateRootUpdateStream>,
183 parallel_bal_execution: bool,
184 ) -> IteratorPayloadHandle<Evm, I>
185 where
186 P: DatabaseProviderFactory + Clone + 'static,
187 P::Provider: BlockNumReader
188 + PruneCheckpointReader
189 + StageCheckpointReader
190 + ChangeSetReader
191 + StorageChangeSetReader
192 + StorageSettingsCache
193 + HistoryReader
194 + 'static,
195 {
196 let prewarm_transactions =
197 self.prewarms_transactions(env.transaction_count, parallel_bal_execution);
198 let (prewarm_rx, execution_rx) = self.spawn_tx_iterator(
199 transactions,
200 env.transaction_count,
201 parallel_bal_execution,
202 prewarm_transactions,
203 );
204 let prewarm_handle = self.spawn_caching_with(
205 env,
206 prewarm_rx,
207 state_provider_factory,
208 hint_stream,
209 hashed_update_stream,
210 parallel_bal_execution,
211 );
212 PayloadHandle { prewarm_handle, transactions: execution_rx, _span: Span::current() }
213 }
214
215 const fn prewarms_transactions(
222 &self,
223 transaction_count: usize,
224 parallel_bal_execution: bool,
225 ) -> bool {
226 !parallel_bal_execution &&
227 !self.disable_transaction_prewarming &&
228 transaction_count >= SMALL_BLOCK_TX_THRESHOLD
229 }
230
231 const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
238
239 const PARALLEL_PREFETCH_COUNT: usize = 4;
247
248 const FIRST_PARALLEL_TX_WINDOW_SIZE: usize = 64;
250
251 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
263 fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
264 &self,
265 transactions: I,
266 transaction_count: usize,
267 parallel_bal_execution: bool,
268 prewarm_transactions: bool,
269 ) -> (Option<IteratorPrewarmTxReceiver<Evm, I>>, IteratorExecuteTxReceiver<Evm, I>) {
270 let (prewarm_tx, prewarm_rx) =
271 prewarm_transactions.then(|| mpsc::sync_channel(transaction_count)).unzip();
272 let (execute_tx, execute_rx) = crossbeam_channel::bounded(transaction_count);
273 let parent_span = Span::current();
274
275 if transaction_count == 0 {
276 } else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
278 debug!(
281 target: "engine::tree::payload_processor",
282 transaction_count,
283 "using sequential sig recovery for small block"
284 );
285 self.executor.spawn_blocking_named("tx-iterator", move || {
286 let _parent = parent_span.enter();
288 let _span = debug_span!(target: "engine::tree::payload_processor", "tx_iterator", transaction_count).entered();
289 let (transactions, convert) = transactions.into_parts();
290 convert_serial(
291 transactions.into_iter(),
292 &convert,
293 prewarm_tx.as_ref(),
294 &execute_tx,
295 );
296 });
297 } else {
298 let executor = self.executor.clone();
301 self.executor.spawn_blocking_named("tx-iterator", move || {
302 let _parent = parent_span.enter();
303 let _span = debug_span!(target: "engine::tree::payload_processor", "tx_iterator", transaction_count).entered();
304 let (transactions, convert) = transactions.into_parts();
305 if parallel_bal_execution {
306 let parent_span = Span::current();
309 executor.cpu_pool().install(|| {
310 let _parent = parent_span.enter();
311 let span = debug_span!(target: "engine::tree::payload_processor", "convert_transactions").or_current();
312 let _entered = span.enter();
313 let _ = transactions
314 .into_par_iter()
315 .enumerate()
316 .by_exponential_blocks()
318 .try_for_each(|(idx, tx)| {
319 let _parent = span.enter();
320 let _span = trace_span!(target: "engine::tree::payload_processor", "convert_transaction", idx).entered();
321 let tx = convert.convert(tx).map(WithTxEnv::new);
322 if let (Some(prewarm_tx), Ok(tx)) = (&prewarm_tx, &tx) {
323 let _ = prewarm_tx.send((idx, tx.clone()));
324 }
325 let disconnected = execute_tx.send((idx, tx)).is_err();
326 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
327 if disconnected {
330 Err(())
331 } else {
332 Ok(())
333 }
334 });
335 });
336 } else {
337 let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
341 let mut iter = transactions.into_iter();
342
343 if !convert_serial(
346 iter.by_ref().take(prefetch),
347 &convert,
348 prewarm_tx.as_ref(),
349 &execute_tx,
350 ) {
351 return
352 }
353
354 let mut iter = iter.enumerate();
355
356 let mut batch_size = Self::FIRST_PARALLEL_TX_WINDOW_SIZE;
357
358 let parent_span = Span::current();
361 executor.cpu_pool().install(move || {
362 let _parent = parent_span.enter();
363 let span = debug_span!(target: "engine::tree::payload_processor", "convert_transactions").or_current();
364 let _entered = span.enter();
365 loop {
366 let chunk = iter
367 .by_ref()
368 .take(batch_size)
369 .collect::<Vec<_>>();
370 if chunk.is_empty() {
371 break;
372 }
373
374 batch_size = batch_size.saturating_mul(2);
375
376 let chunk = chunk
377 .into_par_iter()
378 .map(|(i, tx)| {
379 let idx = i + prefetch;
380 let _parent = span.enter();
381 let _span = trace_span!(target: "engine::tree::payload_processor", "convert_transaction", idx).entered();
382 let tx = convert.convert(tx).map(WithTxEnv::new);
383 (idx, tx)
384 })
385 .collect::<Vec<_>>();
386
387 for (idx, tx) in chunk {
388 let failed = tx.is_err();
389 if let (Some(prewarm_tx), Ok(tx)) = (&prewarm_tx, &tx) {
390 let _ = prewarm_tx.send((idx, tx.clone()));
391 }
392 if execute_tx.send((idx, tx)).is_err() || failed {
393 return
394 }
395 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
396 }
397 }
398 });
399 }
400 });
401 }
402
403 (prewarm_rx, execute_rx)
404 }
405
406 #[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
412 fn spawn_caching_with<P>(
413 &self,
414 env: ExecutionEnv<Evm>,
415 transactions: Option<
416 mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + Sync + 'static)>,
417 >,
418 state_provider_factory: OverlayStateProviderFactory<P, Evm::Primitives>,
419 hint_stream: Option<StateRootHintStream>,
420 hashed_update_stream: Option<StateRootUpdateStream>,
421 parallel_bal_execution: bool,
422 ) -> CacheTaskHandle<<Evm::Primitives as NodePrimitives>::Receipt, EvmTypesFor<Evm>>
423 where
424 P: DatabaseProviderFactory + Clone + 'static,
425 P::Provider: BlockNumReader
426 + PruneCheckpointReader
427 + StageCheckpointReader
428 + ChangeSetReader
429 + StorageChangeSetReader
430 + StorageSettingsCache
431 + HistoryReader
432 + 'static,
433 {
434 let mode = if parallel_bal_execution {
437 PrewarmMode::BlockAccessList {
438 bal: env.decoded_bal.clone().expect("BAL dispatch implies decoded BAL"),
439 updates: hashed_update_stream,
440 }
441 } else if let Some(pending) = transactions {
442 PrewarmMode::Transactions { pending, hints: hint_stream }
443 } else {
444 PrewarmMode::Skipped
445 };
446 let saved_cache =
447 self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash)).flatten();
448
449 let prewarm_results = (self.reuse_prewarm_results &&
453 matches!(mode, PrewarmMode::Transactions { .. }) &&
454 env.decoded_bal.is_none() &&
455 cfg_reuses_prewarm_results(env.evm_env.version()) &&
456 self.evm_config.reuses_prewarm_results())
457 .then(|| Arc::new(PrewarmResults::new(env.transaction_count)));
458
459 let executed_tx_index = Arc::new(AtomicUsize::new(0));
460 let prewarm_ctx = PrewarmContext {
462 env,
463 evm_config: self.evm_config.clone(),
464 saved_cache: saved_cache.clone(),
465 provider: state_provider_factory,
466 bal_prewarm_pool: parallel_bal_execution.then(|| self.bal_prewarm_pool()),
467 metrics: PrewarmMetrics::default(),
468 cache_metrics: self.cache_metrics.clone(),
469 cache_state_metrics: self.cache_state_metrics.clone(),
470 terminate_execution: Arc::new(AtomicBool::new(false)),
471 executed_tx_index: Arc::clone(&executed_tx_index),
472 precompile_cache_disabled: self.precompile_cache_disabled,
473 precompile_cache_map: self.precompile_cache_map.clone(),
474 disable_bal_parallel_state_root: self.disable_bal_parallel_state_root,
475 disable_bal_batch_io: self.disable_bal_batch_io,
476 prewarm_results: prewarm_results.clone(),
477 };
478
479 let (prewarm_task, to_prewarm_task) =
480 PrewarmCacheTask::new(self.executor.clone(), self.execution_cache.clone(), prewarm_ctx);
481 {
482 let to_prewarm_task = to_prewarm_task.clone();
483 self.executor.spawn_blocking_named("prewarm", move || {
484 prewarm_task.run(mode, to_prewarm_task);
485 });
486 }
487
488 CacheTaskHandle {
489 saved_cache,
490 to_prewarm_task: Some(to_prewarm_task),
491 executed_tx_index,
492 prewarm_results,
493 cache_metrics: self.cache_metrics.clone(),
494 }
495 }
496
497 #[instrument(level = "debug", target = "engine::caching", skip(self))]
504 pub fn cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
505 match self.execution_cache.checkout(parent_hash) {
506 CacheCheckout::Available(cache) => {
507 debug!("reusing execution cache");
508 Some(cache)
509 }
510 CacheCheckout::InUse { cached_hash } => {
511 warn!(%cached_hash, "Execution cache still in use, executing without it");
512 None
513 }
514 CacheCheckout::Empty => {
515 debug!("creating new execution cache");
516 let start = Instant::now();
517 let cache = ExecutionCache::new(self.cross_block_cache_size);
518 if let Some(metrics) = &self.cache_metrics {
519 metrics.record_cache_creation(start.elapsed());
520 }
521 Some(SavedCache::new(parent_hash, cache))
522 }
523 }
524 }
525
526 pub fn on_inserted_executed_block(
534 &self,
535 block_with_parent: BlockWithParent,
536 bundle_state: &BundleState,
537 ) {
538 let cache_state_metrics = self.cache_state_metrics.clone();
539 self.execution_cache.update_with_guard(|cached| {
540 if cached.as_ref().is_some_and(|c| c.executed_block_hash() != block_with_parent.parent) {
541 debug!(
542 target: "engine::caching",
543 parent_hash = %block_with_parent.parent,
544 "Cannot find cache for parent hash, skip updating cache with new state for inserted executed block",
545 );
546 return
547 }
548
549 if let Some(cache) = cached.as_ref().filter(|cache| !cache.is_available()) {
550 debug!(
551 target: "engine::caching",
552 parent_hash = %block_with_parent.parent,
553 usage_count = cache.usage_count(),
554 "Execution cache is in use, skip updating cache with new state for inserted executed block",
555 );
556 return
557 }
558
559 let caches = match cached.take() {
561 Some(existing) => existing.cache().clone(),
562 None => ExecutionCache::new(self.cross_block_cache_size),
563 };
564
565 let new_cache = SavedCache::new(block_with_parent.block.hash, caches);
567 if new_cache.cache().insert_state(bundle_state).is_err() {
568 *cached = None;
569 debug!(target: "engine::caching", "cleared execution cache on update error");
570 return
571 }
572 new_cache.update_metrics(cache_state_metrics.as_ref());
573
574 *cached = Some(new_cache);
576 debug!(target: "engine::caching", ?block_with_parent, "Updated execution cache for inserted block");
577 });
578 }
579}
580
581fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
584 iter: impl Iterator<Item = RawTx>,
585 convert: &C,
586 prewarm_tx: Option<&mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>>,
587 execute_tx: &ExecuteTxSender<TxEnv, Recovered, Err>,
588) -> bool
589where
590 Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
591 TxEnv: Clone,
592 C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
593{
594 for (idx, raw_tx) in iter.enumerate() {
595 let _span =
596 trace_span!(target: "engine::tree::payload_processor", "convert_transaction", idx)
597 .entered();
598 let tx = convert.convert(raw_tx);
599 let failed = tx.is_err();
600 let tx = tx.map(WithTxEnv::new);
601 if let (Some(prewarm_tx), Ok(tx)) = (prewarm_tx, &tx) {
602 let _ = prewarm_tx.send((idx, tx.clone()));
603 }
604 if execute_tx.send((idx, tx)).is_err() || failed {
605 return false
606 }
607 trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
608 }
609 true
610}
611
612#[derive_where::derive_where(Debug; Tx, Err, R)]
618pub struct PayloadHandle<Tx, Err, R, H: evm2::EvmTypes> {
619 prewarm_handle: CacheTaskHandle<R, H>,
620 transactions: IndexedTxReceiver<Tx, Err>,
622 _span: Span,
624}
625
626impl<Tx, Err, R: Send + Sync + 'static, H: evm2::EvmTypes> PayloadHandle<Tx, Err, R, H> {
627 pub fn caches(&self) -> Option<ExecutionCache> {
629 self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
630 }
631
632 pub fn cache_metrics(&self) -> Option<CachedStateMetrics> {
634 self.prewarm_handle.cache_metrics.clone()
635 }
636
637 pub const fn executed_tx_index(&self) -> &Arc<AtomicUsize> {
642 &self.prewarm_handle.executed_tx_index
643 }
644
645 pub(crate) fn prewarm_results(&self) -> Option<Arc<PrewarmResults<H>>> {
647 self.prewarm_handle.prewarm_results.clone()
648 }
649
650 pub fn stop_prewarming_execution(&self) {
654 self.prewarm_handle.stop_prewarming_execution()
655 }
656
657 pub fn terminate_caching(
665 &mut self,
666 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
667 ) -> Option<mpsc::Sender<()>> {
668 self.prewarm_handle.terminate_caching(execution_outcome)
669 }
670
671 pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
673 self.transactions.iter().map(|(_, tx)| tx)
674 }
675
676 pub fn clone_transaction_receiver(&self) -> IndexedTxReceiver<Tx, Err> {
678 self.transactions.clone()
679 }
680}
681
682#[derive_where::derive_where(Debug; R)]
688pub struct CacheTaskHandle<R, H: evm2::EvmTypes> {
689 saved_cache: Option<SavedCache>,
691 to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
693 executed_tx_index: Arc<AtomicUsize>,
696 prewarm_results: Option<Arc<PrewarmResults<H>>>,
698 cache_metrics: Option<CachedStateMetrics>,
700}
701
702impl<R: Send + Sync + 'static, H: evm2::EvmTypes> CacheTaskHandle<R, H> {
703 pub fn stop_prewarming_execution(&self) {
707 self.to_prewarm_task
708 .as_ref()
709 .map(|tx| tx.send(PrewarmTaskEvent::TerminateTransactionExecution).ok());
710 }
711
712 #[must_use = "sender must be used and notified on block validation success"]
717 pub fn terminate_caching(
718 &mut self,
719 execution_outcome: Option<Arc<BlockExecutionOutput<R>>>,
720 ) -> Option<mpsc::Sender<()>> {
721 self.to_prewarm_task.take().map(|tx| {
722 let (valid_block_tx, valid_block_rx) = mpsc::channel();
723 let event = PrewarmTaskEvent::Terminate { execution_outcome, valid_block_rx };
724 let _ = tx.send(event);
725
726 valid_block_tx
727 })
728 }
729}
730
731impl<R, H: evm2::EvmTypes> Drop for CacheTaskHandle<R, H> {
732 fn drop(&mut self) {
733 if let Some(tx) = self.to_prewarm_task.take() {
735 let _ = tx.send(PrewarmTaskEvent::Terminate {
736 execution_outcome: None,
737 valid_block_rx: mpsc::channel().1,
738 });
739 }
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use super::*;
746 use crate::tree::{
747 payload_processor::PayloadProcessor, ExecutionCache, PayloadExecutionCache, SavedCache,
748 TreeConfig,
749 };
750 use alloy_consensus::constants::KECCAK_EMPTY;
751 use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};
752 use alloy_primitives::{Address, B256, U256};
753 use evm2::evm::AccountInfo;
754 use proptest::{
755 prelude::any,
756 test_runner::{Config, TestRunner},
757 };
758 use reth_chainspec::ChainSpec;
759 use reth_evm_ethereum::EthEvmConfig;
760 use reth_execution_cache::CachedStatus;
761 use reth_trie_common::bundle_state::BundleState;
762 use std::{
763 collections::HashSet,
764 process::Command,
765 sync::{atomic::Ordering, Arc, Mutex},
766 };
767 use tracing_subscriber::{
768 filter::LevelFilter, layer::SubscriberExt, registry::LookupSpan, Registry,
769 };
770
771 type TestTx = reth_evm::execute::WithTxEnv<
772 reth_evm::TxEnvFor<EthEvmConfig>,
773 reth_primitives_traits::Recovered<reth_ethereum_primitives::TransactionSigned>,
774 >;
775
776 fn converted_tx() -> TestTx {
777 TestTx::new(reth_primitives_traits::Recovered::new_unchecked(
778 reth_ethereum_primitives::TransactionSigned::Legacy(
779 alloy_consensus::Signed::new_unchecked(
780 alloy_consensus::TxLegacy::default(),
781 alloy_primitives::Signature::test_signature(),
782 B256::ZERO,
783 ),
784 ),
785 Address::ZERO,
786 ))
787 }
788
789 fn test_processor() -> PayloadProcessor<EthEvmConfig> {
790 PayloadProcessor::new(
791 reth_tasks::Runtime::test(),
792 EthEvmConfig::new(Arc::new(ChainSpec::default())),
793 &TreeConfig::default(),
794 PrecompileCacheMap::default(),
795 )
796 }
797
798 #[test]
799 fn transaction_conversion_preserves_results() {
800 for (count, bal) in [(10, false), (200, false), (200, true)] {
801 let processor = test_processor();
802 let (prewarm, receiver) = processor.spawn_tx_iterator(
803 ((0..count).collect::<Vec<_>>(), |_| Ok::<_, std::io::Error>(converted_tx())),
804 count,
805 bal,
806 true,
807 );
808 let mut indices = Vec::new();
809 for _ in 0..count {
810 let (idx, tx) = receiver.recv_timeout(std::time::Duration::from_secs(10)).unwrap();
811 assert!(tx.is_ok());
812 indices.push(idx);
813 }
814 if bal {
815 indices.sort_unstable();
816 }
817 assert_eq!(indices, (0..count).collect::<Vec<_>>());
818 assert_eq!(prewarm.unwrap().iter().count(), count);
819 }
820 }
821
822 #[test]
823 fn transaction_conversion_stops_on_error() {
824 for (count, bal, fail_at) in
825 [(10, false, 0), (10, true, 0), (200, false, 0), (200, false, 4), (200, false, 20)]
826 {
827 let processor = test_processor();
828 let (_, receiver) = processor.spawn_tx_iterator(
829 ((0..count).collect::<Vec<_>>(), move |idx| {
830 if idx >= fail_at {
831 Err(std::io::Error::other("invalid transaction"))
832 } else {
833 Ok(converted_tx())
834 }
835 }),
836 count,
837 bal,
838 false,
839 );
840 let mut results = Vec::new();
841 loop {
842 match receiver.recv_timeout(std::time::Duration::from_secs(10)) {
843 Ok(tx) => results.push(tx),
844 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
845 Err(err) => panic!("conversion did not terminate: {err}"),
846 }
847 }
848 assert!(results.iter().any(|(_, tx)| tx.is_err()));
849 assert!(results.len() < count);
850 if !bal {
851 assert_eq!(results.len(), fail_at + 1);
852 assert!(results.last().unwrap().1.is_err());
853 }
854 }
855 }
856
857 #[test]
858 fn bal_transaction_conversion_preserves_all_error_slots() {
859 let count = 200;
860 let processor = test_processor();
861 let (_, receiver) = processor.spawn_tx_iterator(
862 ((0..count).collect::<Vec<_>>(), |idx| {
863 if idx % 3 == 0 {
864 Err(std::io::Error::other("invalid transaction"))
865 } else {
866 Ok(converted_tx())
867 }
868 }),
869 count,
870 true,
871 false,
872 );
873 let mut indices = Vec::new();
874 for _ in 0..count {
875 let (idx, tx) = receiver.recv_timeout(std::time::Duration::from_secs(10)).unwrap();
876 assert_eq!(tx.is_err(), idx % 3 == 0);
877 indices.push(idx);
878 }
879 indices.sort_unstable();
880 assert_eq!(indices, (0..count).collect::<Vec<_>>());
881 }
882
883 #[test]
884 fn dropping_payload_handle_stops_transaction_conversion() {
885 for (count, bal, pause_at) in
886 [(10, false, 0), (1000, false, 0), (1000, false, 4), (1000, true, 0)]
887 {
888 let processor = test_processor();
889 let calls = Arc::new(super::AtomicUsize::new(0));
890 let converted = calls.clone();
891 let (started_tx, started_rx) = crossbeam_channel::unbounded();
892 let (release_tx, release_rx) = crossbeam_channel::bounded::<()>(0);
893 let (_, receiver) = processor.spawn_tx_iterator(
894 ((0..count).collect::<Vec<_>>(), move |idx| {
895 converted.fetch_add(1, Ordering::Relaxed);
896 if idx >= pause_at {
897 started_tx.send(()).unwrap();
898 let _ = release_rx.recv_timeout(std::time::Duration::from_secs(10));
899 }
900 Ok::<_, std::io::Error>(converted_tx())
901 }),
902 count,
903 bal,
904 false,
905 );
906 let handle = super::PayloadHandle {
907 prewarm_handle: super::CacheTaskHandle::<(), evm2::BaseEvmTypes> {
908 saved_cache: None,
909 to_prewarm_task: None,
910 executed_tx_index: Default::default(),
911 prewarm_results: None,
912 cache_metrics: None,
913 },
914 transactions: receiver,
915 _span: tracing::Span::none(),
916 };
917 started_rx.recv_timeout(std::time::Duration::from_secs(10)).unwrap();
918 drop(handle);
919 drop(release_tx);
920
921 loop {
923 match started_rx.recv_timeout(std::time::Duration::from_secs(10)) {
924 Ok(()) => {}
925 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
926 Err(err) => panic!("conversion did not terminate: {err}"),
927 }
928 }
929 assert!(calls.load(Ordering::Relaxed) < count);
930 }
931 }
932
933 fn make_saved_cache(hash: B256) -> SavedCache {
934 let execution_cache = ExecutionCache::new(1_000);
935 SavedCache::new(hash, execution_cache)
936 }
937
938 #[test]
939 fn execution_cache_allows_single_checkout() {
940 let execution_cache = PayloadExecutionCache::default();
941 let hash = B256::repeat_byte(1u8);
942
943 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
944
945 let first = execution_cache.get_cache_for(hash);
946 assert!(first.is_some(), "expected initial checkout to succeed");
947
948 let second = execution_cache.get_cache_for(hash);
949 assert!(second.is_none(), "second checkout should be blocked while guard is active");
950
951 drop(first);
952
953 let third = execution_cache.get_cache_for(hash);
954 assert!(third.is_some(), "third checkout should succeed after guard is dropped");
955 }
956
957 #[test]
958 fn execution_cache_checkout_releases_on_drop() {
959 let execution_cache = PayloadExecutionCache::default();
960 let hash = B256::repeat_byte(2u8);
961
962 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
963
964 {
965 let guard = execution_cache.get_cache_for(hash);
966 assert!(guard.is_some(), "expected checkout to succeed");
967 }
969
970 let retry = execution_cache.get_cache_for(hash);
971 assert!(retry.is_some(), "checkout should succeed after guard drop");
972 }
973
974 #[test]
975 fn execution_cache_mismatch_parent_clears_and_returns() {
976 let execution_cache = PayloadExecutionCache::default();
977 let hash = B256::repeat_byte(3u8);
978
979 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
980
981 let different_hash = B256::repeat_byte(4u8);
984 let cache = execution_cache.get_cache_for(different_hash);
985 assert!(cache.is_some(), "cache should be returned for reuse after clearing");
986
987 drop(cache);
988
989 let original = execution_cache.get_cache_for(hash);
992 assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
993 }
994
995 #[test]
996 fn execution_cache_update_after_release_succeeds() {
997 let execution_cache = PayloadExecutionCache::default();
998 let initial = B256::repeat_byte(5u8);
999
1000 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(initial)));
1001
1002 let guard =
1003 execution_cache.get_cache_for(initial).expect("expected initial checkout to succeed");
1004
1005 drop(guard);
1006
1007 let updated = B256::repeat_byte(6u8);
1008 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(updated)));
1009
1010 let new_checkout = execution_cache.get_cache_for(updated);
1011 assert!(new_checkout.is_some(), "new checkout should succeed after release and update");
1012 }
1013
1014 #[test]
1015 fn on_inserted_executed_block_populates_cache() {
1016 let payload_processor = PayloadProcessor::new(
1017 reth_tasks::Runtime::test(),
1018 EthEvmConfig::new(Arc::new(ChainSpec::default())),
1019 &TreeConfig::default(),
1020 PrecompileCacheMap::default(),
1021 );
1022
1023 let parent_hash = B256::repeat_byte(1u8);
1024 let block_hash = B256::repeat_byte(10u8);
1025 let block_with_parent = BlockWithParent {
1026 block: BlockNumHash { hash: block_hash, number: 1 },
1027 parent: parent_hash,
1028 };
1029 let bundle_state = BundleState::default();
1030
1031 assert!(payload_processor.execution_cache.get_cache_for(block_hash).is_none());
1033
1034 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
1036
1037 let cached = payload_processor.execution_cache.get_cache_for(block_hash);
1039 assert!(cached.is_some());
1040 assert_eq!(cached.unwrap().executed_block_hash(), block_hash);
1041 }
1042
1043 #[test]
1044 fn cache_for_executes_without_cache_while_stored_cache_is_in_use() {
1045 let payload_processor = PayloadProcessor::new(
1046 reth_tasks::Runtime::test(),
1047 EthEvmConfig::new(Arc::new(ChainSpec::default())),
1048 &TreeConfig::default(),
1049 PrecompileCacheMap::default(),
1050 );
1051 let hash = B256::repeat_byte(1u8);
1052 let next = B256::repeat_byte(2u8);
1053 assert!(payload_processor.cache_for(hash).is_some(), "empty slot creates a cache");
1054
1055 payload_processor
1056 .execution_cache
1057 .update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
1058 let held = payload_processor.cache_for(hash).expect("stored cache is available");
1059 assert!(payload_processor.cache_for(next).is_none(), "held cache is not replaced");
1060
1061 drop(held);
1062 assert_eq!(
1063 payload_processor.cache_for(next).map(|cache| cache.executed_block_hash()),
1064 Some(next)
1065 );
1066 }
1067
1068 #[test]
1069 fn on_inserted_executed_block_skips_on_parent_mismatch() {
1070 let payload_processor = PayloadProcessor::new(
1071 reth_tasks::Runtime::test(),
1072 EthEvmConfig::new(Arc::new(ChainSpec::default())),
1073 &TreeConfig::default(),
1074 PrecompileCacheMap::default(),
1075 );
1076
1077 let block1_hash = B256::repeat_byte(1u8);
1079 payload_processor
1080 .execution_cache
1081 .update_with_guard(|slot| *slot = Some(make_saved_cache(block1_hash)));
1082
1083 let wrong_parent = B256::repeat_byte(99u8);
1085 let block3_hash = B256::repeat_byte(3u8);
1086 let block_with_parent = BlockWithParent {
1087 block: BlockNumHash { hash: block3_hash, number: 3 },
1088 parent: wrong_parent,
1089 };
1090 let bundle_state = BundleState::default();
1091
1092 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
1093
1094 let cached = payload_processor.execution_cache.get_cache_for(block1_hash);
1096 assert!(cached.is_some(), "Original cache should be preserved");
1097
1098 let cached3 = payload_processor.execution_cache.get_cache_for(block3_hash);
1100 assert!(cached3.is_none(), "New block cache should not be created on mismatch");
1101 }
1102
1103 #[test]
1104 fn on_inserted_executed_block_does_not_mutate_checked_out_parent_cache() {
1105 let payload_processor = PayloadProcessor::new(
1106 reth_tasks::Runtime::test(),
1107 EthEvmConfig::new(Arc::new(ChainSpec::default())),
1108 &TreeConfig::default(),
1109 PrecompileCacheMap::default(),
1110 );
1111
1112 let parent_hash = B256::repeat_byte(1u8);
1113 payload_processor
1114 .execution_cache
1115 .update_with_guard(|slot| *slot = Some(make_saved_cache(parent_hash)));
1116
1117 let checked_out = payload_processor
1121 .execution_cache
1122 .get_cache_for(parent_hash)
1123 .expect("expected parent cache checkout to succeed");
1124
1125 let polluted_address = Address::random();
1126 let bundle_state = BundleState::builder(2..=2)
1127 .state_present_account_info(
1128 polluted_address,
1129 AccountInfo {
1130 balance: U256::from(1337),
1131 nonce: 7,
1132 code_hash: KECCAK_EMPTY,
1133 code: None,
1134 ..Default::default()
1135 },
1136 )
1137 .build();
1138
1139 let block_with_parent = BlockWithParent {
1142 block: BlockNumHash { hash: B256::repeat_byte(2u8), number: 2 },
1143 parent: parent_hash,
1144 };
1145
1146 payload_processor.on_inserted_executed_block(block_with_parent, &bundle_state);
1147
1148 let account = checked_out
1151 .cache()
1152 .get_or_try_insert_account_with(polluted_address, || Ok::<_, ()>(None))
1153 .expect("cache read should succeed");
1154
1155 assert_eq!(
1156 account,
1157 CachedStatus::NotCached(None),
1158 "checked-out parent cache should not observe state from inserted local block"
1159 );
1160 }
1161
1162 #[test]
1173 fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
1174 let execution_cache = PayloadExecutionCache::default();
1175
1176 let block4_hash = B256::repeat_byte(4u8);
1178 execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
1179
1180 let fork_parent = B256::repeat_byte(2u8);
1183 let prewarm_cache = execution_cache.get_cache_for(fork_parent);
1184 assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
1185 let prewarm_cache = prewarm_cache.unwrap();
1186 assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
1187
1188 let fork_addr = Address::repeat_byte(0xBB);
1191 let fork_key = B256::repeat_byte(0xCC);
1192 prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
1193
1194 let during_prewarm = execution_cache.get_cache_for(block4_hash);
1196 assert!(
1197 during_prewarm.is_none(),
1198 "cache must be unavailable while prewarm holds a reference"
1199 );
1200
1201 drop(prewarm_cache);
1203
1204 let block5_cache = execution_cache.get_cache_for(block4_hash);
1208 assert!(
1209 block5_cache.is_some(),
1210 "canonical chain must get cache after fork prewarm is dropped"
1211 );
1212 assert_eq!(
1213 block5_cache.as_ref().unwrap().executed_block_hash(),
1214 block4_hash,
1215 "cache must carry the canonical parent hash, not the fork parent"
1216 );
1217 }
1218
1219 #[test]
1220 fn transaction_conversion_has_child_spans() {
1221 const LEVEL_ENV: &str = "RETH_TEST_CONVERSION_SPAN_LEVEL";
1224 let Ok(level) = std::env::var(LEVEL_ENV) else {
1225 for level in ["trace", "debug", "info"] {
1226 let output = Command::new(std::env::current_exe().unwrap())
1227 .args([
1228 "--exact",
1229 "tree::payload_processor::tests::transaction_conversion_has_child_spans",
1230 "--nocapture",
1231 ])
1232 .env(LEVEL_ENV, level)
1233 .output()
1234 .unwrap();
1235 assert!(
1236 output.status.success(),
1237 "{level}: {}\n{}",
1238 String::from_utf8_lossy(&output.stdout),
1239 String::from_utf8_lossy(&output.stderr),
1240 );
1241 }
1242 return;
1243 };
1244 let level = level.parse::<LevelFilter>().unwrap();
1245 tracing::subscriber::set_global_default(tracing_subscriber::registry().with(level))
1246 .unwrap();
1247 let processor = test_processor();
1248
1249 for count in [0, 1, 29, 30, 68, 69] {
1252 for bal in [false, true] {
1253 assert_conversion_spans(&processor, count, bal, level);
1254 }
1255 }
1256 TestRunner::new(Config::with_cases(32))
1257 .run(&(0..200usize, any::<bool>()), |(count, bal)| {
1258 assert_conversion_spans(&processor, count, bal, level);
1259 Ok(())
1260 })
1261 .unwrap();
1262 }
1263
1264 fn assert_conversion_spans(
1265 processor: &PayloadProcessor<EthEvmConfig>,
1266 count: usize,
1267 bal: bool,
1268 level: LevelFilter,
1269 ) {
1270 let request = tracing::info_span!(parent: None, "request");
1271 let _entered = request.enter();
1272 let observed = Arc::new(Mutex::new(Vec::new()));
1274 let converted = observed.clone();
1275 let (_, receiver) = processor.spawn_tx_iterator(
1276 ((0..count).collect::<Vec<_>>(), move |idx| {
1277 converted.lock().unwrap().push((idx, Span::current()));
1278 Ok::<_, std::io::Error>(converted_tx())
1279 }),
1280 count,
1281 bal,
1282 false,
1283 );
1284 let mut indices = Vec::new();
1285 loop {
1286 match receiver.recv_timeout(std::time::Duration::from_secs(10)) {
1287 Ok((idx, tx)) => {
1288 assert!(tx.is_ok());
1289 indices.push(idx);
1290 }
1291 Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
1292 Err(err) => panic!("conversion did not terminate: {err}"),
1293 }
1294 }
1295 if bal {
1296 indices.sort_unstable();
1297 }
1298 assert_eq!(indices, (0..count).collect::<Vec<_>>());
1299 let observed = observed.lock().unwrap();
1300 assert_eq!(observed.len(), count);
1301 let mut transaction_ids = HashSet::new();
1302 for (idx, span) in observed.iter() {
1303 let scope = span
1304 .with_subscriber(|(id, dispatch)| {
1305 dispatch
1306 .downcast_ref::<Registry>()
1307 .unwrap()
1308 .span(id)
1309 .unwrap()
1310 .scope()
1311 .from_root()
1312 .map(|span| (span.id(), span.name()))
1313 .collect::<Vec<_>>()
1314 })
1315 .expect("conversion lost its request context");
1316 assert_eq!(Some(scope[0].0.clone()), request.id());
1317 let mut expected = vec!["request"];
1318 if level >= LevelFilter::DEBUG {
1319 expected.push("spawn_tx_iterator");
1320 expected.push("tx_iterator");
1321 if count >= 30 && (bal || *idx >= 4) {
1322 expected.push("convert_transactions");
1323 }
1324 }
1325 if level == LevelFilter::TRACE {
1326 expected.push("convert_transaction");
1327 assert!(transaction_ids.insert(span.id().unwrap()), "reused a transaction span");
1328 }
1329 assert_eq!(scope.iter().map(|(_, name)| *name).collect::<Vec<_>>(), expected);
1330 }
1331 }
1332}