reth_e2e_test_utils/testsuite/actions/
engine_api.rs1use 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#[derive(Debug)]
19pub struct SendNewPayload<Engine>
20where
21 Engine: EngineTypes,
22{
23 pub node_idx: usize,
25 pub block_number: u64,
27 pub source_node_idx: usize,
29 pub expected_status: ExpectedPayloadStatus,
31 _phantom: PhantomData<Engine>,
32}
33
34#[derive(Debug, Clone)]
36pub enum ExpectedPayloadStatus {
37 Valid,
39 Invalid,
41 SyncingOrAccepted,
43}
44
45impl<Engine> SendNewPayload<Engine>
46where
47 Engine: EngineTypes,
48{
49 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 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, )
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 let payload = block_to_payload_v3(block.clone());
135
136 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, )
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 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#[derive(Debug)]
194pub struct SendNewPayloads<Engine>
195where
196 Engine: EngineTypes,
197{
198 target_node: Option<usize>,
200 source_node: Option<usize>,
202 start_block: Option<u64>,
204 total_blocks: Option<u64>,
206 reverse_order: bool,
208 custom_block_numbers: Option<Vec<u64>>,
210 _phantom: PhantomData<Engine>,
211}
212
213impl<Engine> SendNewPayloads<Engine>
214where
215 Engine: EngineTypes,
216{
217 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 pub const fn with_target_node(mut self, node_idx: usize) -> Self {
232 self.target_node = Some(node_idx);
233 self
234 }
235
236 pub const fn with_source_node(mut self, node_idx: usize) -> Self {
238 self.source_node = Some(node_idx);
239 self
240 }
241
242 pub const fn with_start_block(mut self, block_num: u64) -> Self {
244 self.start_block = Some(block_num);
245 self
246 }
247
248 pub const fn with_total_blocks(mut self, count: u64) -> Self {
250 self.total_blocks = Some(count);
251 self
252 }
253
254 pub const fn in_reverse_order(mut self) -> Self {
256 self.reverse_order = true;
257 self
258 }
259
260 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 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 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 (0..count).map(|i| start + count - 1 - i).collect()
300 } else {
301 (0..count).map(|i| start + i).collect()
303 }
304 };
305
306 for &block_number in &block_numbers {
307 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
331fn 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![], },
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}