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