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