1use crate::{
7 eth_payload_attributes, node::NodeTestContext, test_chain_spec, wallet::Wallet, Adapter,
8 NodeBuilderHelper, NodeHelperType, TmpNodeAdapter,
9};
10use eyre::ensure;
11use futures_util::future::{BoxFuture, TryJoinAll};
12use reth_chainspec::{ChainSpec, EthChainSpec, EthereumHardfork};
13use reth_node_api::{PayloadAttrTy, TreeConfig};
14use reth_node_builder::{
15 DebugNode, DebugNodeLauncher, EngineNodeLauncher, Node, NodeBuilder, NodeBuilderWithComponents,
16 NodeConfig, NodeHandle,
17};
18use reth_node_core::{
19 args::{DatadirArgs, DiscoveryArgs, NetworkArgs, PruningArgs, RpcServerArgs, StorageArgs},
20 dirs::{ChainPath, DataDirPath, MaybePlatformPath},
21};
22use reth_primitives_traits::AlloyBlockHeader;
23use reth_provider::providers::BlockchainProvider;
24use reth_rpc_server_types::RpcModuleSelection;
25use reth_tasks::Runtime;
26use std::{path::PathBuf, sync::Arc, time::Duration};
27use tracing::{span, Instrument, Level};
28
29pub struct E2ETestSetupBuilder<N: NodeBuilderHelper> {
54 num_nodes: usize,
55 chain_spec: Arc<N::ChainSpec>,
56 runtime: Option<Runtime>,
57 attributes_generator: Option<AttributesGenerator<N>>,
58 connect_nodes: bool,
59 tree_config_modifiers: Vec<TreeConfigModifier>,
60 node_config_modifiers: Vec<NodeConfigModifier<N::ChainSpec>>,
61 storage_v2: bool,
62 dev_launcher: Option<NodeLauncher<N>>,
63 dev_payload_attributes: Option<PayloadAttributesMapper<N>>,
64}
65
66impl<N: NodeBuilderHelper> E2ETestSetupBuilder<N> {
67 pub fn new(num_nodes: usize, chain_spec: Arc<N::ChainSpec>) -> Self {
69 Self {
70 num_nodes,
71 chain_spec,
72 runtime: None,
73 attributes_generator: None,
74 connect_nodes: true,
75 tree_config_modifiers: Vec::new(),
76 node_config_modifiers: Vec::new(),
77 storage_v2: StorageArgs::default().v2,
78 dev_launcher: None,
79 dev_payload_attributes: None,
80 }
81 }
82
83 pub const fn with_num_nodes(mut self, num_nodes: usize) -> Self {
85 self.num_nodes = num_nodes;
86 self
87 }
88
89 pub fn with_runtime(mut self, runtime: Runtime) -> Self {
95 self.runtime = Some(runtime);
96 self
97 }
98
99 pub fn with_attributes_generator<G>(mut self, generator: G) -> Self
104 where
105 G: Fn(u64) -> PayloadAttrTy<N> + Send + Sync + 'static,
106 {
107 self.attributes_generator = Some(Arc::new(generator));
108 self
109 }
110
111 pub const fn with_connect_nodes(mut self, connect_nodes: bool) -> Self {
113 self.connect_nodes = connect_nodes;
114 self
115 }
116
117 pub fn with_tree_config_modifier<G>(mut self, modifier: G) -> Self
122 where
123 G: Fn(TreeConfig) -> TreeConfig + Send + Sync + 'static,
124 {
125 self.tree_config_modifiers.push(Box::new(modifier));
126 self
127 }
128
129 pub fn with_node_config_modifier<G>(mut self, modifier: G) -> Self
133 where
134 G: Fn(NodeConfig<N::ChainSpec>) -> NodeConfig<N::ChainSpec> + Send + Sync + 'static,
135 {
136 self.node_config_modifiers.push(Box::new(modifier));
137 self
138 }
139
140 pub fn with_rpc_modifier<G>(self, modifier: G) -> Self
145 where
146 G: Fn(RpcServerArgs) -> RpcServerArgs + Send + Sync + 'static,
147 {
148 self.with_node_config_modifier(move |mut config| {
149 config.rpc = modifier(config.rpc);
150 config
151 })
152 }
153
154 pub fn with_pruning(self, pruning: PruningArgs) -> Self {
156 self.with_node_config_modifier(move |config| config.with_pruning(pruning.clone()))
157 }
158
159 pub const fn with_storage_v2(mut self, storage_v2: bool) -> Self {
165 self.storage_v2 = storage_v2;
166 self
167 }
168
169 pub fn with_dev_mode(self, dev: bool) -> Self {
174 self.with_node_config_modifier(move |config| config.set_dev(dev))
175 }
176
177 pub fn with_dev_mining(mut self, block_time: Option<Duration>) -> Self
187 where
188 N: DebugNode<Adapter<N>>,
189 {
190 self.dev_launcher = Some(|args| Box::pin(launch_dev_node::<N>(args)));
191 self.with_node_config_modifier(move |mut config| {
192 config.dev.dev = true;
193 config.dev.block_time = block_time;
194 config
195 })
196 }
197
198 pub fn map_dev_payload_attributes<G>(mut self, map: G) -> Self
204 where
205 G: Fn(PayloadAttrTy<N>) -> PayloadAttrTy<N> + Send + Sync + 'static,
206 {
207 self.dev_payload_attributes = Some(match self.dev_payload_attributes.take() {
208 Some(prev) => Arc::new(move |attributes| map(prev(attributes))),
209 None => Arc::new(map),
210 });
211 self
212 }
213
214 pub async fn build(self) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)> {
216 ensure!(
217 self.dev_launcher.is_none() || self.num_nodes == 1,
218 "dev mining requires a single node setup, got {} nodes",
219 self.num_nodes
220 );
221 let dev_mining = self.dev_launcher.is_some();
222 let launch = self.dev_launcher.unwrap_or(|args| Box::pin(launch_test_node::<N>(args)));
223 let runtime = self.runtime.unwrap_or_else(Runtime::test);
224 let attributes_generator = self.attributes_generator.unwrap_or_else(|| {
225 let chain_spec = self.chain_spec.clone();
226 Arc::new(move |timestamp| eth_payload_attributes(&chain_spec, timestamp).into())
227 });
228 let tree_config = self
229 .tree_config_modifiers
230 .iter()
231 .fold(test_tree_config(), |config, modifier| modifier(config));
232
233 let mut nodes = (0..self.num_nodes)
234 .map(async |idx| {
235 let node_config = self.node_config_modifiers.iter().fold(
236 test_node_config(self.chain_spec.clone())
237 .with_storage(StorageArgs { v2: self.storage_v2 }),
238 |config, modifier| modifier(config),
239 );
240 let mines = dev_mining && node_config.dev.dev;
243 let node = launch(LaunchArgs {
244 node_config,
245 runtime: runtime.clone(),
246 tree_config: tree_config.clone(),
247 datadir: reth_db::test_utils::tempdir_path(),
248 attributes_generator: attributes_generator.clone(),
249 dev_payload_attributes: self.dev_payload_attributes.clone(),
250 })
251 .instrument(span!(Level::INFO, "node", idx))
252 .await?;
253
254 if !mines {
255 let genesis = node.block_hash(self.chain_spec.genesis_header().number());
256 node.update_forkchoice(genesis, genesis).await?;
257 }
258
259 eyre::Ok(node)
260 })
261 .collect::<TryJoinAll<_>>()
262 .await?;
263
264 if self.connect_nodes {
265 for idx in 1..self.num_nodes {
266 let (prev, current) = nodes.split_at_mut(idx);
267 prev[idx - 1].connect(&mut current[0]).await;
268 }
269
270 if self.num_nodes > 2 {
272 let (first, rest) = nodes.split_at_mut(1);
273 rest.last_mut().unwrap().connect(&mut first[0]).await;
274 }
275 }
276
277 Ok((nodes, Wallet::default().with_chain_id(self.chain_spec.chain().into())))
278 }
279
280 pub async fn build_single(self) -> eyre::Result<(NodeHelperType<N>, Wallet)> {
284 ensure!(self.num_nodes == 1, "expected a single node setup, got {} nodes", self.num_nodes);
285 let (mut nodes, wallet) = self.build().await?;
286 Ok((nodes.pop().expect("one node was launched"), wallet))
287 }
288}
289
290impl<N: NodeBuilderHelper> std::fmt::Debug for E2ETestSetupBuilder<N> {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 f.debug_struct("E2ETestSetupBuilder")
293 .field("num_nodes", &self.num_nodes)
294 .field("runtime", &self.runtime)
295 .field("connect_nodes", &self.connect_nodes)
296 .field("tree_config_modifiers", &self.tree_config_modifiers.len())
297 .field("node_config_modifiers", &self.node_config_modifiers.len())
298 .field("storage_v2", &self.storage_v2)
299 .field("dev_mining", &self.dev_launcher.is_some())
300 .finish_non_exhaustive()
301 }
302}
303
304pub trait E2ETestSetupExt: NodeBuilderHelper {
306 fn test_setup(num_nodes: usize, chain_spec: Arc<Self::ChainSpec>) -> E2ETestSetupBuilder<Self> {
308 E2ETestSetupBuilder::new(num_nodes, chain_spec)
309 }
310
311 fn test_setup_for(fork: EthereumHardfork) -> E2ETestSetupBuilder<Self>
316 where
317 Self::ChainSpec: From<ChainSpec>,
318 {
319 let chain_spec = Arc::unwrap_or_clone(test_chain_spec(fork));
320 E2ETestSetupBuilder::new(1, Arc::new(chain_spec.into()))
321 }
322}
323
324impl<N: NodeBuilderHelper> E2ETestSetupExt for N {}
325
326type TreeConfigModifier = Box<dyn Fn(TreeConfig) -> TreeConfig + Send + Sync>;
328
329type NodeConfigModifier<C> = Box<dyn Fn(NodeConfig<C>) -> NodeConfig<C> + Send + Sync>;
331
332pub(crate) type AttributesGenerator<N> = Arc<dyn Fn(u64) -> PayloadAttrTy<N> + Send + Sync>;
334
335type PayloadAttributesMapper<N> = Arc<dyn Fn(PayloadAttrTy<N>) -> PayloadAttrTy<N> + Send + Sync>;
337
338type TestNodeBuilder<N> = NodeBuilderWithComponents<
340 TmpNodeAdapter<N>,
341 <N as Node<TmpNodeAdapter<N>>>::ComponentsBuilder,
342 <N as Node<TmpNodeAdapter<N>>>::AddOns,
343>;
344
345type NodeLauncher<N> = fn(LaunchArgs<N>) -> BoxFuture<'static, eyre::Result<NodeHelperType<N>>>;
347
348pub(crate) struct LaunchArgs<N: NodeBuilderHelper> {
350 pub(crate) node_config: NodeConfig<N::ChainSpec>,
352 pub(crate) runtime: Runtime,
354 pub(crate) tree_config: TreeConfig,
356 pub(crate) datadir: PathBuf,
358 pub(crate) attributes_generator: AttributesGenerator<N>,
360 pub(crate) dev_payload_attributes: Option<PayloadAttributesMapper<N>>,
362}
363
364pub(crate) fn test_tree_config() -> TreeConfig {
366 TreeConfig::default().with_cross_block_cache_size(1024 * 1024)
367}
368
369pub(crate) fn test_node_config<C>(chain_spec: Arc<C>) -> NodeConfig<C> {
374 let mut config = NodeConfig::new(chain_spec)
375 .with_network(NetworkArgs {
376 discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
377 ..NetworkArgs::default()
378 })
379 .with_unused_ports()
380 .with_rpc(
381 RpcServerArgs::default()
382 .with_unused_ports()
383 .with_http()
384 .with_http_api(RpcModuleSelection::All),
385 );
386 config.debug.startup_sync_state_idle = true;
389 config
390}
391
392pub(crate) async fn launch_test_node<N: NodeBuilderHelper>(
394 args: LaunchArgs<N>,
395) -> eyre::Result<NodeHelperType<N>> {
396 let LaunchArgs { node_config, runtime, tree_config, datadir, attributes_generator, .. } = args;
397 let (builder, datadir) = test_node_builder::<N>(node_config, datadir);
398 let NodeHandle { node, node_exit_future: _ } =
399 builder.launch_with(EngineNodeLauncher::new(runtime, datadir, tree_config)).await?;
400
401 NodeTestContext::new(node, move |timestamp| attributes_generator(timestamp)).await
402}
403
404async fn launch_dev_node<N>(args: LaunchArgs<N>) -> eyre::Result<NodeHelperType<N>>
406where
407 N: NodeBuilderHelper + DebugNode<Adapter<N>>,
408{
409 let LaunchArgs {
410 node_config,
411 runtime,
412 tree_config,
413 datadir,
414 attributes_generator,
415 dev_payload_attributes,
416 } = args;
417 let (builder, datadir) = test_node_builder::<N>(node_config, datadir);
418 let launch = builder.launch_with(DebugNodeLauncher::new(EngineNodeLauncher::new(
419 runtime,
420 datadir,
421 tree_config,
422 )));
423 let launch = match dev_payload_attributes {
424 Some(map) => launch.map_debug_payload_attributes(move |attributes| map(attributes)),
425 None => launch,
426 };
427 let NodeHandle { node, node_exit_future: _ } = launch.await?;
428
429 NodeTestContext::new(node, move |timestamp| attributes_generator(timestamp)).await
430}
431
432fn test_node_builder<N: NodeBuilderHelper>(
437 node_config: NodeConfig<N::ChainSpec>,
438 datadir: PathBuf,
439) -> (TestNodeBuilder<N>, ChainPath<DataDirPath>) {
440 let datadir_args =
441 DatadirArgs { datadir: MaybePlatformPath::from(datadir), ..node_config.datadir.clone() };
442 let node_config = node_config.with_datadir_args(datadir_args);
443 let datadir = node_config.datadir();
444 let database = reth_db::test_utils::create_test_rw_db_with_datadir(datadir.data_dir());
445 let node = N::default();
446 let builder = NodeBuilder::new(node_config)
447 .with_database(database)
448 .with_types_and_provider::<N, BlockchainProvider<_>>()
449 .with_components(node.components_builder())
450 .with_add_ons(node.add_ons());
451 (builder, datadir)
452}