Skip to main content

reth_e2e_test_utils/testsuite/actions/
engine_api.rs

1//! Engine API specific actions for testing.
2
3use crate::testsuite::{Action, Environment};
4use alloy_primitives::B256;
5use alloy_rpc_types_engine::{ExecutionPayloadV3, PayloadStatus, PayloadStatusEnum};
6use alloy_rpc_types_eth::Block;
7use eyre::Result;
8use futures_util::future::BoxFuture;
9use reth_node_api::EngineTypes;
10use std::marker::PhantomData;
11use tracing::debug;
12
13/// Action that sends a newPayload request to a specific node.
14#[derive(Debug)]
15pub struct SendNewPayload<Engine>
16where
17    Engine: EngineTypes,
18{
19    /// The node index to send to
20    pub node_idx: usize,
21    /// The block number to send
22    pub block_number: u64,
23    /// The source node to get the block from
24    pub source_node_idx: usize,
25    /// Expected payload status
26    pub expected_status: ExpectedPayloadStatus,
27    _phantom: PhantomData<Engine>,
28}
29
30/// Expected status for a payload
31#[derive(Debug, Clone)]
32pub enum ExpectedPayloadStatus {
33    /// Expect the payload to be valid
34    Valid,
35    /// Expect the payload to be invalid
36    Invalid,
37    /// Expect the payload to be syncing or accepted (buffered)
38    SyncingOrAccepted,
39}
40
41impl<Engine> SendNewPayload<Engine>
42where
43    Engine: EngineTypes,
44{
45    /// Create a new `SendNewPayload` action
46    pub fn new(
47        node_idx: usize,
48        block_number: u64,
49        source_node_idx: usize,
50        expected_status: ExpectedPayloadStatus,
51    ) -> Self {
52        Self {
53            node_idx,
54            block_number,
55            source_node_idx,
56            expected_status,
57            _phantom: Default::default(),
58        }
59    }
60}
61
62impl<Engine> Action<Engine> for SendNewPayload<Engine>
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_idx >= env.node_clients.len() {
69                return Err(eyre::eyre!("Target node index out of bounds: {}", self.node_idx));
70            }
71            if self.source_node_idx >= env.node_clients.len() {
72                return Err(eyre::eyre!(
73                    "Source node index out of bounds: {}",
74                    self.source_node_idx
75                ));
76            }
77
78            // Get the block from the source node with retries
79            let source_rpc = &env.node_clients[self.source_node_idx].rpc;
80            let mut block = None;
81            let mut retries = 0;
82            const MAX_RETRIES: u32 = 5;
83
84            while retries < MAX_RETRIES {
85                match source_rpc
86                    .request::<_, Option<Block>>(
87                        "eth_getBlockByNumber",
88                        (
89                            alloy_eips::BlockNumberOrTag::Number(self.block_number),
90                            true, // include transactions
91                        ),
92                    )
93                    .await
94                {
95                    Ok(Some(b)) => {
96                        block = Some(b);
97                        break;
98                    }
99                    Ok(None) => {
100                        debug!(
101                            "Block {} not found on source node {} (attempt {}/{})",
102                            self.block_number,
103                            self.source_node_idx,
104                            retries + 1,
105                            MAX_RETRIES
106                        );
107                        retries += 1;
108                        if retries < MAX_RETRIES {
109                            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
110                        }
111                    }
112                    Err(e) => return Err(e.into()),
113                }
114            }
115
116            let block = block.ok_or_else(|| {
117                eyre::eyre!(
118                    "Block {} not found on source node {} after {} retries",
119                    self.block_number,
120                    self.source_node_idx,
121                    MAX_RETRIES
122                )
123            })?;
124
125            // Convert block to ExecutionPayloadV3
126            let payload = ExecutionPayloadV3::from_block_unchecked(
127                block.hash(),
128                &block.map_transactions(|tx| tx.inner).into_consensus(),
129            );
130
131            // Send the payload to the target node
132            let target_engine = env.node_clients[self.node_idx].engine.http_client();
133            let result = target_engine
134                .request::<_, PayloadStatus>(
135                    "engine_newPayloadV3",
136                    (
137                        payload,
138                        Vec::<B256>::new(),
139                        B256::ZERO, // parent_beacon_block_root
140                    ),
141                )
142                .await?;
143
144            debug!(
145                "Node {}: new_payload for block {} response - status: {:?}, latest_valid_hash: {:?}",
146                self.node_idx, self.block_number, result.status, result.latest_valid_hash
147            );
148
149            // Validate the response based on expectations
150            match (&result.status, &self.expected_status) {
151                (PayloadStatusEnum::Valid, ExpectedPayloadStatus::Valid) => {
152                    debug!(
153                        "Node {}: Block {} marked as VALID as expected",
154                        self.node_idx, self.block_number
155                    );
156                    Ok(())
157                }
158                (
159                    PayloadStatusEnum::Invalid { validation_error },
160                    ExpectedPayloadStatus::Invalid,
161                ) => {
162                    debug!(
163                        "Node {}: Block {} marked as INVALID as expected: {:?}",
164                        self.node_idx, self.block_number, validation_error
165                    );
166                    Ok(())
167                }
168                (
169                    PayloadStatusEnum::Syncing | PayloadStatusEnum::Accepted,
170                    ExpectedPayloadStatus::SyncingOrAccepted,
171                ) => {
172                    debug!(
173                        "Node {}: Block {} marked as SYNCING/ACCEPTED as expected (buffered)",
174                        self.node_idx, self.block_number
175                    );
176                    Ok(())
177                }
178                (status, expected) => Err(eyre::eyre!(
179                    "Node {}: Unexpected payload status for block {}. Got {:?}, expected {:?}",
180                    self.node_idx,
181                    self.block_number,
182                    status,
183                    expected
184                )),
185            }
186        })
187    }
188}
189
190/// Action that sends multiple blocks to a node in a specific order.
191#[derive(Debug)]
192pub struct SendNewPayloads<Engine>
193where
194    Engine: EngineTypes,
195{
196    /// The node index to send to
197    target_node: Option<usize>,
198    /// The source node to get the blocks from
199    source_node: Option<usize>,
200    /// The starting block number
201    start_block: Option<u64>,
202    /// The total number of blocks to send
203    total_blocks: Option<u64>,
204    /// Whether to send in reverse order
205    reverse_order: bool,
206    /// Custom block numbers to send (if not using `start_block` + `total_blocks`)
207    custom_block_numbers: Option<Vec<u64>>,
208    _phantom: PhantomData<Engine>,
209}
210
211impl<Engine> SendNewPayloads<Engine>
212where
213    Engine: EngineTypes,
214{
215    /// Create a new `SendNewPayloads` action builder
216    pub fn new() -> Self {
217        Self {
218            target_node: None,
219            source_node: None,
220            start_block: None,
221            total_blocks: None,
222            reverse_order: false,
223            custom_block_numbers: None,
224            _phantom: Default::default(),
225        }
226    }
227
228    /// Set the target node index
229    pub const fn with_target_node(mut self, node_idx: usize) -> Self {
230        self.target_node = Some(node_idx);
231        self
232    }
233
234    /// Set the source node index
235    pub const fn with_source_node(mut self, node_idx: usize) -> Self {
236        self.source_node = Some(node_idx);
237        self
238    }
239
240    /// Set the starting block number
241    pub const fn with_start_block(mut self, block_num: u64) -> Self {
242        self.start_block = Some(block_num);
243        self
244    }
245
246    /// Set the total number of blocks to send
247    pub const fn with_total_blocks(mut self, count: u64) -> Self {
248        self.total_blocks = Some(count);
249        self
250    }
251
252    /// Send blocks in reverse order (useful for testing buffering)
253    pub const fn in_reverse_order(mut self) -> Self {
254        self.reverse_order = true;
255        self
256    }
257
258    /// Set custom block numbers to send
259    pub fn with_block_numbers(mut self, block_numbers: Vec<u64>) -> Self {
260        self.custom_block_numbers = Some(block_numbers);
261        self
262    }
263}
264
265impl<Engine> Default for SendNewPayloads<Engine>
266where
267    Engine: EngineTypes,
268{
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl<Engine> Action<Engine> for SendNewPayloads<Engine>
275where
276    Engine: EngineTypes,
277{
278    fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
279        Box::pin(async move {
280            // Validate required fields
281            let target_node =
282                self.target_node.ok_or_else(|| eyre::eyre!("Target node not specified"))?;
283            let source_node =
284                self.source_node.ok_or_else(|| eyre::eyre!("Source node not specified"))?;
285
286            // Determine block numbers to send
287            let block_numbers = if let Some(custom_numbers) = &self.custom_block_numbers {
288                custom_numbers.clone()
289            } else {
290                let start =
291                    self.start_block.ok_or_else(|| eyre::eyre!("Start block not specified"))?;
292                let count =
293                    self.total_blocks.ok_or_else(|| eyre::eyre!("Total blocks not specified"))?;
294
295                if self.reverse_order {
296                    // Send blocks in reverse order (e.g., for count=2, start=1: [2, 1])
297                    (0..count).map(|i| start + count - 1 - i).collect()
298                } else {
299                    // Send blocks in normal order
300                    (0..count).map(|i| start + i).collect()
301                }
302            };
303
304            for &block_number in &block_numbers {
305                // For the first block in reverse order, expect buffering
306                // For subsequent blocks, they might connect immediately
307                let expected_status =
308                    if self.reverse_order && block_number == *block_numbers.first().unwrap() {
309                        ExpectedPayloadStatus::SyncingOrAccepted
310                    } else {
311                        ExpectedPayloadStatus::Valid
312                    };
313
314                let mut action = SendNewPayload::<Engine>::new(
315                    target_node,
316                    block_number,
317                    source_node,
318                    expected_status,
319                );
320
321                action.execute(env).await?;
322            }
323
324            Ok(())
325        })
326    }
327}