reth_e2e_test_utils/testsuite/actions/
node_ops.rs1use crate::testsuite::{Action, Environment};
4use alloy_rpc_types_eth::{Block, Header, Receipt, Transaction, TransactionRequest};
5use eyre::Result;
6use futures_util::future::BoxFuture;
7use reth_ethereum_primitives::TransactionSigned;
8use reth_node_api::EngineTypes;
9use reth_rpc_api::clients::EthApiClient;
10use std::time::Duration;
11use tokio::time::{sleep, timeout};
12use tracing::debug;
13
14#[derive(Debug)]
16pub struct SelectActiveNode {
17 pub node_idx: usize,
19}
20
21impl SelectActiveNode {
22 pub const fn new(node_idx: usize) -> Self {
24 Self { node_idx }
25 }
26}
27
28impl<Engine> Action<Engine> for SelectActiveNode
29where
30 Engine: EngineTypes,
31{
32 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
33 Box::pin(async move {
34 env.set_active_node(self.node_idx)?;
35 debug!("Set active node to {}", self.node_idx);
36 Ok(())
37 })
38 }
39}
40
41#[derive(Debug)]
43pub struct CompareNodeChainTips {
44 pub node_a: usize,
46 pub node_b: usize,
48 pub should_be_equal: bool,
50}
51
52impl CompareNodeChainTips {
53 pub const fn expect_same(node_a: usize, node_b: usize) -> Self {
55 Self { node_a, node_b, should_be_equal: true }
56 }
57
58 pub const fn expect_different(node_a: usize, node_b: usize) -> Self {
60 Self { node_a, node_b, should_be_equal: false }
61 }
62}
63
64impl<Engine> Action<Engine> for CompareNodeChainTips
65where
66 Engine: EngineTypes,
67{
68 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
69 Box::pin(async move {
70 if self.node_a >= env.node_count() || self.node_b >= env.node_count() {
71 return Err(eyre::eyre!("Node index out of bounds"));
72 }
73
74 let node_a_client = &env.node_clients[self.node_a];
75 let node_b_client = &env.node_clients[self.node_b];
76
77 let block_a = EthApiClient::<
79 TransactionRequest,
80 Transaction,
81 Block,
82 Receipt,
83 Header,
84 TransactionSigned,
85 >::block_by_number(
86 &node_a_client.rpc, alloy_eips::BlockNumberOrTag::Latest, false
87 )
88 .await?
89 .ok_or_else(|| eyre::eyre!("Failed to get latest block from node {}", self.node_a))?;
90
91 let block_b = EthApiClient::<
92 TransactionRequest,
93 Transaction,
94 Block,
95 Receipt,
96 Header,
97 TransactionSigned,
98 >::block_by_number(
99 &node_b_client.rpc, alloy_eips::BlockNumberOrTag::Latest, false
100 )
101 .await?
102 .ok_or_else(|| eyre::eyre!("Failed to get latest block from node {}", self.node_b))?;
103
104 let tips_equal = block_a.header.hash == block_b.header.hash;
105
106 debug!(
107 "Node {} chain tip: {} (block {}), Node {} chain tip: {} (block {})",
108 self.node_a,
109 block_a.header.hash,
110 block_a.header.number,
111 self.node_b,
112 block_b.header.hash,
113 block_b.header.number
114 );
115
116 if self.should_be_equal && !tips_equal {
117 return Err(eyre::eyre!(
118 "Expected nodes {} and {} to have the same chain tip, but node {} has {} and node {} has {}",
119 self.node_a, self.node_b, self.node_a, block_a.header.hash, self.node_b, block_b.header.hash
120 ));
121 }
122
123 if !self.should_be_equal && tips_equal {
124 return Err(eyre::eyre!(
125 "Expected nodes {} and {} to have different chain tips, but both have {}",
126 self.node_a,
127 self.node_b,
128 block_a.header.hash
129 ));
130 }
131
132 Ok(())
133 })
134 }
135}
136
137#[derive(Debug)]
139pub struct CaptureBlockOnNode {
140 pub tag: String,
142 pub node_idx: usize,
144}
145
146impl CaptureBlockOnNode {
147 pub fn new(tag: impl Into<String>, node_idx: usize) -> Self {
149 Self { tag: tag.into(), node_idx }
150 }
151}
152
153impl<Engine> Action<Engine> for CaptureBlockOnNode
154where
155 Engine: EngineTypes,
156{
157 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
158 Box::pin(async move {
159 let node_state = env.node_state(self.node_idx)?;
160 let current_block = node_state.current_block_info.ok_or_else(|| {
161 eyre::eyre!("No current block information available for node {}", self.node_idx)
162 })?;
163
164 env.block_registry.insert(self.tag.clone(), (current_block, self.node_idx));
165
166 debug!(
167 "Captured block {} (hash: {}) from node {} with tag '{}'",
168 current_block.number, current_block.hash, self.node_idx, self.tag
169 );
170
171 Ok(())
172 })
173 }
174}
175
176#[derive(Debug)]
178pub struct ValidateBlockTag {
179 pub tag: String,
181 pub expected_node_idx: Option<usize>,
183}
184
185impl ValidateBlockTag {
186 pub fn exists(tag: impl Into<String>) -> Self {
188 Self { tag: tag.into(), expected_node_idx: None }
189 }
190
191 pub fn from_node(tag: impl Into<String>, node_idx: usize) -> Self {
193 Self { tag: tag.into(), expected_node_idx: Some(node_idx) }
194 }
195}
196
197impl<Engine> Action<Engine> for ValidateBlockTag
198where
199 Engine: EngineTypes,
200{
201 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
202 Box::pin(async move {
203 let (block_info, node_idx) = env
204 .block_registry
205 .get(&self.tag)
206 .copied()
207 .ok_or_else(|| eyre::eyre!("Block tag '{}' not found in registry", self.tag))?;
208
209 if let Some(expected_node) = self.expected_node_idx &&
210 node_idx != expected_node
211 {
212 return Err(eyre::eyre!(
213 "Block tag '{}' came from node {} but expected node {}",
214 self.tag,
215 node_idx,
216 expected_node
217 ));
218 }
219
220 debug!(
221 "Validated block tag '{}': block {} (hash: {}) from node {}",
222 self.tag, block_info.number, block_info.hash, node_idx
223 );
224
225 Ok(())
226 })
227 }
228}
229
230#[derive(Debug)]
232pub struct WaitForSync {
233 pub node_a: usize,
235 pub node_b: usize,
237 pub timeout_secs: u64,
239 pub poll_interval_secs: u64,
241}
242
243impl WaitForSync {
244 pub const fn new(node_a: usize, node_b: usize) -> Self {
246 Self { node_a, node_b, timeout_secs: 30, poll_interval_secs: 1 }
247 }
248
249 pub const fn with_timeout(mut self, timeout_secs: u64) -> Self {
251 self.timeout_secs = timeout_secs;
252 self
253 }
254
255 pub const fn with_poll_interval(mut self, poll_interval_secs: u64) -> Self {
257 self.poll_interval_secs = poll_interval_secs;
258 self
259 }
260}
261
262impl<Engine> Action<Engine> for WaitForSync
263where
264 Engine: EngineTypes,
265{
266 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
267 Box::pin(async move {
268 if self.node_a >= env.node_count() || self.node_b >= env.node_count() {
269 return Err(eyre::eyre!("Node index out of bounds"));
270 }
271
272 let timeout_duration = Duration::from_secs(self.timeout_secs);
273 let poll_interval = Duration::from_secs(self.poll_interval_secs);
274
275 debug!(
276 "Waiting for nodes {} and {} to sync (timeout: {}s, poll interval: {}s)",
277 self.node_a, self.node_b, self.timeout_secs, self.poll_interval_secs
278 );
279
280 let sync_check = async {
281 loop {
282 let node_a_client = &env.node_clients[self.node_a];
283 let node_b_client = &env.node_clients[self.node_b];
284
285 let block_a = EthApiClient::<
287 TransactionRequest,
288 Transaction,
289 Block,
290 Receipt,
291 Header,
292 TransactionSigned,
293 >::block_by_number(
294 &node_a_client.rpc,
295 alloy_eips::BlockNumberOrTag::Latest,
296 false,
297 )
298 .await?
299 .ok_or_else(|| {
300 eyre::eyre!("Failed to get latest block from node {}", self.node_a)
301 })?;
302
303 let block_b = EthApiClient::<
304 TransactionRequest,
305 Transaction,
306 Block,
307 Receipt,
308 Header,
309 TransactionSigned,
310 >::block_by_number(
311 &node_b_client.rpc,
312 alloy_eips::BlockNumberOrTag::Latest,
313 false,
314 )
315 .await?
316 .ok_or_else(|| {
317 eyre::eyre!("Failed to get latest block from node {}", self.node_b)
318 })?;
319
320 debug!(
321 "Sync check: Node {} tip: {} (block {}), Node {} tip: {} (block {})",
322 self.node_a,
323 block_a.header.hash,
324 block_a.header.number,
325 self.node_b,
326 block_b.header.hash,
327 block_b.header.number
328 );
329
330 if block_a.header.hash == block_b.header.hash {
331 debug!(
332 "Nodes {} and {} successfully synced to block {} (hash: {})",
333 self.node_a, self.node_b, block_a.header.number, block_a.header.hash
334 );
335 return Ok(());
336 }
337
338 sleep(poll_interval).await;
339 }
340 };
341
342 match timeout(timeout_duration, sync_check).await {
343 Ok(result) => result,
344 Err(_) => Err(eyre::eyre!(
345 "Timeout waiting for nodes {} and {} to sync after {}s",
346 self.node_a,
347 self.node_b,
348 self.timeout_secs
349 )),
350 }
351 })
352 }
353}
354
355#[derive(Debug)]
357pub struct AssertChainTip {
358 pub expected_block_number: u64,
360}
361
362impl AssertChainTip {
363 pub const fn new(expected_block_number: u64) -> Self {
365 Self { expected_block_number }
366 }
367}
368
369impl<Engine> Action<Engine> for AssertChainTip
370where
371 Engine: EngineTypes,
372{
373 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
374 Box::pin(async move {
375 let current_block = env
376 .current_block_info()
377 .ok_or_else(|| eyre::eyre!("No current block information available"))?;
378
379 if current_block.number != self.expected_block_number {
380 return Err(eyre::eyre!(
381 "Expected chain tip to be at block {}, but found block {}",
382 self.expected_block_number,
383 current_block.number
384 ));
385 }
386
387 debug!(
388 "Chain tip verified at block {} (hash: {})",
389 current_block.number, current_block.hash
390 );
391
392 Ok(())
393 })
394 }
395}