Skip to main content

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