Skip to main content

reth_e2e_test_utils/testsuite/actions/
node_ops.rs

1//! Node-specific operations for multi-node testing.
2
3use 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/// Action to select which node should be active for subsequent single-node operations.
13#[derive(Debug)]
14pub struct SelectActiveNode {
15    /// Node index to set as active
16    pub node_idx: usize,
17}
18
19impl SelectActiveNode {
20    /// Create a new `SelectActiveNode` action
21    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/// Action to compare chain tips between two nodes.
40#[derive(Debug)]
41pub struct CompareNodeChainTips {
42    /// First node index
43    pub node_a: usize,
44    /// Second node index
45    pub node_b: usize,
46    /// Whether tips should be the same or different
47    pub should_be_equal: bool,
48}
49
50impl CompareNodeChainTips {
51    /// Create a new action expecting nodes to have the same chain tip
52    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    /// Create a new action expecting nodes to have different chain tips
57    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            // Get latest block from each node
76            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/// Action to capture a block with a tag, associating it with a specific node.
132#[derive(Debug)]
133pub struct CaptureBlockOnNode {
134    /// Tag name to associate with the block
135    pub tag: String,
136    /// Node index to capture the block from
137    pub node_idx: usize,
138}
139
140impl CaptureBlockOnNode {
141    /// Create a new `CaptureBlockOnNode` action
142    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/// Action to get a block by tag and verify which node it came from.
171#[derive(Debug)]
172pub struct ValidateBlockTag {
173    /// Tag to look up
174    pub tag: String,
175    /// Expected node index (optional)
176    pub expected_node_idx: Option<usize>,
177}
178
179impl ValidateBlockTag {
180    /// Create a new action to validate a block tag exists
181    pub fn exists(tag: impl Into<String>) -> Self {
182        Self { tag: tag.into(), expected_node_idx: None }
183    }
184
185    /// Create a new action to validate a block tag came from a specific node
186    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/// Action that waits for two nodes to sync and have the same chain tip.
225#[derive(Debug)]
226pub struct WaitForSync {
227    /// First node index
228    pub node_a: usize,
229    /// Second node index
230    pub node_b: usize,
231    /// Maximum time to wait for sync (default: 30 seconds)
232    pub timeout_secs: u64,
233    /// Polling interval (default: 1 second)
234    pub poll_interval_secs: u64,
235}
236
237impl WaitForSync {
238    /// Create a new `WaitForSync` action with default timeouts
239    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    /// Set custom timeout
244    pub const fn with_timeout(mut self, timeout_secs: u64) -> Self {
245        self.timeout_secs = timeout_secs;
246        self
247    }
248
249    /// Set custom poll interval
250    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                    // Get latest block from each node
280                    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/// Action to assert the current chain tip is at a specific block number.
338#[derive(Debug)]
339pub struct AssertChainTip {
340    /// Expected block number
341    pub expected_block_number: u64,
342}
343
344impl AssertChainTip {
345    /// Create a new `AssertChainTip` action
346    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}