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