reth_exex_test_utils/
lib.rs

1//! Test helpers for `reth-exex`
2
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
5    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
6    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
7)]
8#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
9#![cfg_attr(not(test), warn(unused_crate_dependencies))]
10
11use std::{
12    fmt::Debug,
13    future::{poll_fn, Future},
14    sync::Arc,
15    task::Poll,
16};
17
18use alloy_eips::BlockNumHash;
19use futures_util::FutureExt;
20use reth_chainspec::{ChainSpec, MAINNET};
21use reth_consensus::test_utils::TestConsensus;
22use reth_db::{
23    test_utils::{create_test_rw_db, create_test_static_files_dir, TempDatabase},
24    DatabaseEnv,
25};
26use reth_db_common::init::init_genesis;
27use reth_ethereum_primitives::{EthPrimitives, TransactionSigned};
28use reth_evm_ethereum::MockEvmConfig;
29use reth_execution_types::Chain;
30use reth_exex::{ExExContext, ExExEvent, ExExNotification, ExExNotifications, Wal};
31use reth_network::{config::rng_secret_key, NetworkConfigBuilder, NetworkManager};
32use reth_node_api::{
33    FullNodeTypes, FullNodeTypesAdapter, NodePrimitives, NodeTypes, NodeTypesWithDBAdapter,
34};
35use reth_node_builder::{
36    components::{
37        BasicPayloadServiceBuilder, Components, ComponentsBuilder, ConsensusBuilder,
38        ExecutorBuilder, PoolBuilder,
39    },
40    BuilderContext, Node, NodeAdapter, RethFullAdapter,
41};
42use reth_node_core::node_config::NodeConfig;
43use reth_node_ethereum::{
44    node::{
45        EthereumAddOns, EthereumEngineValidatorBuilder, EthereumEthApiBuilder,
46        EthereumNetworkBuilder, EthereumPayloadBuilder,
47    },
48    EthEngineTypes,
49};
50use reth_payload_builder::noop::NoopPayloadBuilderService;
51use reth_primitives_traits::{Block as _, RecoveredBlock};
52use reth_provider::{
53    providers::{BlockchainProvider, StaticFileProvider},
54    BlockReader, EthStorage, ProviderFactory,
55};
56use reth_tasks::TaskManager;
57use reth_transaction_pool::test_utils::{testing_pool, TestPool};
58use tempfile::TempDir;
59use thiserror::Error;
60use tokio::sync::mpsc::{Sender, UnboundedReceiver};
61
62/// A test [`PoolBuilder`] that builds a [`TestPool`].
63#[derive(Debug, Default, Clone, Copy)]
64#[non_exhaustive]
65pub struct TestPoolBuilder;
66
67impl<Node> PoolBuilder<Node> for TestPoolBuilder
68where
69    Node: FullNodeTypes<Types: NodeTypes<Primitives: NodePrimitives<SignedTx = TransactionSigned>>>,
70{
71    type Pool = TestPool;
72
73    async fn build_pool(self, _ctx: &BuilderContext<Node>) -> eyre::Result<Self::Pool> {
74        Ok(testing_pool())
75    }
76}
77
78/// A test [`ExecutorBuilder`] that builds a [`MockEvmConfig`] for testing.
79#[derive(Debug, Default, Clone, Copy)]
80#[non_exhaustive]
81pub struct TestExecutorBuilder;
82
83impl<Node> ExecutorBuilder<Node> for TestExecutorBuilder
84where
85    Node: FullNodeTypes<Types: NodeTypes<ChainSpec = ChainSpec, Primitives = EthPrimitives>>,
86{
87    type EVM = MockEvmConfig;
88
89    async fn build_evm(self, _ctx: &BuilderContext<Node>) -> eyre::Result<Self::EVM> {
90        let evm_config = MockEvmConfig::default();
91        Ok(evm_config)
92    }
93}
94
95/// A test [`ConsensusBuilder`] that builds a [`TestConsensus`].
96#[derive(Debug, Default, Clone, Copy)]
97#[non_exhaustive]
98pub struct TestConsensusBuilder;
99
100impl<Node> ConsensusBuilder<Node> for TestConsensusBuilder
101where
102    Node: FullNodeTypes,
103{
104    type Consensus = Arc<TestConsensus>;
105
106    async fn build_consensus(self, _ctx: &BuilderContext<Node>) -> eyre::Result<Self::Consensus> {
107        Ok(Arc::new(TestConsensus::default()))
108    }
109}
110
111/// A test [`Node`].
112#[derive(Debug, Default, Clone, Copy)]
113#[non_exhaustive]
114pub struct TestNode;
115
116impl NodeTypes for TestNode {
117    type Primitives = EthPrimitives;
118    type ChainSpec = ChainSpec;
119    type Storage = EthStorage;
120    type Payload = EthEngineTypes;
121}
122
123impl<N> Node<N> for TestNode
124where
125    N: FullNodeTypes<Types = Self>,
126{
127    type ComponentsBuilder = ComponentsBuilder<
128        N,
129        TestPoolBuilder,
130        BasicPayloadServiceBuilder<EthereumPayloadBuilder>,
131        EthereumNetworkBuilder,
132        TestExecutorBuilder,
133        TestConsensusBuilder,
134    >;
135    type AddOns =
136        EthereumAddOns<NodeAdapter<N>, EthereumEthApiBuilder, EthereumEngineValidatorBuilder>;
137
138    fn components_builder(&self) -> Self::ComponentsBuilder {
139        ComponentsBuilder::default()
140            .node_types::<N>()
141            .pool(TestPoolBuilder::default())
142            .executor(TestExecutorBuilder::default())
143            .payload(BasicPayloadServiceBuilder::default())
144            .network(EthereumNetworkBuilder::default())
145            .consensus(TestConsensusBuilder::default())
146    }
147
148    fn add_ons(&self) -> Self::AddOns {
149        EthereumAddOns::default()
150    }
151}
152
153/// A shared [`TempDatabase`] used for testing
154pub type TmpDB = Arc<TempDatabase<DatabaseEnv>>;
155/// The [`NodeAdapter`] for the [`TestExExContext`]. Contains type necessary to
156/// boot the testing environment
157pub type Adapter = NodeAdapter<RethFullAdapter<TmpDB, TestNode>>;
158/// An [`ExExContext`] using the [`Adapter`] type.
159pub type TestExExContext = ExExContext<Adapter>;
160
161/// A helper type for testing Execution Extensions.
162#[derive(Debug)]
163pub struct TestExExHandle {
164    /// Genesis block that was inserted into the storage
165    pub genesis: RecoveredBlock<reth_ethereum_primitives::Block>,
166    /// Provider Factory for accessing the emphemeral storage of the host node
167    pub provider_factory: ProviderFactory<NodeTypesWithDBAdapter<TestNode, TmpDB>>,
168    /// Channel for receiving events from the Execution Extension
169    pub events_rx: UnboundedReceiver<ExExEvent>,
170    /// Channel for sending notifications to the Execution Extension
171    pub notifications_tx: Sender<ExExNotification>,
172    /// Node task manager
173    pub tasks: TaskManager,
174    /// WAL temp directory handle
175    _wal_directory: TempDir,
176}
177
178impl TestExExHandle {
179    /// Send a notification to the Execution Extension that the chain has been committed
180    pub async fn send_notification_chain_committed(&self, chain: Chain) -> eyre::Result<()> {
181        self.notifications_tx
182            .send(ExExNotification::ChainCommitted { new: Arc::new(chain) })
183            .await?;
184        Ok(())
185    }
186
187    /// Send a notification to the Execution Extension that the chain has been reorged
188    pub async fn send_notification_chain_reorged(
189        &self,
190        old: Chain,
191        new: Chain,
192    ) -> eyre::Result<()> {
193        self.notifications_tx
194            .send(ExExNotification::ChainReorged { old: Arc::new(old), new: Arc::new(new) })
195            .await?;
196        Ok(())
197    }
198
199    /// Send a notification to the Execution Extension that the chain has been reverted
200    pub async fn send_notification_chain_reverted(&self, chain: Chain) -> eyre::Result<()> {
201        self.notifications_tx
202            .send(ExExNotification::ChainReverted { old: Arc::new(chain) })
203            .await?;
204        Ok(())
205    }
206
207    /// Asserts that the Execution Extension did not emit any events.
208    #[track_caller]
209    pub fn assert_events_empty(&self) {
210        assert!(self.events_rx.is_empty());
211    }
212
213    /// Asserts that the Execution Extension emitted a `FinishedHeight` event with the correct
214    /// height.
215    #[track_caller]
216    pub fn assert_event_finished_height(&mut self, height: BlockNumHash) -> eyre::Result<()> {
217        let event = self.events_rx.try_recv()?;
218        assert_eq!(event, ExExEvent::FinishedHeight(height));
219        Ok(())
220    }
221}
222
223/// Creates a new [`ExExContext`].
224///
225/// This is a convenience function that does the following:
226/// 1. Sets up an [`ExExContext`] with all dependencies.
227/// 2. Inserts the genesis block of the provided (chain spec)[`ChainSpec`] into the storage.
228/// 3. Creates a channel for receiving events from the Execution Extension.
229/// 4. Creates a channel for sending notifications to the Execution Extension.
230///
231/// # Warning
232/// The genesis block is not sent to the notifications channel. The caller is responsible for
233/// doing this.
234pub async fn test_exex_context_with_chain_spec(
235    chain_spec: Arc<ChainSpec>,
236) -> eyre::Result<(ExExContext<Adapter>, TestExExHandle)> {
237    let transaction_pool = testing_pool();
238    let evm_config = MockEvmConfig::default();
239    let consensus = Arc::new(TestConsensus::default());
240
241    let (static_dir, _) = create_test_static_files_dir();
242    let db = create_test_rw_db();
243    let provider_factory = ProviderFactory::<NodeTypesWithDBAdapter<TestNode, _>>::new(
244        db,
245        chain_spec.clone(),
246        StaticFileProvider::read_write(static_dir.keep()).expect("static file provider"),
247    );
248
249    let genesis_hash = init_genesis(&provider_factory)?;
250    let provider = BlockchainProvider::new(provider_factory.clone())?;
251
252    let network_manager = NetworkManager::new(
253        NetworkConfigBuilder::new(rng_secret_key())
254            .with_unused_discovery_port()
255            .with_unused_listener_port()
256            .build(provider_factory.clone()),
257    )
258    .await?;
259    let network = network_manager.handle().clone();
260    let tasks = TaskManager::current();
261    let task_executor = tasks.executor();
262    tasks.executor().spawn(network_manager);
263
264    let (_, payload_builder_handle) = NoopPayloadBuilderService::<EthEngineTypes>::new();
265
266    let components = NodeAdapter::<FullNodeTypesAdapter<_, _, _>, _> {
267        components: Components {
268            transaction_pool,
269            evm_config,
270            consensus,
271            network,
272            payload_builder_handle,
273        },
274        task_executor,
275        provider,
276    };
277
278    let genesis = provider_factory
279        .block_by_hash(genesis_hash)?
280        .ok_or_else(|| eyre::eyre!("genesis block not found"))?
281        .seal_slow()
282        .try_recover()?;
283
284    let head = genesis.num_hash();
285
286    let wal_directory = tempfile::tempdir()?;
287    let wal = Wal::new(wal_directory.path())?;
288
289    let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel();
290    let (notifications_tx, notifications_rx) = tokio::sync::mpsc::channel(1);
291    let notifications = ExExNotifications::new(
292        head,
293        components.provider.clone(),
294        components.components.evm_config.clone(),
295        notifications_rx,
296        wal.handle(),
297    );
298
299    let ctx = ExExContext {
300        head,
301        config: NodeConfig::test(),
302        reth_config: reth_config::Config::default(),
303        events: events_tx,
304        notifications,
305        components,
306    };
307
308    Ok((
309        ctx,
310        TestExExHandle {
311            genesis,
312            provider_factory,
313            events_rx,
314            notifications_tx,
315            tasks,
316            _wal_directory: wal_directory,
317        },
318    ))
319}
320
321/// Creates a new [`ExExContext`] with (mainnet)[`MAINNET`] chain spec.
322///
323/// For more information see [`test_exex_context_with_chain_spec`].
324pub async fn test_exex_context() -> eyre::Result<(ExExContext<Adapter>, TestExExHandle)> {
325    test_exex_context_with_chain_spec(MAINNET.clone()).await
326}
327
328/// An extension trait for polling an Execution Extension future.
329pub trait PollOnce {
330    /// Polls the given Execution Extension future __once__. The future should be
331    /// (pinned)[`std::pin::pin`].
332    ///
333    /// # Returns
334    /// - `Ok(())` if the future returned [`Poll::Pending`]. The future can be polled again.
335    /// - `Err(PollOnceError::FutureIsReady)` if the future returned [`Poll::Ready`] without an
336    ///   error. The future should never resolve.
337    /// - `Err(PollOnceError::FutureError(err))` if the future returned [`Poll::Ready`] with an
338    ///   error. Something went wrong.
339    fn poll_once(&mut self) -> impl Future<Output = Result<(), PollOnceError>> + Send;
340}
341
342/// An Execution Extension future polling error.
343#[derive(Error, Debug)]
344pub enum PollOnceError {
345    /// The future returned [`Poll::Ready`] without an error, but it should never resolve.
346    #[error("Execution Extension future returned Ready, but it should never resolve")]
347    FutureIsReady,
348    /// The future returned [`Poll::Ready`] with an error.
349    #[error(transparent)]
350    FutureError(#[from] eyre::Error),
351}
352
353impl<F: Future<Output = eyre::Result<()>> + Unpin + Send> PollOnce for F {
354    async fn poll_once(&mut self) -> Result<(), PollOnceError> {
355        poll_fn(|cx| match self.poll_unpin(cx) {
356            Poll::Ready(Ok(())) => Poll::Ready(Err(PollOnceError::FutureIsReady)),
357            Poll::Ready(Err(err)) => Poll::Ready(Err(PollOnceError::FutureError(err))),
358            Poll::Pending => Poll::Ready(Ok(())),
359        })
360        .await
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[tokio::test]
369    async fn check_test_context_creation() {
370        let _ = test_exex_context().await.unwrap();
371    }
372}