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