reth_e2e_test_utils/testsuite/actions/
engine_api.rs1use 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#[derive(Debug)]
15pub struct SendNewPayload<Engine>
16where
17 Engine: EngineTypes,
18{
19 pub node_idx: usize,
21 pub block_number: u64,
23 pub source_node_idx: usize,
25 pub expected_status: ExpectedPayloadStatus,
27 _phantom: PhantomData<Engine>,
28}
29
30#[derive(Debug, Clone)]
32pub enum ExpectedPayloadStatus {
33 Valid,
35 Invalid,
37 SyncingOrAccepted,
39}
40
41impl<Engine> SendNewPayload<Engine>
42where
43 Engine: EngineTypes,
44{
45 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 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, ),
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 let payload = ExecutionPayloadV3::from_block_unchecked(
127 block.hash(),
128 &block.map_transactions(|tx| tx.inner).into_consensus(),
129 );
130
131 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, ),
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 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#[derive(Debug)]
192pub struct SendNewPayloads<Engine>
193where
194 Engine: EngineTypes,
195{
196 target_node: Option<usize>,
198 source_node: Option<usize>,
200 start_block: Option<u64>,
202 total_blocks: Option<u64>,
204 reverse_order: bool,
206 custom_block_numbers: Option<Vec<u64>>,
208 _phantom: PhantomData<Engine>,
209}
210
211impl<Engine> SendNewPayloads<Engine>
212where
213 Engine: EngineTypes,
214{
215 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 pub const fn with_target_node(mut self, node_idx: usize) -> Self {
230 self.target_node = Some(node_idx);
231 self
232 }
233
234 pub const fn with_source_node(mut self, node_idx: usize) -> Self {
236 self.source_node = Some(node_idx);
237 self
238 }
239
240 pub const fn with_start_block(mut self, block_num: u64) -> Self {
242 self.start_block = Some(block_num);
243 self
244 }
245
246 pub const fn with_total_blocks(mut self, count: u64) -> Self {
248 self.total_blocks = Some(count);
249 self
250 }
251
252 pub const fn in_reverse_order(mut self) -> Self {
254 self.reverse_order = true;
255 self
256 }
257
258 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 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 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 (0..count).map(|i| start + count - 1 - i).collect()
298 } else {
299 (0..count).map(|i| start + i).collect()
301 }
302 };
303
304 for &block_number in &block_numbers {
305 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}