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