reth_transaction_pool/validate/
task.rs1use crate::{
4 blobstore::BlobStore,
5 metrics::TxPoolValidatorMetrics,
6 validate::{EthTransactionValidatorBuilder, TransactionValidatorError},
7 EthTransactionValidator, PoolTransaction, TransactionOrigin, TransactionValidationOutcome,
8 TransactionValidator,
9};
10use futures_util::{lock::Mutex, StreamExt};
11use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
12use reth_evm::ConfigureEvm;
13use reth_primitives_traits::{HeaderTy, SealedBlock};
14use reth_storage_api::BlockReaderIdExt;
15use reth_tasks::Runtime;
16use std::{future::Future, pin::Pin, sync::Arc};
17use tokio::{
18 sync,
19 sync::{mpsc, oneshot},
20};
21use tokio_stream::wrappers::ReceiverStream;
22
23type ValidationFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
25
26type ValidationStream = ReceiverStream<ValidationFuture>;
28
29#[derive(Clone)]
35pub struct ValidationTask {
36 validation_jobs: Arc<Mutex<ValidationStream>>,
37}
38
39impl ValidationTask {
40 pub fn new() -> (ValidationJobSender, Self) {
44 Self::with_capacity(1)
45 }
46
47 pub fn with_capacity(capacity: usize) -> (ValidationJobSender, Self) {
49 let (tx, rx) = mpsc::channel(capacity);
50 let metrics = TxPoolValidatorMetrics::default();
51 (ValidationJobSender { tx, metrics }, Self::with_receiver(rx))
52 }
53
54 pub fn with_receiver(jobs: mpsc::Receiver<Pin<Box<dyn Future<Output = ()> + Send>>>) -> Self {
56 Self { validation_jobs: Arc::new(Mutex::new(ReceiverStream::new(jobs))) }
57 }
58
59 pub async fn run(self) {
63 loop {
64 let task = { self.validation_jobs.lock().await.next().await };
67 let Some(task) = task else { break };
68 task.await;
69 }
70 }
71}
72
73impl std::fmt::Debug for ValidationTask {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("ValidationTask").field("validation_jobs", &"...").finish()
76 }
77}
78
79#[derive(Debug)]
81pub struct ValidationJobSender {
82 tx: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>,
83 metrics: TxPoolValidatorMetrics,
84}
85
86impl ValidationJobSender {
87 pub async fn send(
89 &self,
90 job: Pin<Box<dyn Future<Output = ()> + Send>>,
91 ) -> Result<(), TransactionValidatorError> {
92 self.metrics.inflight_validation_jobs.increment(1);
93 let res = self
94 .tx
95 .send(job)
96 .await
97 .map_err(|_| TransactionValidatorError::ValidationServiceUnreachable);
98 self.metrics.inflight_validation_jobs.decrement(1);
99 res
100 }
101}
102
103#[derive(Debug)]
106pub struct TransactionValidationTaskExecutor<V> {
107 pub validator: Arc<V>,
109 pub to_validation_task: Arc<sync::Mutex<ValidationJobSender>>,
111}
112
113impl<V> Clone for TransactionValidationTaskExecutor<V> {
114 fn clone(&self) -> Self {
115 Self {
116 validator: self.validator.clone(),
117 to_validation_task: self.to_validation_task.clone(),
118 }
119 }
120}
121
122impl TransactionValidationTaskExecutor<()> {
125 pub fn eth_builder<Client, Evm>(
127 client: Client,
128 evm_config: Evm,
129 ) -> EthTransactionValidatorBuilder<Client, Evm>
130 where
131 Client: ChainSpecProvider<ChainSpec: EthereumHardforks>
132 + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>,
133 Evm: ConfigureEvm,
134 {
135 EthTransactionValidatorBuilder::new(client, evm_config)
136 }
137}
138
139impl<V> TransactionValidationTaskExecutor<V> {
140 pub fn map<F, T>(self, mut f: F) -> TransactionValidationTaskExecutor<T>
142 where
143 F: FnMut(V) -> T,
144 {
145 TransactionValidationTaskExecutor {
146 validator: Arc::new(f(Arc::into_inner(self.validator).unwrap())),
147 to_validation_task: self.to_validation_task,
148 }
149 }
150
151 pub fn validator(&self) -> &V {
153 &self.validator
154 }
155}
156
157impl<Client, Tx, Evm> TransactionValidationTaskExecutor<EthTransactionValidator<Client, Tx, Evm>> {
158 pub fn eth<S: BlobStore>(client: Client, evm_config: Evm, blob_store: S, tasks: Runtime) -> Self
163 where
164 Client: ChainSpecProvider<ChainSpec: EthereumHardforks>
165 + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>,
166 Evm: ConfigureEvm,
167 {
168 Self::eth_with_additional_tasks(client, evm_config, blob_store, tasks, 0)
169 }
170
171 pub fn eth_with_additional_tasks<S: BlobStore>(
181 client: Client,
182 evm_config: Evm,
183 blob_store: S,
184 tasks: Runtime,
185 num_additional_tasks: usize,
186 ) -> Self
187 where
188 Client: ChainSpecProvider<ChainSpec: EthereumHardforks>
189 + BlockReaderIdExt<Header = HeaderTy<Evm::Primitives>>,
190 Evm: ConfigureEvm,
191 {
192 EthTransactionValidatorBuilder::new(client, evm_config)
193 .with_additional_tasks(num_additional_tasks)
194 .build_with_tasks(tasks, blob_store)
195 }
196}
197
198impl<V> TransactionValidationTaskExecutor<V> {
199 pub fn new(validator: V) -> (Self, ValidationTask) {
204 let (tx, task) = ValidationTask::new();
205 (
206 Self {
207 validator: Arc::new(validator),
208 to_validation_task: Arc::new(sync::Mutex::new(tx)),
209 },
210 task,
211 )
212 }
213
214 pub fn spawn(validator: V, tasks: &Runtime, additional_tasks: usize) -> Self {
219 let (tx, task) = ValidationTask::new();
220
221 for _ in 0..additional_tasks {
222 let task = task.clone();
223 tasks.spawn_blocking_task(async move {
224 task.run().await;
225 });
226 }
227
228 tasks.spawn_critical_blocking_task("transaction-validation-service", async move {
229 task.run().await;
230 });
231
232 Self { validator: Arc::new(validator), to_validation_task: Arc::new(sync::Mutex::new(tx)) }
233 }
234}
235
236impl<V> TransactionValidator for TransactionValidationTaskExecutor<V>
237where
238 V: TransactionValidator + 'static,
239{
240 type Transaction = <V as TransactionValidator>::Transaction;
241 type Block = V::Block;
242
243 async fn validate_transaction(
244 &self,
245 origin: TransactionOrigin,
246 transaction: Self::Transaction,
247 ) -> TransactionValidationOutcome<Self::Transaction> {
248 let hash = *transaction.hash();
249 let (tx, rx) = oneshot::channel();
250 {
251 let res = {
252 let validator = self.validator.clone();
253 let fut = Box::pin(async move {
254 let res = validator.validate_transaction(origin, transaction).await;
255 let _ = tx.send(res);
256 });
257 self.to_validation_task.lock().await.send(fut).await
258 };
259 if res.is_err() {
260 return TransactionValidationOutcome::Error(
261 hash,
262 Box::new(TransactionValidatorError::ValidationServiceUnreachable),
263 );
264 }
265 }
266
267 match rx.await {
268 Ok(res) => res,
269 Err(_) => TransactionValidationOutcome::Error(
270 hash,
271 Box::new(TransactionValidatorError::ValidationServiceUnreachable),
272 ),
273 }
274 }
275
276 async fn validate_transactions(
277 &self,
278 transactions: impl IntoIterator<Item = (TransactionOrigin, Self::Transaction), IntoIter: Send>
279 + Send,
280 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
281 let transactions: Vec<_> = transactions.into_iter().collect();
282 let hashes: Vec<_> = transactions.iter().map(|(_, tx)| *tx.hash()).collect();
283 let (tx, rx) = oneshot::channel();
284 {
285 let res = {
286 let validator = self.validator.clone();
287 let fut = Box::pin(async move {
288 let res = validator.validate_transactions(transactions).await;
289 let _ = tx.send(res);
290 });
291 self.to_validation_task.lock().await.send(fut).await
292 };
293 if res.is_err() {
294 return validation_service_error_outcomes(hashes)
295 }
296 }
297 match rx.await {
298 Ok(res) => res,
299 Err(_) => validation_service_error_outcomes(hashes),
300 }
301 }
302
303 async fn validate_transactions_with_origin(
304 &self,
305 origin: TransactionOrigin,
306 transactions: impl IntoIterator<Item = Self::Transaction, IntoIter: Send> + Send,
307 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
308 let transactions: Vec<_> = transactions.into_iter().collect();
309 let hashes: Vec<_> = transactions.iter().map(|tx| *tx.hash()).collect();
310 let (tx, rx) = oneshot::channel();
311 let validator = self.validator.clone();
312 let fut = Box::pin(async move {
313 let res = validator.validate_transactions_with_origin(origin, transactions).await;
314 let _ = tx.send(res);
315 });
316
317 if self.to_validation_task.lock().await.send(fut).await.is_err() {
318 return validation_service_error_outcomes(hashes)
319 }
320
321 match rx.await {
322 Ok(res) => res,
323 Err(_) => validation_service_error_outcomes(hashes),
324 }
325 }
326
327 fn on_new_head_block(&self, new_tip_block: &SealedBlock<Self::Block>) {
328 self.validator.on_new_head_block(new_tip_block)
329 }
330}
331
332#[inline]
333fn validation_service_error_outcomes<T: PoolTransaction>(
334 hashes: Vec<alloy_primitives::TxHash>,
335) -> Vec<TransactionValidationOutcome<T>> {
336 hashes
337 .into_iter()
338 .map(|hash| {
339 TransactionValidationOutcome::Error(
340 hash,
341 Box::new(TransactionValidatorError::ValidationServiceUnreachable),
342 )
343 })
344 .collect()
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::{
351 test_utils::MockTransaction,
352 validate::{TransactionValidationOutcome, ValidTransaction},
353 TransactionOrigin,
354 };
355 use alloy_primitives::{Address, U256};
356
357 #[tokio::test]
358 async fn cloned_workers_validate_while_another_job_is_blocked() {
359 let (sender, task) = ValidationTask::new();
360 let first_worker = tokio::spawn(task.clone().run());
361 let second_worker = tokio::spawn(task.run());
362 let (started_tx, started_rx) = oneshot::channel();
363 let (release_tx, release_rx) = oneshot::channel();
364 sender
365 .send(Box::pin(async move {
366 started_tx.send(()).unwrap();
367 release_rx.await.unwrap();
368 }))
369 .await
370 .unwrap();
371 started_rx.await.unwrap();
372
373 let (completed_tx, completed_rx) = oneshot::channel();
376 sender
377 .send(Box::pin(async move {
378 completed_tx.send(()).unwrap();
379 }))
380 .await
381 .unwrap();
382 tokio::time::timeout(std::time::Duration::from_secs(1), completed_rx)
383 .await
384 .expect("second worker cannot dequeue while first validation holds receiver lock")
385 .unwrap();
386
387 release_tx.send(()).unwrap();
388 drop(sender);
389 tokio::time::timeout(std::time::Duration::from_secs(1), async {
391 first_worker.await.unwrap();
392 second_worker.await.unwrap();
393 })
394 .await
395 .unwrap();
396 }
397
398 #[derive(Debug)]
399 struct NoopValidator;
400
401 impl TransactionValidator for NoopValidator {
402 type Transaction = MockTransaction;
403 type Block = reth_ethereum_primitives::Block;
404
405 async fn validate_transaction(
406 &self,
407 _origin: TransactionOrigin,
408 transaction: Self::Transaction,
409 ) -> TransactionValidationOutcome<Self::Transaction> {
410 TransactionValidationOutcome::Valid {
411 balance: U256::ZERO,
412 state_nonce: 0,
413 bytecode_hash: None,
414 transaction: ValidTransaction::Valid(transaction),
415 propagate: false,
416 authorities: Some(Vec::<Address>::new()),
417 }
418 }
419 }
420
421 #[tokio::test]
422 async fn executor_new_spawns_and_validates_single() {
423 let validator = NoopValidator;
424 let (executor, task) = TransactionValidationTaskExecutor::new(validator);
425 tokio::spawn(task.run());
426 let tx = MockTransaction::legacy();
427 let out = executor.validate_transaction(TransactionOrigin::External, tx).await;
428 assert!(matches!(out, TransactionValidationOutcome::Valid { .. }));
429 }
430
431 #[tokio::test]
432 async fn executor_new_spawns_and_validates_batch() {
433 let validator = NoopValidator;
434 let (executor, task) = TransactionValidationTaskExecutor::new(validator);
435 tokio::spawn(task.run());
436 let txs = vec![
437 (TransactionOrigin::External, MockTransaction::legacy()),
438 (TransactionOrigin::Local, MockTransaction::legacy()),
439 ];
440 let out = executor.validate_transactions(txs).await;
441 assert_eq!(out.len(), 2);
442 assert!(out.iter().all(|o| matches!(o, TransactionValidationOutcome::Valid { .. })));
443 }
444
445 #[derive(Debug)]
446 struct SameOriginBatchValidator;
447
448 impl TransactionValidator for SameOriginBatchValidator {
449 type Transaction = MockTransaction;
450 type Block = reth_ethereum_primitives::Block;
451
452 async fn validate_transaction(
453 &self,
454 _origin: TransactionOrigin,
455 _transaction: Self::Transaction,
456 ) -> TransactionValidationOutcome<Self::Transaction> {
457 panic!("same-origin batches must use the batch validator")
458 }
459
460 async fn validate_transactions_with_origin(
461 &self,
462 origin: TransactionOrigin,
463 transactions: impl IntoIterator<Item = Self::Transaction, IntoIter: Send> + Send,
464 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
465 transactions
466 .into_iter()
467 .map(|transaction| TransactionValidationOutcome::Valid {
468 balance: U256::ZERO,
469 state_nonce: 0,
470 bytecode_hash: None,
471 transaction: ValidTransaction::Valid(transaction),
472 propagate: matches!(origin, TransactionOrigin::Local),
473 authorities: None,
474 })
475 .collect()
476 }
477 }
478
479 #[tokio::test]
480 async fn executor_forwards_same_origin_batches() {
481 let (executor, task) = TransactionValidationTaskExecutor::new(SameOriginBatchValidator);
482 tokio::spawn(task.run());
483
484 let transactions = vec![MockTransaction::legacy(), MockTransaction::eip1559()];
485 let expected_hashes = transactions.iter().map(|tx| *tx.hash()).collect::<Vec<_>>();
486 let outcomes = executor
487 .validate_transactions_with_origin(TransactionOrigin::Local, transactions)
488 .await;
489
490 assert_eq!(outcomes.len(), expected_hashes.len());
491 assert!(outcomes.into_iter().zip(expected_hashes).all(|(outcome, expected_hash)| {
492 matches!(
493 outcome,
494 TransactionValidationOutcome::Valid { transaction, propagate: true, .. }
495 if transaction.hash() == &expected_hash
496 )
497 }));
498 }
499}