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