Skip to main content

reth_e2e_test_utils/
setup_builder.rs

1//! Builder for configuring and launching test node setups.
2//!
3//! This module provides a flexible builder API for setting up test nodes with custom
4//! configurations through closures that modify `NodeConfig` and `TreeConfig`.
5
6use crate::{
7    eth_payload_attributes,
8    node::{NodeRestart, NodeTestContext, Relaunch},
9    test_chain_spec,
10    wallet::Wallet,
11    Adapter, NodeBuilderHelper, NodeHelperType, TmpDB, TmpNodeAdapter,
12};
13use alloy_primitives::B256;
14use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes};
15use alloy_rpc_types_eth::BlockNumberOrTag;
16use eyre::{ensure, eyre, WrapErr};
17use futures_util::future::{BoxFuture, TryJoinAll};
18use reth_chainspec::{ChainSpec, EthChainSpec, EthereumHardfork};
19use reth_db::{init_db, mdbx::DatabaseArguments, test_utils::TempDatabase};
20use reth_node_api::{PayloadAttrTy, TreeConfig};
21use reth_node_builder::{
22    sync::{BackfillClientFor, BackfillSyncBuilder, PipelineBackfill},
23    ComponentsFor, DebugNode, DebugNodeLauncher, EngineNodeLauncher, Node, NodeBuilder,
24    NodeBuilderWithComponents, NodeConfig, NodeHandle, NodeTypesWithDBAdapter,
25};
26use reth_node_core::{
27    args::{DatadirArgs, DiscoveryArgs, NetworkArgs, PruningArgs, RpcServerArgs, StorageArgs},
28    dirs::{ChainPath, DataDirPath, MaybePlatformPath},
29};
30use reth_primitives_traits::AlloyBlockHeader;
31use reth_provider::{
32    providers::BlockchainProvider, BlockReaderIdExt, DatabaseProviderFactory, MetadataProvider,
33};
34use reth_rpc_server_types::RpcModuleSelection;
35use reth_tasks::Runtime;
36use std::{
37    path::{Path, PathBuf},
38    sync::{Arc, Mutex},
39    time::Duration,
40};
41use tracing::{span, Instrument, Level};
42
43/// Builder for configuring and launching test node setups.
44///
45/// By default, the nodes:
46/// - are [`Default`] instances of `N`, see [`Self::with_node`],
47/// - run on a shared [`Runtime::test`] runtime, unless they are
48///   [restartable](Self::with_restartable_nodes),
49/// - build payloads with [`eth_payload_attributes`] for the hardforks active in the chain spec,
50///   unless created with [`Self::new_with_attributes_generator`],
51/// - have discovery disabled, use unused ports and serve all RPC modules except `testing` over
52///   HTTP,
53/// - report an idle sync state from startup, so they gossip transactions before their first block,
54/// - do not persist their peers, so a restarted node only connects to the peers a test connects it
55///   to,
56/// - are connected to each other.
57///
58/// Once launched, each node receives a forkchoice update that makes genesis the head, safe and
59/// finalized block, unless [dev mining](Self::with_dev_mining) is enabled.
60///
61/// Configuration and tree configuration modifiers are applied in the order they are added. Like
62/// for a node launched from the command line, the tree configuration of a node is derived from its
63/// final node configuration, see [`NodeConfig::tree_config`], so the engine arguments and
64/// `--debug.skip-state-root` set by node configuration modifiers take effect. Tree configuration
65/// modifiers are applied last and take precedence over them.
66///
67/// Use [`E2ETestSetupExt::test_setup_for`] to set up nodes on the [`test_chain_spec`] at a
68/// hardfork, or [`E2ETestSetupExt::test_setup`] for any other chain spec:
69///
70/// ```ignore
71/// let (mut node, wallet) = EthereumNode::test_setup_for(EthereumHardfork::Cancun)
72///     .with_tree_config_modifier(|config| config.with_persistence_threshold(0))
73///     .build_single()
74///     .await?;
75/// ```
76pub struct E2ETestSetupBuilder<N: NodeBuilderHelper> {
77    num_nodes: usize,
78    chain_spec: Arc<N::ChainSpec>,
79    runtime: Option<Runtime>,
80    attributes_generator: AttributesGenerator<N>,
81    connect_nodes: bool,
82    tree_config_modifiers: Vec<TreeConfigModifier>,
83    node_config_modifiers: Vec<NodeConfigModifier<N::ChainSpec>>,
84    storage_v2: bool,
85    dev_launcher: Option<NodeLauncher<N>>,
86    backfill_launcher: Option<NodeLauncher<N>>,
87    dev_payload_attributes: Option<PayloadAttributesMapper<N>>,
88    node_factory: NodeFactory<N>,
89    node_builder_modifiers: Vec<NodeBuilderModifier<N>>,
90    restartable: bool,
91}
92
93impl<N: NodeBuilderHelper> E2ETestSetupBuilder<N> {
94    /// Creates a new builder for `num_nodes` nodes of the given chain.
95    ///
96    /// The nodes build payloads with [`eth_payload_attributes`] for the chain spec, which requires
97    /// the payload attributes of the node to be convertible from Ethereum's. Use
98    /// [`Self::new_with_attributes_generator`] for other nodes.
99    pub fn new(num_nodes: usize, chain_spec: Arc<N::ChainSpec>) -> Self
100    where
101        PayloadAttrTy<N>: From<PayloadAttributes>,
102    {
103        let attributes_chain_spec = chain_spec.clone();
104        Self::new_with_attributes_generator(num_nodes, chain_spec, move |timestamp| {
105            eth_payload_attributes(&attributes_chain_spec, timestamp).into()
106        })
107    }
108
109    /// Creates a new builder for `num_nodes` nodes of the given chain, whose payloads are built
110    /// with the attributes returned by `attributes_generator`, see
111    /// [`Self::with_attributes_generator`].
112    ///
113    /// Unlike [`Self::new`], this does not require the payload attributes of the node to be
114    /// convertible from Ethereum's.
115    pub fn new_with_attributes_generator<G>(
116        num_nodes: usize,
117        chain_spec: Arc<N::ChainSpec>,
118        attributes_generator: G,
119    ) -> Self
120    where
121        G: Fn(u64) -> PayloadAttrTy<N> + Send + Sync + 'static,
122    {
123        Self {
124            num_nodes,
125            chain_spec,
126            runtime: None,
127            attributes_generator: Arc::new(attributes_generator),
128            connect_nodes: true,
129            tree_config_modifiers: Vec::new(),
130            node_config_modifiers: Vec::new(),
131            storage_v2: StorageArgs::default().v2,
132            dev_launcher: None,
133            backfill_launcher: None,
134            dev_payload_attributes: None,
135            node_factory: Arc::new(|_| N::default()),
136            node_builder_modifiers: Vec::new(),
137            restartable: false,
138        }
139    }
140
141    /// Sets the number of nodes to launch.
142    pub const fn with_num_nodes(mut self, num_nodes: usize) -> Self {
143        self.num_nodes = num_nodes;
144        self
145    }
146
147    /// Launches the nodes on the given runtime instead of a new [`Runtime::test`].
148    ///
149    /// This lets multiple setups, or other components of a test, share the same tokio handle and
150    /// rayon pools. Note that the tasks of the nodes are only shut down once all handles to the
151    /// runtime are dropped, not when the nodes are dropped. Restartable nodes, see
152    /// [`Self::with_restartable_nodes`], can not share a runtime.
153    pub fn with_runtime(mut self, runtime: Runtime) -> Self {
154        self.runtime = Some(runtime);
155        self
156    }
157
158    /// Sets the generator for the payload attributes of the payloads built by the test nodes.
159    ///
160    /// The generator is called with the timestamp of the next payload. It replaces the generator
161    /// of the constructor, which is [`eth_payload_attributes`] for the chain spec of the setup for
162    /// [`Self::new`].
163    pub fn with_attributes_generator<G>(mut self, generator: G) -> Self
164    where
165        G: Fn(u64) -> PayloadAttrTy<N> + Send + Sync + 'static,
166    {
167        self.attributes_generator = Arc::new(generator);
168        self
169    }
170
171    /// Sets whether nodes should be interconnected (default: true).
172    pub const fn with_connect_nodes(mut self, connect_nodes: bool) -> Self {
173        self.connect_nodes = connect_nodes;
174        self
175    }
176
177    /// Adds a modifier for the tree configuration.
178    ///
179    /// The closure receives the current tree config and returns a modified version. The base
180    /// config is derived from the node configuration after all node configuration modifiers ran,
181    /// see [`NodeConfig::tree_config`]. Unless a node configuration modifier changes
182    /// `engine.cross_block_cache_size`, it uses a small cross block cache of 1 MiB.
183    pub fn with_tree_config_modifier<G>(mut self, modifier: G) -> Self
184    where
185        G: Fn(TreeConfig) -> TreeConfig + Send + Sync + 'static,
186    {
187        self.tree_config_modifiers.push(Box::new(modifier));
188        self
189    }
190
191    /// Adds a modifier for the node configuration.
192    ///
193    /// The closure receives the current node config and returns a modified version. Its engine
194    /// arguments and `debug.skip_state_root` configure the engine tree, unless a
195    /// [tree configuration modifier](Self::with_tree_config_modifier) overrides them.
196    pub fn with_node_config_modifier<G>(mut self, modifier: G) -> Self
197    where
198        G: Fn(NodeConfig<N::ChainSpec>) -> NodeConfig<N::ChainSpec> + Send + Sync + 'static,
199    {
200        self.node_config_modifiers.push(Box::new(modifier));
201        self
202    }
203
204    /// Adds a modifier for the RPC server arguments.
205    ///
206    /// The closure receives the current arguments, which serve all modules except `testing` over
207    /// HTTP on an unused port.
208    pub fn with_rpc_modifier<G>(self, modifier: G) -> Self
209    where
210        G: Fn(RpcServerArgs) -> RpcServerArgs + Send + Sync + 'static,
211    {
212        self.with_node_config_modifier(move |mut config| {
213            config.rpc = modifier(config.rpc);
214            config
215        })
216    }
217
218    /// Sets the pruning arguments for the test nodes.
219    pub fn with_pruning(self, pruning: PruningArgs) -> Self {
220        self.with_node_config_modifier(move |config| config.with_pruning(pruning.clone()))
221    }
222
223    /// Sets whether nodes use the v2 storage layout (`--storage.v2`), which routes tx hashes,
224    /// history indices, etc. to `RocksDB` and changesets/senders to static files.
225    ///
226    /// Defaults to the node's `--storage.v2` default. Node config modifiers run afterwards and can
227    /// still override it.
228    pub const fn with_storage_v2(mut self, storage_v2: bool) -> Self {
229        self.storage_v2 = storage_v2;
230        self
231    }
232
233    /// Sets whether the nodes run in dev mode (`--dev`).
234    ///
235    /// Unlike [dev mining](Self::with_dev_mining), this only sets the dev flag and does not start a
236    /// local miner.
237    pub fn with_dev_mode(self, dev: bool) -> Self {
238        self.with_node_config_modifier(move |config| config.set_dev(dev))
239    }
240
241    /// Launches the node in dev mode with a local miner that builds a block every `block_time`, or
242    /// as soon as a transaction is pending if `None`.
243    ///
244    /// Dev mining is limited to a single node, since every node would mine its own chain. The local
245    /// miner drives forkchoice and payload building, so the node does not receive the initial
246    /// forkchoice update to genesis, and the block producing helpers of [`NodeTestContext`], e.g.
247    /// [`NodeTestContext::advance_block`] and [`NodeTestContext::update_forkchoice`], must not be
248    /// used. Use [`Self::map_dev_payload_attributes`] to customize the payload attributes of the
249    /// mined blocks.
250    pub fn with_dev_mining(mut self, block_time: Option<Duration>) -> Self
251    where
252        N: DebugNode<Adapter<N>>,
253    {
254        self.dev_launcher =
255            Some(Arc::new(|args, database| Box::pin(launch_dev_node::<N>(args, database))));
256        self.with_node_config_modifier(move |mut config| {
257            config.dev.dev = true;
258            config.dev.block_time = block_time;
259            config
260        })
261    }
262
263    /// Maps the payload attributes of the blocks built by the local miner of
264    /// [dev mining](Self::with_dev_mining) nodes, e.g. to set the fee recipient.
265    ///
266    /// Mappers are applied in the order they are added. Has no effect unless dev mining is
267    /// enabled.
268    pub fn map_dev_payload_attributes<G>(mut self, map: G) -> Self
269    where
270        G: Fn(PayloadAttrTy<N>) -> PayloadAttrTy<N> + Send + Sync + 'static,
271    {
272        self.dev_payload_attributes = Some(match self.dev_payload_attributes.take() {
273            Some(prev) => Arc::new(move |attributes| map(prev(attributes))),
274            None => Arc::new(map),
275        });
276        self
277    }
278
279    /// Launches each node with the engine's backfill built by the [`BackfillSyncBuilder`] that
280    /// `backfill` returns for the node's final configuration, instead of the staged pipeline.
281    ///
282    /// Like a node launched from the command line, the backfill follows the node's flags, e.g.
283    /// `with_backfill(EthereumBackfill::new)` snap syncs once a node configuration modifier sets
284    /// `--snap.v2`. A restarted node builds its backfill again from its configuration.
285    ///
286    /// Cannot be combined with [dev mining](Self::with_dev_mining): the harness launches dev mining
287    /// nodes with the staged pipeline.
288    pub fn with_backfill<F, B>(mut self, backfill: F) -> Self
289    where
290        F: Fn(&NodeConfig<N::ChainSpec>) -> B + Send + Sync + 'static,
291        B: BackfillSyncBuilder<NodeTypesWithDBAdapter<N, TmpDB>, BackfillClient<N>> + 'static,
292    {
293        self.backfill_launcher = Some(Arc::new(move |args, database| {
294            let backfill = backfill(&args.node_config);
295            Box::pin(launch_test_node(args, database, backfill))
296        }));
297        self
298    }
299
300    /// Sets the factory of the node instances, which is called with the index of each node when it
301    /// is launched.
302    ///
303    /// Defaults to [`Default::default`]. Use this to configure the node type, or to keep a handle
304    /// of the node instance, e.g. one that reads state of the node once it is launched.
305    pub fn with_node<F>(mut self, node: F) -> Self
306    where
307        F: Fn(usize) -> N + Send + Sync + 'static,
308    {
309        self.node_factory = Arc::new(node);
310        self
311    }
312
313    /// Adds a modifier for the node builder of each test node.
314    ///
315    /// The closure receives the [`TestNodeBuilder`] after the node types, components and add-ons
316    /// are configured, and returns it to be launched. It can e.g. install an `ExEx` with
317    /// [`install_exex`](NodeBuilderWithComponents::install_exex), extend the RPC modules with
318    /// [`extend_rpc_modules`](NodeBuilderWithComponents::extend_rpc_modules), set the
319    /// `on_component_initialized`, `on_node_started` and `on_rpc_started` hooks, or modify the
320    /// add-ons with [`map_add_ons`](NodeBuilderWithComponents::map_add_ons).
321    ///
322    /// Modifiers are applied to every node in the order they are added. The node builder keeps a
323    /// single hook of each kind, so a modifier that sets e.g. the `extend_rpc_modules` hook
324    /// replaces the one set by an earlier modifier. The datadir and tree configuration of the node
325    /// are derived before the modifiers run, so change the node configuration with
326    /// [`Self::with_node_config_modifier`] instead of `builder.config`.
327    pub fn with_node_builder_modifier<G>(mut self, modifier: G) -> Self
328    where
329        G: Fn(TestNodeBuilder<N>) -> TestNodeBuilder<N> + Send + Sync + 'static,
330    {
331        self.node_builder_modifiers.push(Arc::new(modifier));
332        self
333    }
334
335    /// Makes the nodes restartable, so they can be stopped and launched again on the same datadir
336    /// with [`NodeTestContext::stop`] and [`NodeTestContext::restart`].
337    ///
338    /// Each restartable node runs on a [`Runtime::test`] runtime of its own instead of the shared
339    /// one, so stopping it shuts down only its tasks. This costs a few threads per node, which is
340    /// why nodes are not restartable by default. Conflicts with [`Self::with_runtime`].
341    pub const fn with_restartable_nodes(mut self) -> Self {
342        self.restartable = true;
343        self
344    }
345
346    /// Builds and launches the test nodes.
347    pub async fn build(self) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)> {
348        ensure!(
349            self.dev_launcher.is_none() || self.num_nodes == 1,
350            "dev mining requires a single node setup, got {} nodes",
351            self.num_nodes
352        );
353        ensure!(
354            !self.restartable || self.runtime.is_none(),
355            "restartable nodes run on a runtime of their own and can not use the runtime set with \
356             `with_runtime`"
357        );
358        ensure!(
359            self.dev_launcher.is_none() || self.backfill_launcher.is_none(),
360            "dev mining nodes backfill with the staged pipeline and can not use the backfill set \
361             with `with_backfill`"
362        );
363        // The pipeline would silently replace the snap sync the flag opts into.
364        ensure!(
365            self.backfill_launcher.is_some() || !self.node_and_tree_config().0.network.snap_v2,
366            "nodes with `--snap.v2` need a backfill set with `with_backfill`, e.g. \
367             `EthereumBackfill::new`"
368        );
369        let dev_mining = self.dev_launcher.is_some();
370        let launch = self.dev_launcher.as_ref().or(self.backfill_launcher.as_ref()).cloned();
371        let launch = launch.unwrap_or_else(|| {
372            Arc::new(|args, database| Box::pin(launch_test_node(args, database, PipelineBackfill)))
373        });
374        // Restartable nodes create their own runtime when they are launched.
375        let runtime =
376            (!self.restartable).then(|| self.runtime.clone().unwrap_or_else(Runtime::test));
377
378        let mut nodes = (0..self.num_nodes)
379            .map(async |idx| {
380                let (node_config, tree_config) = self.node_and_tree_config();
381                // The local miner of dev nodes drives forkchoice, unless a modifier disabled dev
382                // mode.
383                let mines = dev_mining && node_config.dev.dev;
384                let args = LaunchArgs {
385                    idx,
386                    node_factory: self.node_factory.clone(),
387                    node_builder_modifiers: self.node_builder_modifiers.clone(),
388                    node_config,
389                    runtime: runtime.clone(),
390                    tree_config,
391                    datadir: reth_db::test_utils::tempdir_path(),
392                    attributes_generator: self.attributes_generator.clone(),
393                    dev_payload_attributes: self.dev_payload_attributes.clone(),
394                };
395                let node = launch_node(launch.clone(), args, self.restartable, mines)
396                    .instrument(span!(Level::INFO, "node", idx))
397                    .await?;
398
399                if !mines {
400                    let genesis = node.block_hash(self.chain_spec.genesis_header().number());
401                    node.update_forkchoice(genesis, genesis).await?;
402                }
403
404                eyre::Ok(node)
405            })
406            .collect::<TryJoinAll<_>>()
407            .await?;
408
409        if self.connect_nodes {
410            for idx in 1..self.num_nodes {
411                let (prev, current) = nodes.split_at_mut(idx);
412                prev[idx - 1].connect(&mut current[0]).await;
413            }
414
415            // Connect the last node with the first if there are more than two.
416            if self.num_nodes > 2 {
417                let (first, rest) = nodes.split_at_mut(1);
418                rest.last_mut().unwrap().connect(&mut first[0]).await;
419            }
420        }
421
422        Ok((nodes, Wallet::default().with_chain_id(self.chain_spec.chain().into())))
423    }
424
425    /// Builds and launches a single test node.
426    ///
427    /// Returns an error if the builder was not configured with exactly one node.
428    pub async fn build_single(self) -> eyre::Result<(NodeHelperType<N>, Wallet)> {
429        ensure!(self.num_nodes == 1, "expected a single node setup, got {} nodes", self.num_nodes);
430        let (mut nodes, wallet) = self.build().await?;
431        Ok((nodes.pop().expect("one node was launched"), wallet))
432    }
433
434    /// Returns the configuration of a test node and the tree configuration derived from it.
435    ///
436    /// The node configuration modifiers run first, the tree configuration modifiers are applied to
437    /// the tree configuration of the resulting node configuration.
438    fn node_and_tree_config(&self) -> (NodeConfig<N::ChainSpec>, TreeConfig) {
439        let node_config = self.node_config_modifiers.iter().fold(
440            test_node_config(self.chain_spec.clone())
441                .with_storage(StorageArgs { v2: self.storage_v2 }),
442            |config, modifier| modifier(config),
443        );
444        let tree_config = self
445            .tree_config_modifiers
446            .iter()
447            .fold(node_config.tree_config(), |config, modifier| modifier(config));
448        (node_config, tree_config)
449    }
450}
451
452impl<N: NodeBuilderHelper> std::fmt::Debug for E2ETestSetupBuilder<N> {
453    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454        f.debug_struct("E2ETestSetupBuilder")
455            .field("num_nodes", &self.num_nodes)
456            .field("runtime", &self.runtime)
457            .field("connect_nodes", &self.connect_nodes)
458            .field("tree_config_modifiers", &self.tree_config_modifiers.len())
459            .field("node_config_modifiers", &self.node_config_modifiers.len())
460            .field("storage_v2", &self.storage_v2)
461            .field("dev_mining", &self.dev_launcher.is_some())
462            .field("custom_backfill", &self.backfill_launcher.is_some())
463            .field("node_builder_modifiers", &self.node_builder_modifiers.len())
464            .field("restartable", &self.restartable)
465            .finish_non_exhaustive()
466    }
467}
468
469/// Extension trait to create an [`E2ETestSetupBuilder`] from a node type.
470pub trait E2ETestSetupExt: NodeBuilderHelper {
471    /// Returns an [`E2ETestSetupBuilder`] for `num_nodes` nodes of this type, see
472    /// [`E2ETestSetupBuilder::new`].
473    fn test_setup(num_nodes: usize, chain_spec: Arc<Self::ChainSpec>) -> E2ETestSetupBuilder<Self>
474    where
475        PayloadAttrTy<Self>: From<PayloadAttributes>,
476    {
477        E2ETestSetupBuilder::new(num_nodes, chain_spec)
478    }
479
480    /// Returns an [`E2ETestSetupBuilder`] for a single node of this type on the
481    /// [`test_chain_spec`] with every hardfork up to and including `fork` active at genesis, see
482    /// [`E2ETestSetupBuilder::new`].
483    ///
484    /// Use [`E2ETestSetupBuilder::with_num_nodes`] to launch more nodes.
485    fn test_setup_for(fork: EthereumHardfork) -> E2ETestSetupBuilder<Self>
486    where
487        Self::ChainSpec: From<ChainSpec>,
488        PayloadAttrTy<Self>: From<PayloadAttributes>,
489    {
490        let chain_spec = Arc::unwrap_or_clone(test_chain_spec(fork));
491        E2ETestSetupBuilder::new(1, Arc::new(chain_spec.into()))
492    }
493}
494
495impl<N: NodeBuilderHelper> E2ETestSetupExt for N {}
496
497/// Closure that modifies the tree configuration of the test nodes.
498type TreeConfigModifier = Box<dyn Fn(TreeConfig) -> TreeConfig + Send + Sync>;
499
500/// Closure that modifies the node configuration of each test node.
501type NodeConfigModifier<C> = Box<dyn Fn(NodeConfig<C>) -> NodeConfig<C> + Send + Sync>;
502
503/// Closure that generates the payload attributes for a given timestamp.
504pub(crate) type AttributesGenerator<N> = Arc<dyn Fn(u64) -> PayloadAttrTy<N> + Send + Sync>;
505
506/// Closure that maps payload attributes.
507type PayloadAttributesMapper<N> = Arc<dyn Fn(PayloadAttrTy<N>) -> PayloadAttrTy<N> + Send + Sync>;
508
509/// Builder of a test node with the node types, components and add-ons of `N`, which is ready to
510/// be launched.
511///
512/// This is what [`E2ETestSetupBuilder::with_node_builder_modifier`] receives.
513pub type TestNodeBuilder<N> = NodeBuilderWithComponents<
514    TmpNodeAdapter<N>,
515    <N as Node<TmpNodeAdapter<N>>>::ComponentsBuilder,
516    <N as Node<TmpNodeAdapter<N>>>::AddOns,
517>;
518
519/// Closure that returns the node instance of the test node with the given index.
520type NodeFactory<N> = Arc<dyn Fn(usize) -> N + Send + Sync>;
521
522/// Closure that modifies the node builder of each test node.
523type NodeBuilderModifier<N> = Arc<dyn Fn(TestNodeBuilder<N>) -> TestNodeBuilder<N> + Send + Sync>;
524
525/// Closure that launches a single test node on the given database in its datadir.
526type NodeLauncher<N> = Arc<
527    dyn Fn(LaunchArgs<N>, TmpDB) -> BoxFuture<'static, eyre::Result<NodeHelperType<N>>>
528        + Send
529        + Sync,
530>;
531
532/// Client the network of a test node hands its backfill.
533type BackfillClient<N> = BackfillClientFor<TmpNodeAdapter<N>, ComponentsFor<TmpNodeAdapter<N>>>;
534
535/// Arguments for launching a single test node.
536pub(crate) struct LaunchArgs<N: NodeBuilderHelper> {
537    /// The index of the node in the setup.
538    pub(crate) idx: usize,
539    /// Factory of the node instance, called with the index of the node.
540    pub(crate) node_factory: NodeFactory<N>,
541    /// Modifiers for the node builder, applied in order.
542    pub(crate) node_builder_modifiers: Vec<NodeBuilderModifier<N>>,
543    /// The node configuration.
544    pub(crate) node_config: NodeConfig<N::ChainSpec>,
545    /// The runtime to launch the node on, or `None` to launch it on a new [`Runtime::test`]
546    /// runtime of its own.
547    pub(crate) runtime: Option<Runtime>,
548    /// The engine tree configuration.
549    pub(crate) tree_config: TreeConfig,
550    /// The datadir of the node.
551    pub(crate) datadir: PathBuf,
552    /// Generator for the payload attributes of the payloads built by the test context.
553    pub(crate) attributes_generator: AttributesGenerator<N>,
554    /// Mapper for the payload attributes of the local miner in dev mode.
555    pub(crate) dev_payload_attributes: Option<PayloadAttributesMapper<N>>,
556}
557
558// Derived `Clone` would require the chain spec to be `Clone`.
559impl<N: NodeBuilderHelper> Clone for LaunchArgs<N> {
560    fn clone(&self) -> Self {
561        Self {
562            idx: self.idx,
563            node_factory: self.node_factory.clone(),
564            node_builder_modifiers: self.node_builder_modifiers.clone(),
565            node_config: self.node_config.clone(),
566            runtime: self.runtime.clone(),
567            tree_config: self.tree_config.clone(),
568            datadir: self.datadir.clone(),
569            attributes_generator: self.attributes_generator.clone(),
570            dev_payload_attributes: self.dev_payload_attributes.clone(),
571        }
572    }
573}
574
575/// Returns the base configuration of a test node.
576///
577/// Discovery is disabled, all ports are unused, all RPC modules except `testing` are served over
578/// HTTP, the node reports an idle sync state from startup, does not persist its peers and the
579/// engine uses a cross block cache of 1 MiB.
580pub(crate) fn test_node_config<C>(chain_spec: Arc<C>) -> NodeConfig<C> {
581    let mut config = NodeConfig::new(chain_spec)
582        .with_network(NetworkArgs {
583            discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
584            ..NetworkArgs::default()
585        })
586        .with_unused_ports()
587        .with_rpc(
588            RpcServerArgs::default()
589                .with_unused_ports()
590                .with_http()
591                .with_http_api(RpcModuleSelection::All),
592        );
593    // Nodes otherwise report that they are syncing until their first canonical block, which
594    // e.g. stops transaction gossip.
595    config.debug.startup_sync_state_idle = true;
596    // A stopped node would otherwise save its peers and dial them on its own once it is started
597    // again, racing the test that connects it.
598    config.network.no_persist_peers = true;
599    // The cross block cache is allocated up front, and the default of 4 GiB is far more than tests
600    // need. The size is in MiB.
601    config.engine.cross_block_cache_size = 1;
602    // Most tests never validate a blob, so only load the KZG trusted setup when needed.
603    config.debug.lazy_kzg_settings = true;
604    config
605}
606
607/// Launches a test node with the engine launcher and the engine's backfill built by `backfill`.
608pub(crate) async fn launch_test_node<N, B>(
609    args: LaunchArgs<N>,
610    database: TmpDB,
611    backfill: B,
612) -> eyre::Result<NodeHelperType<N>>
613where
614    N: NodeBuilderHelper,
615    B: BackfillSyncBuilder<NodeTypesWithDBAdapter<N, TmpDB>, BackfillClient<N>> + 'static,
616{
617    let LaunchArgs {
618        idx,
619        node_factory,
620        node_builder_modifiers,
621        node_config,
622        runtime,
623        tree_config,
624        datadir,
625        attributes_generator,
626        ..
627    } = args;
628    let (builder, datadir) = test_node_builder(
629        node_factory(idx),
630        node_config,
631        datadir,
632        database,
633        &node_builder_modifiers,
634    );
635    let runtime = runtime.unwrap_or_else(Runtime::test);
636    let NodeHandle { node, node_exit_future } = builder
637        .launch_with(EngineNodeLauncher::new(runtime, datadir, tree_config).with_backfill(backfill))
638        .await?;
639
640    let mut node =
641        NodeTestContext::new(node, move |timestamp| attributes_generator(timestamp)).await?;
642    node.exit_future = Mutex::new(Some(node_exit_future));
643    Ok(node)
644}
645
646/// Launches a test node with the debug launcher, which runs a local miner in dev mode.
647async fn launch_dev_node<N>(args: LaunchArgs<N>, database: TmpDB) -> eyre::Result<NodeHelperType<N>>
648where
649    N: NodeBuilderHelper + DebugNode<Adapter<N>>,
650{
651    let LaunchArgs {
652        idx,
653        node_factory,
654        node_builder_modifiers,
655        node_config,
656        runtime,
657        tree_config,
658        datadir,
659        attributes_generator,
660        dev_payload_attributes,
661    } = args;
662    let (builder, datadir) = test_node_builder(
663        node_factory(idx),
664        node_config,
665        datadir,
666        database,
667        &node_builder_modifiers,
668    );
669    let runtime = runtime.unwrap_or_else(Runtime::test);
670    let launch = builder.launch_with(DebugNodeLauncher::new(EngineNodeLauncher::new(
671        runtime,
672        datadir,
673        tree_config,
674    )));
675    let launch = match dev_payload_attributes {
676        Some(map) => launch.map_debug_payload_attributes(move |attributes| map(attributes)),
677        None => launch,
678    };
679    let NodeHandle { node, node_exit_future } = launch.await?;
680
681    let mut node =
682        NodeTestContext::new(node, move |timestamp| attributes_generator(timestamp)).await?;
683    node.exit_future = Mutex::new(Some(node_exit_future));
684    Ok(node)
685}
686
687/// Returns the builder of the test node `node` with the database in `datadir` and the node
688/// builder modifiers applied, and the resolved datadir of the node.
689fn test_node_builder<N: NodeBuilderHelper>(
690    node: N,
691    node_config: NodeConfig<N::ChainSpec>,
692    datadir: PathBuf,
693    database: TmpDB,
694    node_builder_modifiers: &[NodeBuilderModifier<N>],
695) -> (TestNodeBuilder<N>, ChainPath<DataDirPath>) {
696    let datadir_args =
697        DatadirArgs { datadir: MaybePlatformPath::from(datadir), ..node_config.datadir.clone() };
698    let node_config = node_config.with_datadir_args(datadir_args);
699    let datadir = node_config.datadir();
700    let builder = NodeBuilder::new(node_config)
701        .with_database(database)
702        .with_types_and_provider::<N, BlockchainProvider<_>>()
703        .with_components(node.components_builder())
704        .with_add_ons(node.add_ons());
705    let builder =
706        node_builder_modifiers.iter().fold(builder, |builder, modifier| modifier(builder));
707    (builder, datadir)
708}
709
710/// Launches a test node with `launch` on the database in the datadir of `args`, which is created
711/// if it does not exist yet.
712///
713/// A `restartable` node keeps what [`NodeTestContext::stop`] needs to stop it and launch it again
714/// with the same arguments. Unless the node `mines` in dev mode, the relaunched node receives a
715/// forkchoice update that restates the forkchoice state it loaded from disk.
716fn launch_node<N: NodeBuilderHelper>(
717    launch: NodeLauncher<N>,
718    args: LaunchArgs<N>,
719    restartable: bool,
720    mines: bool,
721) -> BoxFuture<'static, eyre::Result<NodeHelperType<N>>> {
722    Box::pin(async move {
723        let database = open_test_database(&args.datadir)?;
724        let relaunch_args = restartable.then(|| args.clone());
725        let mut node = launch(args, database.clone()).await?;
726        if let Some(args) = relaunch_args {
727            let relaunch: Relaunch<_> =
728                Arc::new(move || relaunch_node(launch.clone(), args.clone(), mines));
729            node.restart = Some(NodeRestart { database, relaunch });
730        }
731        Ok(node)
732    })
733}
734
735/// Launches a stopped restartable node again, see [`launch_node`].
736fn relaunch_node<N: NodeBuilderHelper>(
737    launch: NodeLauncher<N>,
738    args: LaunchArgs<N>,
739    mines: bool,
740) -> BoxFuture<'static, eyre::Result<NodeHelperType<N>>> {
741    let span = span!(Level::INFO, "node", idx = args.idx);
742    Box::pin(
743        async move {
744            let node = launch_node(launch, args, true, mines).await?;
745            if !mines {
746                restate_forkchoice(&node).await?;
747            }
748            Ok(node)
749        }
750        .instrument(span),
751    )
752}
753
754/// Sends a forkchoice update to a restarted node that restates the head, safe and finalized block
755/// it loaded from disk.
756///
757/// Unfinished snap attempts wait for the test to reconnect peers and supply a target.
758/// Returns an error if the engine does not report the update valid.
759async fn restate_forkchoice<N: NodeBuilderHelper>(node: &NodeHelperType<N>) -> eyre::Result<()> {
760    let provider = &node.inner.provider;
761    // During snap sync the latest stored header has no executable state yet. Let the test send
762    // the consensus target after reconnecting peers instead of requiring a VALID response here.
763    if provider
764        .database_provider_ro()?
765        .snap_attempt()?
766        .is_some_and(|attempt| !attempt.is_verified())
767    {
768        return Ok(())
769    }
770    let hash = |tag| -> eyre::Result<Option<B256>> {
771        Ok(provider.sealed_header_by_number_or_tag(tag)?.map(|header| header.hash()))
772    };
773    let head = hash(BlockNumberOrTag::Latest)?
774        .ok_or_else(|| eyre!("the restarted node has no latest block"))?;
775    // A zero hash leaves the safe or finalized block unset if the node did not persist one.
776    let state = ForkchoiceState {
777        head_block_hash: head,
778        safe_block_hash: hash(BlockNumberOrTag::Safe)?.unwrap_or_default(),
779        finalized_block_hash: hash(BlockNumberOrTag::Finalized)?.unwrap_or_default(),
780    };
781    let updated =
782        node.inner.add_ons_handle.beacon_engine_handle.fork_choice_updated(state, None).await?;
783    ensure!(
784        updated.is_valid(),
785        "forkchoice update to the head {head} the restarted node loaded from disk is not valid: {}",
786        updated.payload_status.status
787    );
788    Ok(())
789}
790
791/// Opens the database in `datadir` like [`create_test_rw_db_with_datadir`], creating it if it does
792/// not exist yet.
793///
794/// The datadir is removed when the returned database is dropped.
795///
796/// [`create_test_rw_db_with_datadir`]: reth_db::test_utils::create_test_rw_db_with_datadir
797pub(crate) fn open_test_database(datadir: &Path) -> eyre::Result<TmpDB> {
798    let path = datadir.join("db");
799    let database = init_db(&path, DatabaseArguments::test())
800        .wrap_err_with(|| format!("failed to open the database at {}", path.display()))?;
801    Ok(Arc::new(TempDatabase::new(database, datadir.to_path_buf())))
802}
803
804#[cfg(test)]
805mod tests {
806    use super::*;
807    use reth_node_ethereum::EthereumNode;
808
809    #[test]
810    fn tree_config_uses_small_cross_block_cache() {
811        let (_, tree_config) =
812            EthereumNode::test_setup_for(EthereumHardfork::Cancun).node_and_tree_config();
813        assert_eq!(tree_config.cross_block_cache_size(), 1024 * 1024);
814    }
815
816    #[test]
817    fn tree_config_follows_node_config() {
818        let (_, tree_config) = EthereumNode::test_setup_for(EthereumHardfork::Cancun)
819            .with_node_config_modifier(|mut config| {
820                config.engine.cross_block_cache_size = 2;
821                config.engine.persistence_threshold = 0;
822                config.debug.skip_state_root = true;
823                config
824            })
825            .node_and_tree_config();
826        assert_eq!(tree_config.cross_block_cache_size(), 2 * 1024 * 1024);
827        assert_eq!(tree_config.persistence_threshold(), 0);
828        assert!(tree_config.skip_state_root());
829    }
830
831    #[test]
832    fn tree_config_modifiers_override_node_config() {
833        // Tree config modifiers apply last, even if they are added before node config modifiers.
834        let (_, tree_config) = EthereumNode::test_setup_for(EthereumHardfork::Cancun)
835            .with_tree_config_modifier(|config| config.with_state_root_fallback(false))
836            .with_node_config_modifier(|mut config| {
837                config.engine.state_root_fallback = true;
838                config
839            })
840            .node_and_tree_config();
841        assert!(!tree_config.state_root_fallback());
842    }
843
844    /// Nodes whose payload attributes are not convertible from Ethereum's can be set up with an
845    /// explicit attributes generator.
846    #[expect(dead_code)]
847    async fn build_with_attributes_generator<N: NodeBuilderHelper>(
848        chain_spec: Arc<N::ChainSpec>,
849        attributes_generator: fn(u64) -> PayloadAttrTy<N>,
850    ) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)> {
851        E2ETestSetupBuilder::new_with_attributes_generator(1, chain_spec, attributes_generator)
852            .build()
853            .await
854    }
855
856    /// The node builder modifier can use the hooks of the node builder for any node.
857    #[expect(dead_code)]
858    fn node_builder_modifier_can_use_builder_hooks<N: NodeBuilderHelper>(
859        setup: E2ETestSetupBuilder<N>,
860    ) -> E2ETestSetupBuilder<N> {
861        setup.with_node_builder_modifier(|builder| {
862            builder
863                .on_component_initialized(|_| Ok(()))
864                .on_node_started(|_| Ok(()))
865                .on_rpc_started(|_, _| Ok(()))
866                .extend_rpc_modules(|_| Ok(()))
867                .map_add_ons(|add_ons| add_ons)
868                .install_exex("exex", |_| async { Ok(async { Ok(()) }) })
869        })
870    }
871}