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, PayloadKind, 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 if let Some(builder) = &env.node_clients[producer_idx].payload_builder {
334 tokio::time::timeout(
336 Duration::from_secs(30),
337 builder.resolve_kind(payload_id, PayloadKind::WaitForPending),
338 )
339 .await?
340 .ok_or_else(|| eyre::eyre!("Unknown payload {payload_id}"))??;
341 } else {
342 sleep(Duration::from_secs(1)).await;
344 }
345
346 let built_payload_envelope = EngineApiClient::<Engine>::get_payload_v3(
347 &env.node_clients[producer_idx].engine.http_client(),
348 payload_id,
349 )
350 .await?;
351
352 let built_payload = payload_attributes.clone();
354 env.active_node_state_mut()?
355 .payload_id_history
356 .insert(latest_block.number + 1, payload_id);
357 env.active_node_state_mut()?.latest_payload_built = Some(built_payload);
358 env.active_node_state_mut()?.latest_payload_envelope = Some(built_payload_envelope);
359
360 Ok(())
361 })
362 }
363}
364
365#[derive(Debug, Default)]
367pub struct BroadcastLatestForkchoice {}
368
369impl<Engine> Action<Engine> for BroadcastLatestForkchoice
370where
371 Engine: EngineTypes + PayloadTypes,
372 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
373 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
374{
375 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
376 Box::pin(async move {
377 if env.node_clients.is_empty() {
378 return Err(eyre::eyre!("No node clients available"));
379 }
380
381 let head_hash = if let Some(payload_envelope) =
383 &env.active_node_state()?.latest_payload_envelope
384 {
385 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 =
386 payload_envelope.clone().into();
387 let new_block_hash = execution_payload_envelope
388 .execution_payload
389 .payload_inner
390 .payload_inner
391 .block_hash;
392 debug!("Using newly executed block hash as head: {new_block_hash}");
393 new_block_hash
394 } else {
395 let rpc_client = &env.node_clients[0].rpc;
397 let current_head_block = EthApiClient::<
398 TransactionRequest,
399 Transaction,
400 Block,
401 Receipt,
402 Header,
403 TransactionSigned,
404 >::block_by_number(
405 rpc_client, alloy_eips::BlockNumberOrTag::Latest, false
406 )
407 .await?
408 .ok_or_else(|| eyre::eyre!("No latest block found from RPC"))?;
409 debug!("Using RPC latest block hash as head: {}", current_head_block.header.hash);
410 current_head_block.header.hash
411 };
412
413 let fork_choice_state = ForkchoiceState {
414 head_block_hash: head_hash,
415 safe_block_hash: head_hash,
416 finalized_block_hash: B256::ZERO,
420 };
421 debug!(
422 "Broadcasting forkchoice update to {} clients. Head: {:?}",
423 env.node_clients.len(),
424 fork_choice_state.head_block_hash
425 );
426
427 for (idx, client) in env.node_clients.iter().enumerate() {
428 match EngineApiClient::<Engine>::fork_choice_updated_v3(
429 &client.engine.http_client(),
430 fork_choice_state,
431 None,
432 )
433 .await
434 {
435 Ok(resp) => {
436 debug!(
437 "Client {}: Forkchoice update status: {:?}",
438 idx, resp.payload_status.status
439 );
440 validate_fcu_response(&resp, &format!("Client {idx}"))?;
442 }
443 Err(err) => {
444 return Err(eyre::eyre!(
445 "Client {}: Failed to broadcast forkchoice: {:?}",
446 idx,
447 err
448 ));
449 }
450 }
451 }
452 debug!("Forkchoice update broadcasted successfully");
453 Ok(())
454 })
455 }
456}
457
458#[derive(Debug, Default)]
464pub struct UpdateBlockInfo {}
465
466impl<Engine> Action<Engine> for UpdateBlockInfo
467where
468 Engine: EngineTypes,
469{
470 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
471 Box::pin(async move {
472 let rpc_client = &env.node_clients[0].rpc;
474 let latest_block = EthApiClient::<
475 TransactionRequest,
476 Transaction,
477 Block,
478 Receipt,
479 Header,
480 TransactionSigned,
481 >::block_by_number(
482 rpc_client, alloy_eips::BlockNumberOrTag::Latest, false
483 )
484 .await?
485 .ok_or_else(|| eyre::eyre!("No latest block found from RPC"))?;
486
487 env.set_current_block_info(BlockInfo {
489 hash: latest_block.header.hash,
490 number: latest_block.header.number,
491 timestamp: latest_block.header.timestamp,
492 })?;
493
494 env.active_node_state_mut()?.latest_header_time = latest_block.header.timestamp;
495 env.active_node_state_mut()?.latest_fork_choice_state.head_block_hash =
496 latest_block.header.hash;
497
498 debug!(
499 "Updated environment to block {} (hash: {})",
500 latest_block.header.number, latest_block.header.hash
501 );
502
503 Ok(())
504 })
505 }
506}
507
508#[derive(Debug, Default)]
514pub struct UpdateBlockInfoToLatestPayload {}
515
516impl<Engine> Action<Engine> for UpdateBlockInfoToLatestPayload
517where
518 Engine: EngineTypes + PayloadTypes,
519 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
520{
521 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
522 Box::pin(async move {
523 let payload_envelope = env
524 .active_node_state()?
525 .latest_payload_envelope
526 .as_ref()
527 .ok_or_else(|| eyre::eyre!("No execution payload envelope available"))?;
528
529 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 =
530 payload_envelope.clone().into();
531 let execution_payload = execution_payload_envelope.execution_payload;
532
533 let block_hash = execution_payload.payload_inner.payload_inner.block_hash;
534 let block_number = execution_payload.payload_inner.payload_inner.block_number;
535 let block_timestamp = execution_payload.payload_inner.payload_inner.timestamp;
536
537 env.set_current_block_info(BlockInfo {
539 hash: block_hash,
540 number: block_number,
541 timestamp: block_timestamp,
542 })?;
543
544 env.active_node_state_mut()?.latest_header_time = block_timestamp;
545 env.active_node_state_mut()?.latest_fork_choice_state.head_block_hash = block_hash;
546
547 debug!(
548 "Updated environment to newly produced block {} (hash: {})",
549 block_number, block_hash
550 );
551
552 Ok(())
553 })
554 }
555}
556
557#[derive(Debug, Default)]
559pub struct CheckPayloadAccepted {}
560
561impl<Engine> Action<Engine> for CheckPayloadAccepted
562where
563 Engine: EngineTypes,
564 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
565{
566 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
567 Box::pin(async move {
568 let mut accepted_check: bool = false;
569
570 let latest_block = env
571 .current_block_info()
572 .ok_or_else(|| eyre::eyre!("No latest block information available"))?;
573
574 let payload_id = *env
575 .active_node_state()?
576 .payload_id_history
577 .get(&(latest_block.number + 1))
578 .ok_or_else(|| eyre::eyre!("Cannot find payload_id"))?;
579
580 let node_clients = env.node_clients.clone();
581 for (idx, client) in node_clients.iter().enumerate() {
582 let rpc_client = &client.rpc;
583
584 let rpc_latest_header = EthApiClient::<
586 TransactionRequest,
587 Transaction,
588 Block,
589 Receipt,
590 Header,
591 TransactionSigned,
592 >::header_by_number(
593 rpc_client, alloy_eips::BlockNumberOrTag::Latest
594 )
595 .await?
596 .ok_or_else(|| eyre::eyre!("No latest header found from rpc"))?;
597
598 let next_new_payload = env
600 .active_node_state()?
601 .latest_payload_built
602 .as_ref()
603 .ok_or_else(|| eyre::eyre!("No next built payload found"))?;
604
605 let built_payload = EngineApiClient::<Engine>::get_payload_v3(
606 &client.engine.http_client(),
607 payload_id,
608 )
609 .await?;
610
611 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 = built_payload.into();
612 let new_payload_block_hash = execution_payload_envelope
613 .execution_payload
614 .payload_inner
615 .payload_inner
616 .block_hash;
617
618 if rpc_latest_header.hash != new_payload_block_hash {
619 debug!(
620 "Client {}: The hash is not matched: {:?} {:?}",
621 idx, rpc_latest_header.hash, new_payload_block_hash
622 );
623 continue;
624 }
625
626 if rpc_latest_header.inner.difficulty != alloy_primitives::U256::ZERO {
627 debug!(
628 "Client {}: difficulty != 0: {:?}",
629 idx, rpc_latest_header.inner.difficulty
630 );
631 continue;
632 }
633
634 if rpc_latest_header.inner.mix_hash != next_new_payload.prev_randao {
635 debug!(
636 "Client {}: The mix_hash and prev_randao is not same: {:?} {:?}",
637 idx, rpc_latest_header.inner.mix_hash, next_new_payload.prev_randao
638 );
639 continue;
640 }
641
642 let extra_len = rpc_latest_header.inner.extra_data.len();
643 if extra_len <= 32 {
644 debug!("Client {}: extra_len is fewer than 32. extra_len: {}", idx, extra_len);
645 continue;
646 }
647
648 if !accepted_check {
650 accepted_check = true;
651 env.set_current_block_info(BlockInfo {
653 hash: rpc_latest_header.hash,
654 number: rpc_latest_header.inner.number,
655 timestamp: rpc_latest_header.inner.timestamp,
656 })?;
657
658 env.active_node_state_mut()?.latest_header_time =
661 rpc_latest_header.inner.timestamp;
662 env.active_node_state_mut()?.latest_fork_choice_state.head_block_hash =
663 rpc_latest_header.hash;
664 }
665 }
666
667 if accepted_check {
668 Ok(())
669 } else {
670 Err(eyre::eyre!("No clients passed payload acceptance checks"))
671 }
672 })
673 }
674}
675
676#[derive(Debug, Default)]
678pub struct BroadcastNextNewPayload {
679 active_node_only: bool,
681}
682
683impl BroadcastNextNewPayload {
684 pub const fn with_active_node() -> Self {
686 Self { active_node_only: true }
687 }
688}
689
690impl<Engine> Action<Engine> for BroadcastNextNewPayload
691where
692 Engine: EngineTypes + PayloadTypes,
693 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
694 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
695{
696 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
697 Box::pin(async move {
698 let next_new_payload = env
700 .active_node_state()?
701 .latest_payload_built
702 .as_ref()
703 .ok_or_else(|| eyre::eyre!("No next built payload found"))?
704 .clone();
705 let parent_beacon_block_root = next_new_payload
706 .parent_beacon_block_root
707 .ok_or_else(|| eyre::eyre!("No parent beacon block root for next new payload"))?;
708
709 let payload_envelope = env
710 .active_node_state()?
711 .latest_payload_envelope
712 .as_ref()
713 .ok_or_else(|| eyre::eyre!("No execution payload envelope available"))?
714 .clone();
715
716 let execution_payload_envelope: ExecutionPayloadEnvelopeV3 = payload_envelope.into();
717 let execution_payload = execution_payload_envelope.execution_payload;
718
719 if self.active_node_only {
720 let active_idx = env.active_node_idx;
722 let engine = env.node_clients[active_idx].engine.http_client();
723
724 let result = EngineApiClient::<Engine>::new_payload_v3(
725 &engine,
726 execution_payload.clone(),
727 vec![],
728 parent_beacon_block_root,
729 )
730 .await?;
731
732 debug!("Active node {}: new_payload status: {:?}", active_idx, result.status);
733
734 match result.status {
736 PayloadStatusEnum::Valid => {
737 env.active_node_state_mut()?.latest_payload_executed =
738 Some(next_new_payload);
739 Ok(())
740 }
741 other => Err(eyre::eyre!(
742 "Active node {}: Unexpected payload status: {:?}",
743 active_idx,
744 other
745 )),
746 }
747 } else {
748 let mut broadcast_results = Vec::new();
750 let mut first_valid_seen = false;
751
752 for (idx, client) in env.node_clients.iter().enumerate() {
753 let engine = client.engine.http_client();
754
755 let result = EngineApiClient::<Engine>::new_payload_v3(
757 &engine,
758 execution_payload.clone(),
759 vec![],
760 parent_beacon_block_root,
761 )
762 .await?;
763
764 broadcast_results.push((idx, result.status.clone()));
765 debug!("Node {}: new_payload broadcast status: {:?}", idx, result.status);
766
767 if result.status == PayloadStatusEnum::Valid && !first_valid_seen {
769 first_valid_seen = true;
770 } else if let PayloadStatusEnum::Invalid { validation_error } = result.status {
771 debug!(
772 "Node {}: Invalid payload status returned from broadcast: {:?}",
773 idx, validation_error
774 );
775 }
776 }
777
778 if first_valid_seen {
780 env.active_node_state_mut()?.latest_payload_executed = Some(next_new_payload);
781 }
782
783 let any_valid =
785 broadcast_results.iter().any(|(_, status)| *status == PayloadStatusEnum::Valid);
786 if !any_valid {
787 return Err(eyre::eyre!(
788 "Failed to successfully broadcast payload to any client"
789 ));
790 }
791
792 debug!("Broadcast complete. Results: {:?}", broadcast_results);
793
794 Ok(())
795 }
796 })
797 }
798}
799
800#[derive(Debug)]
802pub struct ProduceBlocks<Engine> {
803 pub num_blocks: u64,
805 _phantom: PhantomData<Engine>,
807}
808
809impl<Engine> ProduceBlocks<Engine> {
810 pub fn new(num_blocks: u64) -> Self {
812 Self { num_blocks, _phantom: Default::default() }
813 }
814}
815
816impl<Engine> Default for ProduceBlocks<Engine> {
817 fn default() -> Self {
818 Self::new(0)
819 }
820}
821
822impl<Engine> Action<Engine> for ProduceBlocks<Engine>
823where
824 Engine: EngineTypes + PayloadTypes,
825 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
826 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
827{
828 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
829 Box::pin(async move {
830 for _ in 0..self.num_blocks {
831 let mut sequence = Sequence::new(vec![
835 Box::new(PickNextBlockProducer::default()),
836 Box::new(GeneratePayloadAttributes::default()),
837 Box::new(GenerateNextPayload::default()),
838 Box::new(BroadcastNextNewPayload::default()),
839 Box::new(UpdateBlockInfoToLatestPayload::default()),
840 ]);
841 sequence.execute(env).await?;
842 }
843 Ok(())
844 })
845 }
846}
847
848#[derive(Debug)]
850pub struct TestFcuToTag {
851 pub tag: String,
853 pub expected_status: PayloadStatusEnum,
855}
856
857impl TestFcuToTag {
858 pub fn new(tag: impl Into<String>, expected_status: PayloadStatusEnum) -> Self {
860 Self { tag: tag.into(), expected_status }
861 }
862}
863
864impl<Engine> Action<Engine> for TestFcuToTag
865where
866 Engine: EngineTypes,
867{
868 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
869 Box::pin(async move {
870 let (target_block, _node_idx) = env
872 .block_registry
873 .get(&self.tag)
874 .copied()
875 .ok_or_else(|| eyre::eyre!("Block tag '{}' not found in registry", self.tag))?;
876
877 let engine_client = env.node_clients[0].engine.http_client();
878 let fcu_state = ForkchoiceState {
879 head_block_hash: target_block.hash,
880 safe_block_hash: target_block.hash,
881 finalized_block_hash: target_block.hash,
882 };
883
884 let fcu_response =
885 EngineApiClient::<Engine>::fork_choice_updated_v2(&engine_client, fcu_state, None)
886 .await?;
887
888 match (&fcu_response.payload_status.status, &self.expected_status) {
890 (PayloadStatusEnum::Valid, PayloadStatusEnum::Valid) => {
891 debug!("FCU to '{}' returned VALID as expected", self.tag);
892 }
893 (PayloadStatusEnum::Invalid { .. }, PayloadStatusEnum::Invalid { .. }) => {
894 debug!("FCU to '{}' returned INVALID as expected", self.tag);
895 }
896 (PayloadStatusEnum::Syncing, PayloadStatusEnum::Syncing) => {
897 debug!("FCU to '{}' returned SYNCING as expected", self.tag);
898 }
899 (PayloadStatusEnum::Accepted, PayloadStatusEnum::Accepted) => {
900 debug!("FCU to '{}' returned ACCEPTED as expected", self.tag);
901 }
902 (actual, expected) => {
903 return Err(eyre::eyre!(
904 "FCU to '{}': expected status {:?}, but got {:?}",
905 self.tag,
906 expected,
907 actual
908 ));
909 }
910 }
911
912 Ok(())
913 })
914 }
915}
916
917#[derive(Debug)]
919pub struct ExpectFcuStatus {
920 pub target_tag: String,
922 pub expected_status: PayloadStatusEnum,
924}
925
926impl ExpectFcuStatus {
927 pub fn valid(target_tag: impl Into<String>) -> Self {
929 Self { target_tag: target_tag.into(), expected_status: PayloadStatusEnum::Valid }
930 }
931
932 pub fn invalid(target_tag: impl Into<String>) -> Self {
934 Self {
935 target_tag: target_tag.into(),
936 expected_status: PayloadStatusEnum::Invalid {
937 validation_error: "corrupted block".to_string(),
938 },
939 }
940 }
941
942 pub fn syncing(target_tag: impl Into<String>) -> Self {
944 Self { target_tag: target_tag.into(), expected_status: PayloadStatusEnum::Syncing }
945 }
946
947 pub fn accepted(target_tag: impl Into<String>) -> Self {
949 Self { target_tag: target_tag.into(), expected_status: PayloadStatusEnum::Accepted }
950 }
951}
952
953impl<Engine> Action<Engine> for ExpectFcuStatus
954where
955 Engine: EngineTypes,
956{
957 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
958 Box::pin(async move {
959 let mut test_fcu = TestFcuToTag::new(&self.target_tag, self.expected_status.clone());
960 test_fcu.execute(env).await
961 })
962 }
963}
964
965#[derive(Debug)]
967pub struct ValidateCanonicalTag {
968 pub tag: String,
970}
971
972impl ValidateCanonicalTag {
973 pub fn new(tag: impl Into<String>) -> Self {
975 Self { tag: tag.into() }
976 }
977}
978
979impl<Engine> Action<Engine> for ValidateCanonicalTag
980where
981 Engine: EngineTypes,
982{
983 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
984 Box::pin(async move {
985 let mut expect_valid = ExpectFcuStatus::valid(&self.tag);
986 expect_valid.execute(env).await?;
987
988 debug!("Successfully validated that '{}' remains canonical", self.tag);
989 Ok(())
990 })
991 }
992}
993
994#[derive(Debug)]
997pub struct ProduceBlocksLocally<Engine> {
998 pub num_blocks: u64,
1000 _phantom: PhantomData<Engine>,
1002}
1003
1004impl<Engine> ProduceBlocksLocally<Engine> {
1005 pub fn new(num_blocks: u64) -> Self {
1007 Self { num_blocks, _phantom: Default::default() }
1008 }
1009}
1010
1011impl<Engine> Default for ProduceBlocksLocally<Engine> {
1012 fn default() -> Self {
1013 Self::new(0)
1014 }
1015}
1016
1017impl<Engine> Action<Engine> for ProduceBlocksLocally<Engine>
1018where
1019 Engine: EngineTypes + PayloadTypes,
1020 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
1021 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
1022{
1023 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
1024 Box::pin(async move {
1025 let producer_idx = env.active_node_idx;
1027
1028 for _ in 0..self.num_blocks {
1029 env.last_producer_idx = Some(producer_idx);
1031
1032 let mut sequence = Sequence::new(vec![
1034 Box::new(GeneratePayloadAttributes::default()),
1036 Box::new(GenerateNextPayload::default()),
1037 Box::new(BroadcastNextNewPayload::with_active_node()),
1039 Box::new(UpdateBlockInfoToLatestPayload::default()),
1040 ]);
1041 sequence.execute(env).await?;
1042 }
1043 Ok(())
1044 })
1045 }
1046}
1047
1048#[derive(Debug)]
1050pub struct ProduceInvalidBlocks<Engine> {
1051 pub num_blocks: u64,
1053 pub invalid_indices: HashSet<u64>,
1055 _phantom: PhantomData<Engine>,
1057}
1058
1059impl<Engine> ProduceInvalidBlocks<Engine> {
1060 pub fn new(num_blocks: u64, invalid_indices: HashSet<u64>) -> Self {
1062 Self { num_blocks, invalid_indices, _phantom: Default::default() }
1063 }
1064
1065 pub fn with_invalid_at(num_blocks: u64, invalid_index: u64) -> Self {
1068 let mut invalid_indices = HashSet::new();
1069 invalid_indices.insert(invalid_index);
1070 Self::new(num_blocks, invalid_indices)
1071 }
1072}
1073
1074impl<Engine> Action<Engine> for ProduceInvalidBlocks<Engine>
1075where
1076 Engine: EngineTypes + PayloadTypes,
1077 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
1078 Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
1079{
1080 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
1081 Box::pin(async move {
1082 for block_index in 0..self.num_blocks {
1083 let is_invalid = self.invalid_indices.contains(&block_index);
1084
1085 if is_invalid {
1086 debug!("Producing invalid block at index {}", block_index);
1087
1088 let mut sequence = Sequence::new(vec![
1090 Box::new(PickNextBlockProducer::default()),
1091 Box::new(GeneratePayloadAttributes::default()),
1092 Box::new(GenerateNextPayload::default()),
1093 ]);
1094 sequence.execute(env).await?;
1095
1096 let latest_envelope =
1098 env.active_node_state()?.latest_payload_envelope.as_ref().ok_or_else(
1099 || eyre::eyre!("No payload envelope available to corrupt"),
1100 )?;
1101
1102 let envelope_v3: ExecutionPayloadEnvelopeV3 = latest_envelope.clone().into();
1103 let mut corrupted_payload = envelope_v3.execution_payload;
1104
1105 corrupted_payload.payload_inner.payload_inner.state_root = B256::random();
1107
1108 debug!(
1109 "Corrupted state root for block {} to: {}",
1110 block_index, corrupted_payload.payload_inner.payload_inner.state_root
1111 );
1112
1113 let engine_client = env.node_clients[0].engine.http_client();
1115 let versioned_hashes = Vec::new();
1117 let parent_beacon_block_root = B256::random();
1119
1120 let new_payload_response = EngineApiClient::<Engine>::new_payload_v3(
1121 &engine_client,
1122 corrupted_payload.clone(),
1123 versioned_hashes,
1124 parent_beacon_block_root,
1125 )
1126 .await?;
1127
1128 match new_payload_response.status {
1130 PayloadStatusEnum::Invalid { validation_error } => {
1131 debug!(
1132 "Block {} correctly rejected as invalid: {:?}",
1133 block_index, validation_error
1134 );
1135 }
1136 other_status => {
1137 return Err(eyre::eyre!(
1138 "Expected block {} to be rejected as INVALID, but got: {:?}",
1139 block_index,
1140 other_status
1141 ));
1142 }
1143 }
1144
1145 env.set_current_block_info(BlockInfo {
1147 hash: corrupted_payload.payload_inner.payload_inner.block_hash,
1148 number: corrupted_payload.payload_inner.payload_inner.block_number,
1149 timestamp: corrupted_payload.timestamp(),
1150 })?;
1151 } else {
1152 debug!("Producing valid block at index {}", block_index);
1153
1154 let mut sequence = Sequence::new(vec![
1156 Box::new(PickNextBlockProducer::default()),
1157 Box::new(GeneratePayloadAttributes::default()),
1158 Box::new(GenerateNextPayload::default()),
1159 Box::new(BroadcastNextNewPayload::default()),
1160 Box::new(UpdateBlockInfoToLatestPayload::default()),
1161 ]);
1162 sequence.execute(env).await?;
1163 }
1164 }
1165 Ok(())
1166 })
1167 }
1168}