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