reth_e2e_test_utils/testsuite/
setup.rs1use crate::{
4 testsuite::{actions::expect_fcu_valid, Environment},
5 wait::{poll_until, poll_until_with, PollOpts},
6 E2ETestSetupExt, NodeBuilderHelper,
7};
8use alloy_eips::BlockNumberOrTag;
9use alloy_rpc_types_engine::{ForkchoiceState, ForkchoiceUpdated, PayloadAttributes};
10use eyre::{eyre, Result};
11use reth_chainspec::ChainSpec;
12use reth_node_api::{EngineTypes, PayloadAttrTy, PayloadTypes, TreeConfig};
13use reth_node_core::args::StorageArgs;
14#[cfg(feature = "chain-import")]
15use std::path::Path;
16use std::{marker::PhantomData, sync::Arc, time::Duration};
17use tokio::sync::mpsc;
18use tracing::debug;
19
20#[derive(Debug)]
22pub struct Setup<I> {
23 pub chain_spec: Option<Arc<ChainSpec>>,
25 pub network: NetworkSetup,
27 pub tree_config: TreeConfig,
29 shutdown_tx: Option<mpsc::Sender<()>>,
31 pub is_dev: bool,
33 pub storage_v2: bool,
37 _phantom: PhantomData<I>,
39 #[cfg(feature = "chain-import")]
42 import_result_holder: Option<crate::setup_import::ChainImportResult>,
43 pub import_rlp_path: Option<std::path::PathBuf>,
45}
46
47impl<I> Default for Setup<I> {
48 fn default() -> Self {
49 Self {
50 chain_spec: None,
51 network: NetworkSetup::default(),
52 tree_config: TreeConfig::default(),
53 shutdown_tx: None,
54 is_dev: true,
55 storage_v2: StorageArgs::default().v2,
56 _phantom: Default::default(),
57 #[cfg(feature = "chain-import")]
58 import_result_holder: None,
59 import_rlp_path: None,
60 }
61 }
62}
63
64impl<I> Drop for Setup<I> {
65 fn drop(&mut self) {
66 if let Some(tx) = self.shutdown_tx.take() {
68 let _ = tx.try_send(());
69 }
70 }
71}
72
73impl<I> Setup<I>
74where
75 I: EngineTypes,
76{
77 pub fn with_chain_spec(mut self, chain_spec: Arc<ChainSpec>) -> Self {
79 self.chain_spec = Some(chain_spec);
80 self
81 }
82
83 pub const fn with_network(mut self, network: NetworkSetup) -> Self {
85 self.network = network;
86 self
87 }
88
89 pub const fn with_dev_mode(mut self, is_dev: bool) -> Self {
91 self.is_dev = is_dev;
92 self
93 }
94
95 pub const fn with_tree_config(mut self, tree_config: TreeConfig) -> Self {
97 self.tree_config = tree_config;
98 self
99 }
100
101 pub const fn with_storage_v2(mut self, storage_v2: bool) -> Self {
103 self.storage_v2 = storage_v2;
104 self
105 }
106
107 #[cfg(feature = "chain-import")]
115 pub async fn apply_with_import<N>(
116 &mut self,
117 env: &mut Environment<I>,
118 rlp_path: &Path,
119 ) -> Result<()>
120 where
121 N: NodeBuilderHelper<Payload = I>,
122 {
123 Box::pin(self.apply_with_import_(env, rlp_path)).await
125 }
126
127 #[cfg(feature = "chain-import")]
129 async fn apply_with_import_(
130 &mut self,
131 env: &mut Environment<I>,
132 rlp_path: &Path,
133 ) -> Result<()> {
134 let import_result = self.create_nodes_with_import(rlp_path).await?;
136
137 let mut node_clients = Vec::new();
139 let nodes = &import_result.nodes;
140 for node in nodes {
141 let rpc = node
142 .rpc_client()
143 .ok_or_else(|| eyre!("Failed to create HTTP RPC client for node"))?;
144 let auth = node.auth_server_handle();
145 let url = node.rpc_url();
146 node_clients.push(crate::testsuite::NodeClient::new(rpc, auth, url));
148 }
149
150 self.import_result_holder = Some(import_result);
153
154 self.finalize_setup(env, node_clients, true).await
156 }
157
158 pub async fn apply<N>(&mut self, env: &mut Environment<I>) -> Result<()>
160 where
161 N: NodeBuilderHelper<Payload = I, ChainSpec: From<ChainSpec>>,
162 PayloadAttrTy<N>: From<PayloadAttributes>,
163 {
164 Box::pin(self.apply_::<N>(env)).await
166 }
167
168 async fn apply_<N>(&mut self, env: &mut Environment<I>) -> Result<()>
170 where
171 N: NodeBuilderHelper<Payload = I, ChainSpec: From<ChainSpec>>,
172 PayloadAttrTy<N>: From<PayloadAttributes>,
173 {
174 if let Some(rlp_path) = self.import_rlp_path.take() {
176 #[cfg(feature = "chain-import")]
177 return self.apply_with_import::<N>(env, &rlp_path).await;
178 #[cfg(not(feature = "chain-import"))]
179 return Err(eyre!(
180 "importing {} requires the `chain-import` feature",
181 rlp_path.display()
182 ));
183 }
184 let chain_spec =
185 self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
186
187 let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1);
188 self.shutdown_tx = Some(shutdown_tx);
189
190 let tree_config = self.tree_config.clone();
191
192 let result = N::test_setup(
193 self.network.node_count,
194 Arc::<N::ChainSpec>::new((*chain_spec).clone().into()),
195 )
196 .with_tree_config_modifier(move |base| {
197 tree_config.clone().with_cross_block_cache_size(base.cross_block_cache_size())
198 })
199 .with_dev_mode(self.is_dev)
200 .with_storage_v2(self.storage_v2)
201 .with_connect_nodes(self.network.connect_nodes)
202 .build()
203 .await;
204
205 let mut node_clients = Vec::new();
206 match result {
207 Ok((nodes, _wallet)) => {
208 for node in &nodes {
210 node_clients.push(node.to_node_client()?);
211 }
212
213 tokio::spawn(async move {
215 let _nodes = nodes;
217 let _ = shutdown_rx.recv().await;
219 });
221 }
222 Err(e) => {
223 return Err(eyre!("Failed to setup nodes: {}", e));
224 }
225 }
226
227 self.finalize_setup(env, node_clients, false).await
229 }
230
231 #[cfg(feature = "chain-import")]
237 async fn create_nodes_with_import(
238 &self,
239 rlp_path: &Path,
240 ) -> Result<crate::setup_import::ChainImportResult> {
241 let chain_spec =
242 self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
243
244 crate::setup_import::setup_engine_with_chain_import(
245 self.network.node_count,
246 chain_spec,
247 self.is_dev,
248 self.storage_v2,
249 self.tree_config.clone(),
250 rlp_path,
251 )
252 .await
253 }
254
255 async fn finalize_setup(
257 &self,
258 env: &mut Environment<I>,
259 node_clients: Vec<crate::testsuite::NodeClient<I>>,
260 use_latest_block: bool,
261 ) -> Result<()> {
262 if node_clients.is_empty() {
263 return Err(eyre!("No nodes were created"));
264 }
265
266 self.wait_for_nodes_ready(&node_clients).await?;
268
269 env.node_clients = node_clients;
270 env.initialize_node_states(self.network.node_count);
271
272 let (initial_block_info, genesis_block_info) = if use_latest_block {
274 let latest =
276 self.get_block_info(&env.node_clients[0], BlockNumberOrTag::Latest).await?;
277 let genesis =
278 self.get_block_info(&env.node_clients[0], BlockNumberOrTag::Number(0)).await?;
279 (latest, genesis)
280 } else {
281 let genesis =
283 self.get_block_info(&env.node_clients[0], BlockNumberOrTag::Number(0)).await?;
284 (genesis, genesis)
285 };
286
287 let fork_choice_state = ForkchoiceState {
289 head_block_hash: initial_block_info.hash,
290 safe_block_hash: initial_block_info.hash,
291 finalized_block_hash: genesis_block_info.hash,
292 };
293 for (node_idx, node_state) in env.node_states.iter_mut().enumerate() {
294 node_state.current_block_info = Some(initial_block_info);
295 node_state.latest_header_time = initial_block_info.timestamp;
296 node_state.latest_fork_choice_state = fork_choice_state;
297
298 debug!(
299 "Node {} initialized with block {} (hash: {})",
300 node_idx, initial_block_info.number, initial_block_info.hash
301 );
302 }
303
304 if use_latest_block {
307 self.wait_for_forkchoice_valid(&env.node_clients, fork_choice_state).await?;
308 }
309
310 debug!(
311 "Environment initialized with {} nodes, starting from block {} (hash: {})",
312 self.network.node_count, initial_block_info.number, initial_block_info.hash
313 );
314
315 Ok(())
316 }
317
318 async fn wait_for_nodes_ready<P>(
320 &self,
321 node_clients: &[crate::testsuite::NodeClient<P>],
322 ) -> Result<()>
323 where
324 P: PayloadTypes,
325 {
326 for (idx, client) in node_clients.iter().enumerate() {
327 poll_until(format!("node {idx} RPC endpoint to accept requests"), || async {
328 Ok(client.is_ready().await.then_some(()))
329 })
330 .await?;
331 debug!("Node {idx} RPC endpoint is ready");
332 }
333 Ok(())
334 }
335
336 async fn wait_for_forkchoice_valid(
345 &self,
346 node_clients: &[crate::testsuite::NodeClient<I>],
347 state: ForkchoiceState,
348 ) -> Result<()> {
349 for (idx, client) in node_clients.iter().enumerate() {
350 let engine = client.engine.http_client();
351 let response = poll_until_with(
352 PollOpts { interval: Duration::from_millis(100), ..Default::default() },
353 format!("node {idx} to stop syncing to block {}", state.head_block_hash),
354 || async {
355 let response = engine
356 .request::<_, ForkchoiceUpdated>(
357 "engine_forkchoiceUpdatedV3",
358 (state, None::<PayloadAttributes>),
359 )
360 .await?;
361 Ok((!response.is_syncing()).then_some(response))
362 },
363 )
364 .await?;
365 expect_fcu_valid(
366 &response,
367 &format!("Node {idx} forkchoice update to block {}", state.head_block_hash),
368 )?;
369 }
370 Ok(())
371 }
372
373 async fn get_block_info<P>(
375 &self,
376 client: &crate::testsuite::NodeClient<P>,
377 block: BlockNumberOrTag,
378 ) -> Result<crate::testsuite::BlockInfo>
379 where
380 P: PayloadTypes,
381 {
382 let block = client
383 .get_block_by_number(block)
384 .await?
385 .ok_or_else(|| eyre!("Block {:?} not found", block))?;
386
387 Ok(crate::testsuite::BlockInfo {
388 hash: block.header.hash,
389 number: block.header.number,
390 timestamp: block.header.timestamp,
391 })
392 }
393}
394
395#[derive(Debug, Default)]
397pub struct NetworkSetup {
398 pub node_count: usize,
400 pub connect_nodes: bool,
402}
403
404impl NetworkSetup {
405 pub const fn single_node() -> Self {
407 Self { node_count: 1, connect_nodes: true }
408 }
409
410 pub const fn multi_node(count: usize) -> Self {
412 Self { node_count: count, connect_nodes: true }
413 }
414
415 pub const fn multi_node_unconnected(count: usize) -> Self {
417 Self { node_count: count, connect_nodes: false }
418 }
419}