1use super::{
2 control::{Command, Publication},
3 Job, Source, Transactions,
4};
5use crate::tree::{StateProviderDatabase, TxPoolPrewarmCacheSnapshot as Snapshot};
6use alloy_evm::Evm;
7use alloy_primitives::B256;
8use crossbeam_channel::{Receiver, RecvTimeoutError, TryRecvError};
9use reth_evm::ConfigureEvm;
10use reth_primitives_traits::NodePrimitives;
11use reth_provider::{
12 BlockNumReader, DatabaseProviderFactory, PruneCheckpointReader, StageCheckpointReader,
13 StorageSettingsCache, TryIntoHistoricalStateProvider,
14};
15use reth_revm::{cached::CachedReads, db::State};
16use std::{
17 sync::Arc,
18 time::{Duration, Instant},
19};
20use tracing::{debug, trace};
21
22const REFRESH_INTERVAL: Duration = Duration::from_millis(100);
24
25const HEAD_POLL_INTERVAL: Duration = Duration::from_millis(10);
27
28pub(super) struct Worker<N, P, Evm>
39where
40 N: NodePrimitives,
41 Evm: ConfigureEvm<Primitives = N>,
42{
43 commands: Receiver<Command<Job<N, P, Evm>>>,
45 publication: Publication,
47 source: Arc<dyn Source<N>>,
49 evm_config: Evm,
51 job: Option<(B256, Job<N, P, Evm>)>,
53 pauses: u64,
55 cache: CachedReads,
57 cache_parent: Option<B256>,
59 published_entries: (usize, usize, usize),
62 transactions: Option<(B256, Transactions<N>)>,
64}
65
66impl<N, P, Evm> Worker<N, P, Evm>
67where
68 N: NodePrimitives,
69 P: DatabaseProviderFactory,
70 P::Provider: BlockNumReader
71 + PruneCheckpointReader
72 + StageCheckpointReader
73 + StorageSettingsCache
74 + TryIntoHistoricalStateProvider
75 + 'static,
76 Evm: ConfigureEvm<Primitives = N>,
77{
78 pub(super) fn new(
79 commands: Receiver<Command<Job<N, P, Evm>>>,
80 publication: Publication,
81 source: Arc<dyn Source<N>>,
82 evm_config: Evm,
83 ) -> Self {
84 Self {
85 commands,
86 publication,
87 source,
88 evm_config,
89 job: None,
90 pauses: 0,
91 cache: CachedReads::default(),
92 cache_parent: None,
93 published_entries: (0, 0, 0),
94 transactions: None,
95 }
96 }
97
98 pub(super) fn run(mut self) {
100 let _ = self.run_until_disconnected();
101 }
102
103 fn run_until_disconnected(&mut self) -> Result<(), ChannelDisconnected> {
104 loop {
105 let parent_hash = self.wait_until_runnable()?;
106
107 if !self.open_transactions(parent_hash) {
110 self.idle(HEAD_POLL_INTERVAL)?;
111 continue
112 }
113
114 if self.cache_parent != Some(parent_hash) {
115 self.cache = CachedReads::default();
116 self.cache_parent = Some(parent_hash);
117 self.published_entries = (0, 0, 0);
118 debug!(
119 target: "engine::tree::txpool_prewarm",
120 ?parent_hash,
121 "started txpool prewarming"
122 );
123 }
124
125 let batch = self.warm_one_batch();
126
127 if !self.commands.is_empty() {
130 continue
131 }
132 self.publish_snapshot_if_dirty();
133 if batch == BatchEnd::Rest {
134 self.idle(REFRESH_INTERVAL)?;
135 }
136 }
137 }
138
139 fn wait_until_runnable(&mut self) -> Result<B256, ChannelDisconnected> {
142 loop {
143 self.apply_pending_commands()?;
144
145 if self.pauses == 0 &&
146 let Some((parent_hash, _)) = self.job.as_ref()
147 {
148 return Ok(*parent_hash)
149 }
150
151 let command = self.commands.recv().map_err(|_| ChannelDisconnected)?;
152 self.apply(command);
153 }
154 }
155
156 fn open_transactions(&mut self, parent_hash: B256) -> bool {
159 if self.transactions.as_ref().is_none_or(|(parent, _)| *parent != parent_hash) {
160 self.transactions = None;
162 self.transactions = self
163 .source
164 .best_transactions(parent_hash)
165 .map(|transactions| (parent_hash, transactions));
166 }
167 self.transactions.is_some()
168 }
169
170 fn warm_one_batch(&mut self) -> BatchEnd {
177 let (_, job) = self.job.as_ref().expect("wait_until_runnable installed a job");
178 let (parent_hash, transactions) =
179 self.transactions.as_mut().expect("open_transactions installed an iterator");
180
181 if !self.commands.is_empty() {
184 return BatchEnd::GoAgain
185 }
186
187 let state_provider = match job.provider_builder.build() {
188 Ok(provider) => provider,
189 Err(err) => {
190 trace!(
191 target: "engine::tree::txpool_prewarm",
192 %err,
193 ?parent_hash,
194 "failed to build txpool prewarming state provider"
195 );
196 return BatchEnd::Rest
197 }
198 };
199 let mut state = State::builder()
200 .with_database(self.cache.as_db_mut(StateProviderDatabase::new(state_provider)))
201 .build();
202 let mut evm_env = job.evm_env.clone();
206 evm_env.cfg_env.disable_nonce_check = true;
207 evm_env.cfg_env.disable_balance_check = true;
208 evm_env.cfg_env.disable_base_fee = true;
209 let mut evm = self.evm_config.evm_with_env(&mut state, evm_env);
210
211 let deadline = Instant::now() + REFRESH_INTERVAL;
212 while self.commands.is_empty() && Instant::now() < deadline {
213 let Some(transaction) = transactions.next() else { return BatchEnd::Rest };
214 if let Err(err) = evm.transact(transaction.transaction) {
215 trace!(
216 target: "engine::tree::txpool_prewarm",
217 %err,
218 tx_hash = ?transaction.hash,
219 sender = %transaction.sender,
220 "speculative txpool transaction execution failed"
221 );
222 }
223 }
224 BatchEnd::GoAgain
225 }
226
227 fn publish_snapshot_if_dirty(&mut self) {
229 let entries = entry_counts(&self.cache);
230 if entries == self.published_entries {
231 return
232 }
233
234 let parent_hash = self.cache_parent.expect("reads only accumulate after a cache reset");
235 *self.publication.write() = Some(Snapshot::new(parent_hash, Arc::new(self.cache.clone())));
236 self.published_entries = entries;
237 let (accounts, storage, bytecodes) = entries;
238 debug!(
239 target: "engine::tree::txpool_prewarm",
240 ?parent_hash,
241 accounts,
242 storage,
243 bytecodes,
244 "published txpool prewarming snapshot"
245 );
246 }
247
248 fn idle(&mut self, timeout: Duration) -> Result<(), ChannelDisconnected> {
250 match self.commands.recv_timeout(timeout) {
251 Ok(command) => {
252 self.apply(command);
253 Ok(())
254 }
255 Err(RecvTimeoutError::Timeout) => Ok(()),
256 Err(RecvTimeoutError::Disconnected) => Err(ChannelDisconnected),
257 }
258 }
259
260 fn apply_pending_commands(&mut self) -> Result<(), ChannelDisconnected> {
262 loop {
263 match self.commands.try_recv() {
264 Ok(command) => self.apply(command),
265 Err(TryRecvError::Empty) => return Ok(()),
266 Err(TryRecvError::Disconnected) => return Err(ChannelDisconnected),
267 }
268 }
269 }
270
271 fn apply(&mut self, command: Command<Job<N, P, Evm>>) {
276 match command {
277 Command::Start { parent_hash, job } => self.job = Some((parent_hash, job)),
278 Command::Pause => {
279 self.pauses =
280 self.pauses.checked_add(1).expect("txpool prewarm pause count overflow");
281 }
282 Command::Resume => {
283 self.pauses = self
284 .pauses
285 .checked_sub(1)
286 .expect("txpool prewarm resumed without a matching pause");
287 }
288 }
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294enum BatchEnd {
295 GoAgain,
298 Rest,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305struct ChannelDisconnected;
306
307fn entry_counts(reads: &CachedReads) -> (usize, usize, usize) {
309 (
310 reads.accounts.len(),
311 reads.accounts.values().map(|account| account.storage.len()).sum(),
312 reads.contracts.len(),
313 )
314}
315
316#[cfg(test)]
317mod tests {
318 use super::{super::Transaction as PoolTransaction, *};
319 use crate::tree::StateProviderBuilder;
320 use alloy_consensus::{transaction::Recovered, Signed, TxLegacy};
321 use alloy_primitives::{Address, Signature, TxKind, U256};
322 use crossbeam_channel::{unbounded, Sender};
323 use parking_lot::{Mutex, RwLock};
324 use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
325 use reth_evm_ethereum::EthEvmConfig;
326 use reth_provider::test_utils::MockEthProvider;
327 use reth_stages_api::{StageCheckpoint, StageId};
328 use std::{
329 collections::{HashMap, VecDeque},
330 sync::atomic::{AtomicUsize, Ordering},
331 thread::{self, JoinHandle},
332 };
333
334 const WAIT_LIMIT: Duration = Duration::from_secs(5);
336 const POLL_INTERVAL: Duration = Duration::from_millis(2);
337
338 type TestJob = Job<EthPrimitives, MockEthProvider, EthEvmConfig>;
339
340 struct Harness {
343 commands: Sender<Command<TestJob>>,
344 publication: Publication,
345 pool: Arc<ScriptedPool>,
346 worker: Option<JoinHandle<()>>,
347 }
348
349 impl Harness {
350 fn spawn() -> Self {
351 let (commands, receiver) = unbounded();
352 let publication: Publication = Arc::new(RwLock::new(None));
353 let pool = Arc::new(ScriptedPool::default());
354 let worker = thread::spawn({
355 let publication = Arc::clone(&publication);
356 let source: Arc<dyn Source<EthPrimitives>> = pool.clone();
357 move || Worker::new(receiver, publication, source, EthEvmConfig::mainnet()).run()
358 });
359 Self { commands, publication, pool, worker: Some(worker) }
360 }
361
362 fn shutdown(mut self) {
364 let (disconnected, _) = unbounded();
365 self.commands = disconnected;
366 let worker = self.worker.take().expect("worker already joined");
367 wait_until("the worker thread exits", || worker.is_finished());
368 worker.join().unwrap();
369 }
370
371 fn start(&self, parent_hash: B256) {
373 let provider = MockEthProvider::default();
374 provider.enable_database_provider();
375 provider.add_header(parent_hash, Default::default());
376 provider.add_stage_checkpoint(StageId::Finish, StageCheckpoint::new(0));
377 let job = Job {
378 evm_env: Default::default(),
379 provider_builder: StateProviderBuilder::new(
380 provider,
381 parent_hash,
382 reth_storage_overlay::OverlayManager::default(),
383 ),
384 };
385 self.commands.send(Command::Start { parent_hash, job }).unwrap();
386 }
387
388 fn pause(&self) {
393 self.commands.send(Command::Pause).unwrap();
394 }
395
396 fn resume(&self) {
397 self.commands.send(Command::Resume).unwrap();
398 }
399
400 fn published(&self, accept: impl Fn(&Snapshot) -> bool) -> Snapshot {
402 let deadline = Instant::now() + WAIT_LIMIT;
403 loop {
404 let snapshot = self.publication.read().as_ref().cloned();
405 if let Some(snapshot) = snapshot &&
406 accept(&snapshot)
407 {
408 return snapshot
409 }
410 assert!(Instant::now() < deadline, "timed out waiting for a matching snapshot");
411 thread::sleep(POLL_INTERVAL);
412 }
413 }
414
415 fn published_for(&self, parent_hash: B256) -> Snapshot {
416 self.published(|snapshot| snapshot.parent_hash() == parent_hash)
417 }
418
419 fn published_entry_counts(&self) -> Option<(usize, usize, usize)> {
420 self.publication.read().as_ref().map(|snapshot| snapshot.entry_counts())
421 }
422 }
423
424 impl Drop for Harness {
425 fn drop(&mut self) {
426 let (disconnected, _) = unbounded();
428 self.commands = disconnected;
429 if let Some(worker) = self.worker.take() {
430 let _ = worker.join();
431 }
432 }
433 }
434
435 #[derive(Debug, Default)]
438 struct ScriptedPool {
439 queues: Arc<Mutex<HashMap<B256, VecDeque<PoolTransaction<EthPrimitives>>>>>,
440 opened: AtomicUsize,
442 not_ready: AtomicUsize,
444 }
445
446 impl ScriptedPool {
447 fn push(&self, parent_hash: B256, transaction: PoolTransaction<EthPrimitives>) {
448 self.queues.lock().entry(parent_hash).or_default().push_back(transaction);
449 }
450 }
451
452 impl Source<EthPrimitives> for ScriptedPool {
453 fn best_transactions(&self, parent_hash: B256) -> Option<Transactions<EthPrimitives>> {
454 if !self.queues.lock().contains_key(&parent_hash) {
455 self.not_ready.fetch_add(1, Ordering::Relaxed);
456 return None
457 }
458 self.opened.fetch_add(1, Ordering::Relaxed);
459 let queues = Arc::clone(&self.queues);
460 Some(Box::new(std::iter::from_fn(move || {
461 queues.lock().get_mut(&parent_hash)?.pop_front()
462 })))
463 }
464 }
465
466 fn transfer(recipient: u8) -> PoolTransaction<EthPrimitives> {
469 let transaction = TxLegacy {
470 gas_limit: 21_000,
471 to: TxKind::Call(Address::repeat_byte(recipient)),
472 value: U256::from(1),
473 ..Default::default()
474 };
475 let hash = B256::repeat_byte(recipient);
476 let signed = TransactionSigned::Legacy(Signed::new_unchecked(
477 transaction,
478 Signature::test_signature(),
479 hash,
480 ));
481 let sender = Address::repeat_byte(0xAA);
482 PoolTransaction { hash, sender, transaction: Recovered::new_unchecked(signed, sender) }
483 }
484
485 fn wait_until(what: &str, condition: impl Fn() -> bool) {
486 let deadline = Instant::now() + WAIT_LIMIT;
487 while !condition() {
488 assert!(Instant::now() < deadline, "timed out waiting until {what}");
489 thread::sleep(POLL_INTERVAL);
490 }
491 }
492
493 #[test]
494 fn warms_pool_transactions_into_a_published_snapshot() {
495 let harness = Harness::spawn();
496 let parent_hash = B256::repeat_byte(0x01);
497
498 harness.start(parent_hash);
500 wait_until("the untracked head is polled", || {
501 harness.pool.not_ready.load(Ordering::Relaxed) >= 1
502 });
503 harness.pool.push(parent_hash, transfer(0xB0));
504
505 let snapshot = harness.published_for(parent_hash);
506 let (accounts, _, _) = snapshot.entry_counts();
507 assert!(accounts >= 1, "speculative execution should cache account reads");
508 }
509
510 #[test]
511 fn pause_quiesces_the_worker_until_resume() {
512 let harness = Harness::spawn();
513 let parent_hash = B256::repeat_byte(0x01);
514 harness.start(parent_hash);
515 harness.pool.push(parent_hash, transfer(0xB0));
516 let before = harness.published_for(parent_hash).entry_counts();
517
518 harness.pause();
519 harness.pool.push(parent_hash, transfer(0xB1));
520 thread::sleep(REFRESH_INTERVAL * 2);
521 assert_eq!(
522 harness.published_entry_counts(),
523 Some(before),
524 "a paused worker must not publish"
525 );
526
527 harness.resume();
528 harness.published(|snapshot| snapshot.entry_counts() != before);
529 }
530
531 #[test]
532 fn overlapping_pauses_require_matching_resumes() {
533 let harness = Harness::spawn();
534 let parent_hash = B256::repeat_byte(0x01);
535 harness.start(parent_hash);
536 harness.pool.push(parent_hash, transfer(0xB0));
537 let before = harness.published_for(parent_hash).entry_counts();
538
539 harness.pause();
540 harness.pause();
541 harness.pool.push(parent_hash, transfer(0xB1));
542 harness.resume();
543 thread::sleep(REFRESH_INTERVAL * 2);
544 assert_eq!(
545 harness.published_entry_counts(),
546 Some(before),
547 "one resume must not release two pauses"
548 );
549
550 harness.resume();
551 harness.published(|snapshot| snapshot.entry_counts() != before);
552 }
553
554 #[test]
555 fn reuses_the_iterator_per_head_and_reopens_on_switch() {
556 let harness = Harness::spawn();
557 let first = B256::repeat_byte(0x01);
558 let second = B256::repeat_byte(0x02);
559
560 harness.start(first);
561 harness.pool.push(first, transfer(0xB0));
562 let before = harness.published_for(first).entry_counts();
563 harness.pool.push(first, transfer(0xB1));
564 harness.published(|snapshot| snapshot.entry_counts() != before);
565 assert_eq!(harness.pool.opened.load(Ordering::Relaxed), 1, "one iterator per head");
566
567 harness.start(second);
568 harness.pool.push(second, transfer(0xB2));
569 harness.published_for(second);
570 assert_eq!(harness.pool.opened.load(Ordering::Relaxed), 2);
571 }
572
573 #[test]
574 fn newest_start_wins() {
575 let harness = Harness::spawn();
576 let stale = B256::repeat_byte(0x01);
577 let newest = B256::repeat_byte(0x02);
578
579 harness.start(stale);
580 harness.start(newest);
581 harness.pool.push(newest, transfer(0xB0));
582
583 harness.published_for(newest);
584 }
585
586 #[test]
587 fn shuts_down_when_control_is_dropped() {
588 let harness = Harness::spawn();
589 let parent_hash = B256::repeat_byte(0x01);
590 harness.start(parent_hash);
591 harness.pool.push(parent_hash, transfer(0xB0));
592 harness.published_for(parent_hash);
593
594 harness.shutdown();
595 }
596}