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