Skip to main content

reth_transaction_pool/validate/
task.rs

1//! A validation service for transactions.
2
3use 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
23/// Represents a future outputting unit type and is sendable.
24type ValidationFuture = Pin<Box<dyn Future<Output = ()> + Send>>;
25
26/// Represents a stream of validation futures.
27type ValidationStream = ReceiverStream<ValidationFuture>;
28
29/// A service that performs validation jobs.
30///
31/// This listens for incoming validation jobs and executes them.
32///
33/// This should be spawned as a task: [`ValidationTask::run`]
34#[derive(Clone)]
35pub struct ValidationTask {
36    validation_jobs: Arc<Mutex<ValidationStream>>,
37}
38
39impl ValidationTask {
40    /// Creates a new cloneable task pair.
41    ///
42    /// The sender sends new (transaction) validation tasks to an available validation task.
43    pub fn new() -> (ValidationJobSender, Self) {
44        Self::with_capacity(1)
45    }
46
47    /// Creates a new cloneable task pair with the given channel capacity.
48    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    /// Creates a new task with the given receiver.
55    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    /// Executes all new validation jobs that come in.
60    ///
61    /// This will run as long as the channel is alive and is expected to be spawned as a task.
62    pub async fn run(self) {
63        loop {
64            // Release the shared receiver before running the job, so other workers
65            // can dequeue validations while this worker is busy.
66            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/// A sender new type for sending validation jobs to [`ValidationTask`].
80#[derive(Debug)]
81pub struct ValidationJobSender {
82    tx: mpsc::Sender<Pin<Box<dyn Future<Output = ()> + Send>>>,
83    metrics: TxPoolValidatorMetrics,
84}
85
86impl ValidationJobSender {
87    /// Sends the given job to the validation task.
88    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/// A [`TransactionValidator`] implementation that validates ethereum transaction.
104/// This validator is non-blocking, all validation work is done in a separate task.
105#[derive(Debug)]
106pub struct TransactionValidationTaskExecutor<V> {
107    /// The validator that will validate transactions on a separate task.
108    pub validator: Arc<V>,
109    /// The sender half to validation tasks that perform the actual validation.
110    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
122// === impl TransactionValidationTaskExecutor ===
123
124impl TransactionValidationTaskExecutor<()> {
125    /// Convenience method to create a [`EthTransactionValidatorBuilder`]
126    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    /// Maps the given validator to a new type.
141    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    /// Returns the validator.
152    pub fn validator(&self) -> &V {
153        &self.validator
154    }
155}
156
157impl<Client, Tx, Evm> TransactionValidationTaskExecutor<EthTransactionValidator<Client, Tx, Evm>> {
158    /// Creates a new instance for the given client
159    ///
160    /// This will spawn a single validation tasks that performs the actual validation.
161    /// See [`TransactionValidationTaskExecutor::eth_with_additional_tasks`]
162    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    /// Creates a new instance for the given client
172    ///
173    /// By default this will enable support for:
174    ///   - shanghai
175    ///   - eip1559
176    ///   - eip2930
177    ///
178    /// This will always spawn a validation task that performs the actual validation. It will spawn
179    /// `num_additional_tasks` additional tasks.
180    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    /// Creates a new executor instance with the given validator for transaction validation.
200    ///
201    /// Initializes the executor with the provided validator and sets up communication for
202    /// validation tasks.
203    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    /// Creates a new executor and spawns the validation tasks on the given runtime.
215    ///
216    /// This spawns `additional_tasks` extra blocking tasks plus one critical blocking task
217    /// for the validation service.
218    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        // The first validation remains blocked. A second configured worker must
374        // independently dequeue and finish this job without releasing the first.
375        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        // Closing the bounded channel still shuts every worker down cleanly.
390        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}