1use crate::testsuite::{
4 actions::{expect_fcu_not_syncing_or_accepted, validate_fcu_response, Action, Sequence},
5 BlockInfo, Environment,
6};
7use alloy_primitives::{Bytes, B256};
8use alloy_rpc_types_engine::{
9 payload::ExecutionPayloadEnvelopeV3, ForkchoiceState, PayloadAttributes, PayloadStatusEnum,
10};
11use alloy_rpc_types_eth::{Block, Header, Receipt, Transaction, TransactionRequest};
12use eyre::Result;
13use futures_util::future::BoxFuture;
14use reth_ethereum_primitives::TransactionSigned;
15use reth_node_api::{EngineTypes, PayloadTypes};
16use reth_rpc_api::clients::{EngineApiClient, EthApiClient};
17use std::{collections::HashSet, marker::PhantomData, time::Duration};
18use tokio::time::sleep;
19use tracing::debug;
20
21#[derive(Debug)]
24pub struct AssertMineBlock<Engine>
25where
26 Engine: PayloadTypes,
27{
28 pub node_idx: usize,
30 pub transactions: Vec<Bytes>,
32 pub expected_hash: Option<B256>,
34 pub payload_attributes: Engine::PayloadAttributes,
37 _phantom: PhantomData<Engine>,
39}
40
41impl<Engine> AssertMineBlock<Engine>
42where
43 Engine: PayloadTypes,
44{
45 pub fn new(
47 node_idx: usize,
48 transactions: Vec<Bytes>,
49 expected_hash: Option<B256>,
50 payload_attributes: Engine::PayloadAttributes,
51 ) -> Self {
52 Self {
53 node_idx,
54 transactions,
55 expected_hash,
56 payload_attributes,
57 _phantom: Default::default(),
58 }
59 }
60}
61
62impl<Engine> Action<Engine> for AssertMineBlock<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!("Node index out of bounds: {}", self.node_idx));
70 }
71
72 let node_client = &env.node_clients[self.node_idx];
73 let rpc_client = &node_client.rpc;
74 let engine_client = node_client.engine.http_client();
75
76 let latest_block = EthApiClient::<
78 TransactionRequest,
79 Transaction,
80 Block,
81 Receipt,
82 Header,
83 TransactionSigned,
84 >::block_by_number(
85 rpc_client, alloy_eips::BlockNumberOrTag::Latest, false
86 )
87 .await?;
88
89 let latest_block = latest_block.ok_or_else(|| eyre::eyre!("Latest block not found"))?;
90 let parent_hash = latest_block.header.hash;
91
92 debug!("Latest block hash: {parent_hash}");
93
94 let fork_choice_state = ForkchoiceState {
96 head_block_hash: parent_hash,
97 safe_block_hash: parent_hash,
98 finalized_block_hash: parent_hash,
99 };
100
101 match EngineApiClient::<Engine>::fork_choice_updated_v2(
103 &engine_client,
104 fork_choice_state,
105 Some(self.payload_attributes.clone()),
106 )
107 .await
108 {
109 Ok(fcu_result) => {
110 debug!(?fcu_result, "FCU v2 result");
111 match fcu_result.payload_status.status {
112 PayloadStatusEnum::Valid => {
113 if let Some(payload_id) = fcu_result.payload_id {
114 debug!(id=%payload_id, "Got payload");
115 let _engine_payload = EngineApiClient::<Engine>::get_payload_v2(
116 &engine_client,
117 payload_id,
118 )
119 .await?;
120 Ok(())
121 } else {
122 Err(eyre::eyre!("No payload ID returned from forkchoiceUpdated"))
123 }
124 }
125 _ => Err(eyre::eyre!(
126 "Payload status not valid: {:?}",
127 fcu_result.payload_status
128 ))?,
129 }
130 }
131 Err(_) => {
132 let fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(
134 &engine_client,
135 fork_choice_state,
136 Some(self.payload_attributes.clone()),
137 )
138 .await?;
139
140 debug!(?fcu_result, "FCU v3 result");
141 match fcu_result.payload_status.status {
142 PayloadStatusEnum::Valid => {
143 if let Some(payload_id) = fcu_result.payload_id {
144 debug!(id=%payload_id, "Got payload");
145 let _engine_payload = EngineApiClient::<Engine>::get_payload_v3(
146 &engine_client,
147 payload_id,
148 )
149 .await?;
150 Ok(())
151 } else {
152 Err(eyre::eyre!("No payload ID returned from forkchoiceUpdated"))
153 }
154 }
155 _ => Err(eyre::eyre!(
156 "Payload status not valid: {:?}",
157 fcu_result.payload_status
158 )),
159 }
160 }
161 }
162 })
163 }
164}
165
166#[derive(Debug, Default)]
168pub struct PickNextBlockProducer {}
169
170impl PickNextBlockProducer {
171 pub const fn new() -> Self {
173 Self {}
174 }
175}
176
177impl<Engine> Action<Engine> for PickNextBlockProducer
178where
179 Engine: EngineTypes,
180{
181 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
182 Box::pin(async move {
183 let num_clients = env.node_clients.len();
184 if num_clients == 0 {
185 return Err(eyre::eyre!("No node clients available"));
186 }
187
188 let latest_info = env
189 .current_block_info()
190 .ok_or_else(|| eyre::eyre!("No latest block information available"))?;
191
192 let next_producer_idx = ((latest_info.number + 1) % num_clients as u64) as usize;
194
195 env.last_producer_idx = Some(next_producer_idx);
196 debug!(
197 "Selected node {} as the next block producer for block {}",
198 next_producer_idx,
199 latest_info.number + 1
200 );
201
202 Ok(())
203 })
204 }
205}
206
207#[derive(Debug, Default)]
209pub struct GeneratePayloadAttributes {}
210
211impl<Engine> Action<Engine> for GeneratePayloadAttributes
212where
213 Engine: EngineTypes + PayloadTypes,
214 Engine::PayloadAttributes: From<PayloadAttributes>,
215{
216 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
217 Box::pin(async move {
218 let latest_block = env
219 .current_block_info()
220 .ok_or_else(|| eyre::eyre!("No latest block information available"))?;
221 let block_number = latest_block.number;
222 let timestamp =
223 env.active_node_state()?.latest_header_time + env.block_timestamp_increment;
224 let payload_attributes = PayloadAttributes {
225 timestamp,
226 prev_randao: B256::random(),
227 suggested_fee_recipient: alloy_primitives::Address::random(),
228 withdrawals: Some(vec![]),
229 parent_beacon_block_root: Some(B256::ZERO),
230 slot_number: None,
231 ..Default::default()
232 };
233
234 env.active_node_state_mut()?
235 .payload_attributes
236 .insert(latest_block.number + 1, payload_attributes);
237 debug!("Stored payload attributes for block {}", block_number + 1);
238 Ok(())
239 })
240 }
241}
242
243#[derive(Debug, Default)]
245pub struct GenerateNextPayload {}
246
247impl<Engine> Action<Engine> for GenerateNextPayload
248where
249 Engine: EngineTypes + PayloadTypes,
250 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
251{
252 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
253 Box::pin(async move {
254 let latest_block = env
255 .current_block_info()
256 .ok_or_else(|| eyre::eyre!("No latest block information available"))?;
257
258 let parent_hash = latest_block.hash;
259 debug!("Latest block hash: {parent_hash}");
260
261 let fork_choice_state = ForkchoiceState {
262 head_block_hash: parent_hash,
263 safe_block_hash: parent_hash,
264 finalized_block_hash: parent_hash,
265 };
266
267 let payload_attributes = env
268 .active_node_state()?
269 .payload_attributes
270 .get(&(latest_block.number + 1))
271 .cloned()
272 .ok_or_else(|| eyre::eyre!("No payload attributes found for next block"))?;
273
274 let producer_idx =
275 env.last_producer_idx.ok_or_else(|| eyre::eyre!("No block producer selected"))?;
276
277 let fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(
278 &env.node_clients[producer_idx].engine.http_client(),
279 fork_choice_state,
280 Some(payload_attributes.clone().into()),
281 )
282 .await?;
283
284 debug!("FCU result: {:?}", fcu_result);
285
286 expect_fcu_not_syncing_or_accepted(&fcu_result, "GenerateNextPayload")?;
290
291 let payload_id = if let Some(payload_id) = fcu_result.payload_id {
292 debug!("Received new payload ID: {:?}", payload_id);
293 payload_id
294 } else {
295 debug!("No payload ID returned, generating fresh payload attributes for forking");
296
297 let fresh_payload_attributes = PayloadAttributes {
298 timestamp: env.active_node_state()?.latest_header_time +
299 env.block_timestamp_increment,
300 prev_randao: B256::random(),
301 suggested_fee_recipient: alloy_primitives::Address::random(),
302 withdrawals: Some(vec![]),
303 parent_beacon_block_root: Some(B256::ZERO),
304 slot_number: None,
305 ..Default::default()
306 };
307
308 let fresh_fcu_result = EngineApiClient::<Engine>::fork_choice_updated_v3(
309 &env.node_clients[producer_idx].engine.http_client(),
310 fork_choice_state,
311 Some(fresh_payload_attributes.clone().into()),
312 )
313 .await?;
314
315 debug!("Fresh FCU result: {:?}", fresh_fcu_result);
316
317 expect_fcu_not_syncing_or_accepted(
319 &fresh_fcu_result,
320 "GenerateNextPayload (fresh)",
321 )?;
322
323 if let Some(payload_id) = fresh_fcu_result.payload_id {
324 payload_id
325 } else {
326 debug!("Engine considers the fork base already canonical, skipping payload generation");
327 return Ok(());
328 }
329 };
330
331 env.active_node_state_mut()?.next_payload_id = Some(payload_id);
332
333 sleep(Duration::from_secs(1)).await;
334
335 let built_payload_envelope = EngineApiClient::<Engine>::get_payload_v3(
336 &env.node_clients[producer_idx].engine.http_client(),
337 payload_id,
338 )
339 .await?;
340
341 let built_payload = payload_attributes.clone();
343 env.active_node_state_mut()?
344 .payload_id_history
345 .insert(latest_block.number + 1, payload_id);
346 env.active_node_state_mut()?.latest_payload_built = Some(built_payload);
347 env.active_node_state_mut()?.latest_payload_envelope = Some(built_payload_envelope);
348
349 Ok(())
350 })
351 }
352}
353
354#[derive(Debug, Default)]
356pub struct BroadcastLatestForkchoice {}
357
358impl<Engine> Action<Engine> for BroadcastLatestForkchoice
359where
360 Engine: EngineTypes + PayloadTypes,
361 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
362 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
363{
364 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
365 Box::pin(async move {
366 if env.node_clients.is_empty() {
367 return Err(eyre::eyre!("No node clients available"));
368 }
369
370 let head_hash = if let Some(payload_envelope) =
372 &env.active_node_state()?.latest_payload_envelope
373 {
374 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 =
375 payload_envelope.clone().into();
376 let new_block_hash = execution_payload_envelope
377 .execution_payload
378 .payload_inner
379 .payload_inner
380 .block_hash;
381 debug!("Using newly executed block hash as head: {new_block_hash}");
382 new_block_hash
383 } else {
384 let rpc_client = &env.node_clients[0].rpc;
386 let current_head_block = EthApiClient::<
387 TransactionRequest,
388 Transaction,
389 Block,
390 Receipt,
391 Header,
392 TransactionSigned,
393 >::block_by_number(
394 rpc_client, alloy_eips::BlockNumberOrTag::Latest, false
395 )
396 .await?
397 .ok_or_else(|| eyre::eyre!("No latest block found from RPC"))?;
398 debug!("Using RPC latest block hash as head: {}", current_head_block.header.hash);
399 current_head_block.header.hash
400 };
401
402 let fork_choice_state = ForkchoiceState {
403 head_block_hash: head_hash,
404 safe_block_hash: head_hash,
405 finalized_block_hash: B256::ZERO,
409 };
410 debug!(
411 "Broadcasting forkchoice update to {} clients. Head: {:?}",
412 env.node_clients.len(),
413 fork_choice_state.head_block_hash
414 );
415
416 for (idx, client) in env.node_clients.iter().enumerate() {
417 match EngineApiClient::<Engine>::fork_choice_updated_v3(
418 &client.engine.http_client(),
419 fork_choice_state,
420 None,
421 )
422 .await
423 {
424 Ok(resp) => {
425 debug!(
426 "Client {}: Forkchoice update status: {:?}",
427 idx, resp.payload_status.status
428 );
429 validate_fcu_response(&resp, &format!("Client {idx}"))?;
431 }
432 Err(err) => {
433 return Err(eyre::eyre!(
434 "Client {}: Failed to broadcast forkchoice: {:?}",
435 idx,
436 err
437 ));
438 }
439 }
440 }
441 debug!("Forkchoice update broadcasted successfully");
442 Ok(())
443 })
444 }
445}
446
447#[derive(Debug, Default)]
453pub struct UpdateBlockInfo {}
454
455impl<Engine> Action<Engine> for UpdateBlockInfo
456where
457 Engine: EngineTypes,
458{
459 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
460 Box::pin(async move {
461 let rpc_client = &env.node_clients[0].rpc;
463 let latest_block = EthApiClient::<
464 TransactionRequest,
465 Transaction,
466 Block,
467 Receipt,
468 Header,
469 TransactionSigned,
470 >::block_by_number(
471 rpc_client, alloy_eips::BlockNumberOrTag::Latest, false
472 )
473 .await?
474 .ok_or_else(|| eyre::eyre!("No latest block found from RPC"))?;
475
476 env.set_current_block_info(BlockInfo {
478 hash: latest_block.header.hash,
479 number: latest_block.header.number,
480 timestamp: latest_block.header.timestamp,
481 })?;
482
483 env.active_node_state_mut()?.latest_header_time = latest_block.header.timestamp;
484 env.active_node_state_mut()?.latest_fork_choice_state.head_block_hash =
485 latest_block.header.hash;
486
487 debug!(
488 "Updated environment to block {} (hash: {})",
489 latest_block.header.number, latest_block.header.hash
490 );
491
492 Ok(())
493 })
494 }
495}
496
497#[derive(Debug, Default)]
503pub struct UpdateBlockInfoToLatestPayload {}
504
505impl<Engine> Action<Engine> for UpdateBlockInfoToLatestPayload
506where
507 Engine: EngineTypes + PayloadTypes,
508 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
509{
510 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
511 Box::pin(async move {
512 let payload_envelope = env
513 .active_node_state()?
514 .latest_payload_envelope
515 .as_ref()
516 .ok_or_else(|| eyre::eyre!("No execution payload envelope available"))?;
517
518 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 =
519 payload_envelope.clone().into();
520 let execution_payload = execution_payload_envelope.execution_payload;
521
522 let block_hash = execution_payload.payload_inner.payload_inner.block_hash;
523 let block_number = execution_payload.payload_inner.payload_inner.block_number;
524 let block_timestamp = execution_payload.payload_inner.payload_inner.timestamp;
525
526 env.set_current_block_info(BlockInfo {
528 hash: block_hash,
529 number: block_number,
530 timestamp: block_timestamp,
531 })?;
532
533 env.active_node_state_mut()?.latest_header_time = block_timestamp;
534 env.active_node_state_mut()?.latest_fork_choice_state.head_block_hash = block_hash;
535
536 debug!(
537 "Updated environment to newly produced block {} (hash: {})",
538 block_number, block_hash
539 );
540
541 Ok(())
542 })
543 }
544}
545
546#[derive(Debug, Default)]
548pub struct CheckPayloadAccepted {}
549
550impl<Engine> Action<Engine> for CheckPayloadAccepted
551where
552 Engine: EngineTypes,
553 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
554{
555 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
556 Box::pin(async move {
557 let mut accepted_check: bool = false;
558
559 let latest_block = env
560 .current_block_info()
561 .ok_or_else(|| eyre::eyre!("No latest block information available"))?;
562
563 let payload_id = *env
564 .active_node_state()?
565 .payload_id_history
566 .get(&(latest_block.number + 1))
567 .ok_or_else(|| eyre::eyre!("Cannot find payload_id"))?;
568
569 let node_clients = env.node_clients.clone();
570 for (idx, client) in node_clients.iter().enumerate() {
571 let rpc_client = &client.rpc;
572
573 let rpc_latest_header = EthApiClient::<
575 TransactionRequest,
576 Transaction,
577 Block,
578 Receipt,
579 Header,
580 TransactionSigned,
581 >::header_by_number(
582 rpc_client, alloy_eips::BlockNumberOrTag::Latest
583 )
584 .await?
585 .ok_or_else(|| eyre::eyre!("No latest header found from rpc"))?;
586
587 let next_new_payload = env
589 .active_node_state()?
590 .latest_payload_built
591 .as_ref()
592 .ok_or_else(|| eyre::eyre!("No next built payload found"))?;
593
594 let built_payload = EngineApiClient::<Engine>::get_payload_v3(
595 &client.engine.http_client(),
596 payload_id,
597 )
598 .await?;
599
600 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 = built_payload.into();
601 let new_payload_block_hash = execution_payload_envelope
602 .execution_payload
603 .payload_inner
604 .payload_inner
605 .block_hash;
606
607 if rpc_latest_header.hash != new_payload_block_hash {
608 debug!(
609 "Client {}: The hash is not matched: {:?} {:?}",
610 idx, rpc_latest_header.hash, new_payload_block_hash
611 );
612 continue;
613 }
614
615 if rpc_latest_header.inner.difficulty != alloy_primitives::U256::ZERO {
616 debug!(
617 "Client {}: difficulty != 0: {:?}",
618 idx, rpc_latest_header.inner.difficulty
619 );
620 continue;
621 }
622
623 if rpc_latest_header.inner.mix_hash != next_new_payload.prev_randao {
624 debug!(
625 "Client {}: The mix_hash and prev_randao is not same: {:?} {:?}",
626 idx, rpc_latest_header.inner.mix_hash, next_new_payload.prev_randao
627 );
628 continue;
629 }
630
631 let extra_len = rpc_latest_header.inner.extra_data.len();
632 if extra_len <= 32 {
633 debug!("Client {}: extra_len is fewer than 32. extra_len: {}", idx, extra_len);
634 continue;
635 }
636
637 if !accepted_check {
639 accepted_check = true;
640 env.set_current_block_info(BlockInfo {
642 hash: rpc_latest_header.hash,
643 number: rpc_latest_header.inner.number,
644 timestamp: rpc_latest_header.inner.timestamp,
645 })?;
646
647 env.active_node_state_mut()?.latest_header_time =
650 rpc_latest_header.inner.timestamp;
651 env.active_node_state_mut()?.latest_fork_choice_state.head_block_hash =
652 rpc_latest_header.hash;
653 }
654 }
655
656 if accepted_check {
657 Ok(())
658 } else {
659 Err(eyre::eyre!("No clients passed payload acceptance checks"))
660 }
661 })
662 }
663}
664
665#[derive(Debug, Default)]
667pub struct BroadcastNextNewPayload {
668 active_node_only: bool,
670}
671
672impl BroadcastNextNewPayload {
673 pub const fn with_active_node() -> Self {
675 Self { active_node_only: true }
676 }
677}
678
679impl<Engine> Action<Engine> for BroadcastNextNewPayload
680where
681 Engine: EngineTypes + PayloadTypes,
682 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
683 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
684{
685 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
686 Box::pin(async move {
687 let next_new_payload = env
689 .active_node_state()?
690 .latest_payload_built
691 .as_ref()
692 .ok_or_else(|| eyre::eyre!("No next built payload found"))?
693 .clone();
694 let parent_beacon_block_root = next_new_payload
695 .parent_beacon_block_root
696 .ok_or_else(|| eyre::eyre!("No parent beacon block root for next new payload"))?;
697
698 let payload_envelope = env
699 .active_node_state()?
700 .latest_payload_envelope
701 .as_ref()
702 .ok_or_else(|| eyre::eyre!("No execution payload envelope available"))?
703 .clone();
704
705 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 = payload_envelope.into();
706 let execution_payload = execution_payload_envelope.execution_payload;
707
708 if self.active_node_only {
709 let active_idx = env.active_node_idx;
711 let engine = env.node_clients[active_idx].engine.http_client();
712
713 let result = EngineApiClient::<Engine>::new_payload_v3(
714 &engine,
715 execution_payload.clone(),
716 vec![],
717 parent_beacon_block_root,
718 )
719 .await?;
720
721 debug!("Active node {}: new_payload status: {:?}", active_idx, result.status);
722
723 match result.status {
725 PayloadStatusEnum::Valid => {
726 env.active_node_state_mut()?.latest_payload_executed =
727 Some(next_new_payload);
728 Ok(())
729 }
730 other => Err(eyre::eyre!(
731 "Active node {}: Unexpected payload status: {:?}",
732 active_idx,
733 other
734 )),
735 }
736 } else {
737 let mut broadcast_results = Vec::new();
739 let mut first_valid_seen = false;
740
741 for (idx, client) in env.node_clients.iter().enumerate() {
742 let engine = client.engine.http_client();
743
744 let result = EngineApiClient::<Engine>::new_payload_v3(
746 &engine,
747 execution_payload.clone(),
748 vec![],
749 parent_beacon_block_root,
750 )
751 .await?;
752
753 broadcast_results.push((idx, result.status.clone()));
754 debug!("Node {}: new_payload broadcast status: {:?}", idx, result.status);
755
756 if result.status == PayloadStatusEnum::Valid && !first_valid_seen {
758 first_valid_seen = true;
759 } else if let PayloadStatusEnum::Invalid { validation_error } = result.status {
760 debug!(
761 "Node {}: Invalid payload status returned from broadcast: {:?}",
762 idx, validation_error
763 );
764 }
765 }
766
767 if first_valid_seen {
769 env.active_node_state_mut()?.latest_payload_executed = Some(next_new_payload);
770 }
771
772 let any_valid =
774 broadcast_results.iter().any(|(_, status)| *status == PayloadStatusEnum::Valid);
775 if !any_valid {
776 return Err(eyre::eyre!(
777 "Failed to successfully broadcast payload to any client"
778 ));
779 }
780
781 debug!("Broadcast complete. Results: {:?}", broadcast_results);
782
783 Ok(())
784 }
785 })
786 }
787}
788
789#[derive(Debug)]
791pub struct ProduceBlocks<Engine> {
792 pub num_blocks: u64,
794 _phantom: PhantomData<Engine>,
796}
797
798impl<Engine> ProduceBlocks<Engine> {
799 pub fn new(num_blocks: u64) -> Self {
801 Self { num_blocks, _phantom: Default::default() }
802 }
803}
804
805impl<Engine> Default for ProduceBlocks<Engine> {
806 fn default() -> Self {
807 Self::new(0)
808 }
809}
810
811impl<Engine> Action<Engine> for ProduceBlocks<Engine>
812where
813 Engine: EngineTypes + PayloadTypes,
814 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
815 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
816{
817 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
818 Box::pin(async move {
819 for _ in 0..self.num_blocks {
820 let mut sequence = Sequence::new(vec![
824 Box::new(PickNextBlockProducer::default()),
825 Box::new(GeneratePayloadAttributes::default()),
826 Box::new(GenerateNextPayload::default()),
827 Box::new(BroadcastNextNewPayload::default()),
828 Box::new(UpdateBlockInfoToLatestPayload::default()),
829 ]);
830 sequence.execute(env).await?;
831 }
832 Ok(())
833 })
834 }
835}
836
837#[derive(Debug)]
839pub struct TestFcuToTag {
840 pub tag: String,
842 pub expected_status: PayloadStatusEnum,
844}
845
846impl TestFcuToTag {
847 pub fn new(tag: impl Into<String>, expected_status: PayloadStatusEnum) -> Self {
849 Self { tag: tag.into(), expected_status }
850 }
851}
852
853impl<Engine> Action<Engine> for TestFcuToTag
854where
855 Engine: EngineTypes,
856{
857 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
858 Box::pin(async move {
859 let (target_block, _node_idx) = env
861 .block_registry
862 .get(&self.tag)
863 .copied()
864 .ok_or_else(|| eyre::eyre!("Block tag '{}' not found in registry", self.tag))?;
865
866 let engine_client = env.node_clients[0].engine.http_client();
867 let fcu_state = ForkchoiceState {
868 head_block_hash: target_block.hash,
869 safe_block_hash: target_block.hash,
870 finalized_block_hash: target_block.hash,
871 };
872
873 let fcu_response =
874 EngineApiClient::<Engine>::fork_choice_updated_v2(&engine_client, fcu_state, None)
875 .await?;
876
877 match (&fcu_response.payload_status.status, &self.expected_status) {
879 (PayloadStatusEnum::Valid, PayloadStatusEnum::Valid) => {
880 debug!("FCU to '{}' returned VALID as expected", self.tag);
881 }
882 (PayloadStatusEnum::Invalid { .. }, PayloadStatusEnum::Invalid { .. }) => {
883 debug!("FCU to '{}' returned INVALID as expected", self.tag);
884 }
885 (PayloadStatusEnum::Syncing, PayloadStatusEnum::Syncing) => {
886 debug!("FCU to '{}' returned SYNCING as expected", self.tag);
887 }
888 (PayloadStatusEnum::Accepted, PayloadStatusEnum::Accepted) => {
889 debug!("FCU to '{}' returned ACCEPTED as expected", self.tag);
890 }
891 (actual, expected) => {
892 return Err(eyre::eyre!(
893 "FCU to '{}': expected status {:?}, but got {:?}",
894 self.tag,
895 expected,
896 actual
897 ));
898 }
899 }
900
901 Ok(())
902 })
903 }
904}
905
906#[derive(Debug)]
908pub struct ExpectFcuStatus {
909 pub target_tag: String,
911 pub expected_status: PayloadStatusEnum,
913}
914
915impl ExpectFcuStatus {
916 pub fn valid(target_tag: impl Into<String>) -> Self {
918 Self { target_tag: target_tag.into(), expected_status: PayloadStatusEnum::Valid }
919 }
920
921 pub fn invalid(target_tag: impl Into<String>) -> Self {
923 Self {
924 target_tag: target_tag.into(),
925 expected_status: PayloadStatusEnum::Invalid {
926 validation_error: "corrupted block".to_string(),
927 },
928 }
929 }
930
931 pub fn syncing(target_tag: impl Into<String>) -> Self {
933 Self { target_tag: target_tag.into(), expected_status: PayloadStatusEnum::Syncing }
934 }
935
936 pub fn accepted(target_tag: impl Into<String>) -> Self {
938 Self { target_tag: target_tag.into(), expected_status: PayloadStatusEnum::Accepted }
939 }
940}
941
942impl<Engine> Action<Engine> for ExpectFcuStatus
943where
944 Engine: EngineTypes,
945{
946 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
947 Box::pin(async move {
948 let mut test_fcu = TestFcuToTag::new(&self.target_tag, self.expected_status.clone());
949 test_fcu.execute(env).await
950 })
951 }
952}
953
954#[derive(Debug)]
956pub struct ValidateCanonicalTag {
957 pub tag: String,
959}
960
961impl ValidateCanonicalTag {
962 pub fn new(tag: impl Into<String>) -> Self {
964 Self { tag: tag.into() }
965 }
966}
967
968impl<Engine> Action<Engine> for ValidateCanonicalTag
969where
970 Engine: EngineTypes,
971{
972 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
973 Box::pin(async move {
974 let mut expect_valid = ExpectFcuStatus::valid(&self.tag);
975 expect_valid.execute(env).await?;
976
977 debug!("Successfully validated that '{}' remains canonical", self.tag);
978 Ok(())
979 })
980 }
981}
982
983#[derive(Debug)]
986pub struct ProduceBlocksLocally<Engine> {
987 pub num_blocks: u64,
989 _phantom: PhantomData<Engine>,
991}
992
993impl<Engine> ProduceBlocksLocally<Engine> {
994 pub fn new(num_blocks: u64) -> Self {
996 Self { num_blocks, _phantom: Default::default() }
997 }
998}
999
1000impl<Engine> Default for ProduceBlocksLocally<Engine> {
1001 fn default() -> Self {
1002 Self::new(0)
1003 }
1004}
1005
1006impl<Engine> Action<Engine> for ProduceBlocksLocally<Engine>
1007where
1008 Engine: EngineTypes + PayloadTypes,
1009 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
1010 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
1011{
1012 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
1013 Box::pin(async move {
1014 let producer_idx = env.active_node_idx;
1016
1017 for _ in 0..self.num_blocks {
1018 env.last_producer_idx = Some(producer_idx);
1020
1021 let mut sequence = Sequence::new(vec![
1023 Box::new(GeneratePayloadAttributes::default()),
1025 Box::new(GenerateNextPayload::default()),
1026 Box::new(BroadcastNextNewPayload::with_active_node()),
1028 Box::new(UpdateBlockInfoToLatestPayload::default()),
1029 ]);
1030 sequence.execute(env).await?;
1031 }
1032 Ok(())
1033 })
1034 }
1035}
1036
1037#[derive(Debug)]
1039pub struct ProduceInvalidBlocks<Engine> {
1040 pub num_blocks: u64,
1042 pub invalid_indices: HashSet<u64>,
1044 _phantom: PhantomData<Engine>,
1046}
1047
1048impl<Engine> ProduceInvalidBlocks<Engine> {
1049 pub fn new(num_blocks: u64, invalid_indices: HashSet<u64>) -> Self {
1051 Self { num_blocks, invalid_indices, _phantom: Default::default() }
1052 }
1053
1054 pub fn with_invalid_at(num_blocks: u64, invalid_index: u64) -> Self {
1057 let mut invalid_indices = HashSet::new();
1058 invalid_indices.insert(invalid_index);
1059 Self::new(num_blocks, invalid_indices)
1060 }
1061}
1062
1063impl<Engine> Action<Engine> for ProduceInvalidBlocks<Engine>
1064where
1065 Engine: EngineTypes + PayloadTypes,
1066 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
1067 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
1068{
1069 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
1070 Box::pin(async move {
1071 for block_index in 0..self.num_blocks {
1072 let is_invalid = self.invalid_indices.contains(&block_index);
1073
1074 if is_invalid {
1075 debug!("Producing invalid block at index {}", block_index);
1076
1077 let mut sequence = Sequence::new(vec![
1079 Box::new(PickNextBlockProducer::default()),
1080 Box::new(GeneratePayloadAttributes::default()),
1081 Box::new(GenerateNextPayload::default()),
1082 ]);
1083 sequence.execute(env).await?;
1084
1085 let latest_envelope =
1087 env.active_node_state()?.latest_payload_envelope.as_ref().ok_or_else(
1088 || eyre::eyre!("No payload envelope available to corrupt"),
1089 )?;
1090
1091 let envelope_v3: ExecutionPayloadEnvelopeV3 = latest_envelope.clone().into();
1092 let mut corrupted_payload = envelope_v3.execution_payload;
1093
1094 corrupted_payload.payload_inner.payload_inner.state_root = B256::random();
1096
1097 debug!(
1098 "Corrupted state root for block {} to: {}",
1099 block_index, corrupted_payload.payload_inner.payload_inner.state_root
1100 );
1101
1102 let engine_client = env.node_clients[0].engine.http_client();
1104 let versioned_hashes = Vec::new();
1106 let parent_beacon_block_root = B256::random();
1108
1109 let new_payload_response = EngineApiClient::<Engine>::new_payload_v3(
1110 &engine_client,
1111 corrupted_payload.clone(),
1112 versioned_hashes,
1113 parent_beacon_block_root,
1114 )
1115 .await?;
1116
1117 match new_payload_response.status {
1119 PayloadStatusEnum::Invalid { validation_error } => {
1120 debug!(
1121 "Block {} correctly rejected as invalid: {:?}",
1122 block_index, validation_error
1123 );
1124 }
1125 other_status => {
1126 return Err(eyre::eyre!(
1127 "Expected block {} to be rejected as INVALID, but got: {:?}",
1128 block_index,
1129 other_status
1130 ));
1131 }
1132 }
1133
1134 env.set_current_block_info(BlockInfo {
1136 hash: corrupted_payload.payload_inner.payload_inner.block_hash,
1137 number: corrupted_payload.payload_inner.payload_inner.block_number,
1138 timestamp: corrupted_payload.timestamp(),
1139 })?;
1140 } else {
1141 debug!("Producing valid block at index {}", block_index);
1142
1143 let mut sequence = Sequence::new(vec![
1145 Box::new(PickNextBlockProducer::default()),
1146 Box::new(GeneratePayloadAttributes::default()),
1147 Box::new(GenerateNextPayload::default()),
1148 Box::new(BroadcastNextNewPayload::default()),
1149 Box::new(UpdateBlockInfoToLatestPayload::default()),
1150 ]);
1151 sequence.execute(env).await?;
1152 }
1153 }
1154 Ok(())
1155 })
1156 }
1157}