Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions crates/e2e-test-utils/src/testsuite/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
use crate::testsuite::{Environment, LatestBlockInfo};
use alloy_primitives::{Bytes, B256, U256};
use alloy_rpc_types_engine::{
payload::ExecutionPayloadEnvelopeV3, ForkchoiceState, PayloadAttributes, PayloadStatusEnum,
payload::ExecutionPayloadEnvelopeV3, ForkchoiceState, ForkchoiceUpdated, PayloadAttributes,
PayloadStatusEnum,
};
use alloy_rpc_types_eth::{Block, Header, Receipt, Transaction};
use eyre::Result;
Expand All @@ -14,6 +15,27 @@ use std::{future::Future, marker::PhantomData, time::Duration};
use tokio::time::sleep;
use tracing::debug;

/// Validates a forkchoice update response and returns an error if invalid
fn validate_fcu_response(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
match &response.payload_status.status {
PayloadStatusEnum::Valid => {
debug!("{}: FCU accepted as valid", context);
Ok(())
}
PayloadStatusEnum::Invalid { validation_error } => {
Err(eyre::eyre!("{}: FCU rejected as invalid: {:?}", context, validation_error))
}
PayloadStatusEnum::Syncing => {
debug!("{}: FCU accepted, node is syncing", context);
Ok(())
}
PayloadStatusEnum::Accepted => {
debug!("{}: FCU accepted for processing", context);
Ok(())
}
}
}
Comment on lines +18 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make a followup issue for helper fns that would expect, for example:

  • only valid
  • only invalid
  • only syncing + accepted
  • not syncing or accepted

Copy link
Member Author

@fgimenez fgimenez May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense added here #16532


/// An action that can be performed on an instance.
///
/// Actions execute operations and potentially make assertions in a single step.
Expand Down Expand Up @@ -282,6 +304,9 @@ where

debug!("FCU result: {:?}", fcu_result);

// validate the FCU status before proceeding
validate_fcu_response(&fcu_result, "GenerateNextPayload")?;

let payload_id = if let Some(payload_id) = fcu_result.payload_id {
debug!("Received new payload ID: {:?}", payload_id);
payload_id
Expand All @@ -305,6 +330,9 @@ where

debug!("Fresh FCU result: {:?}", fresh_fcu_result);

// validate the fresh FCU status
validate_fcu_response(&fresh_fcu_result, "GenerateNextPayload (fresh)")?;

if let Some(payload_id) = fresh_fcu_result.payload_id {
payload_id
} else {
Expand Down Expand Up @@ -400,6 +428,8 @@ where
"Client {}: Forkchoice update status: {:?}",
idx, resp.payload_status.status
);
// validate that the forkchoice update was accepted
validate_fcu_response(&resp, &format!("Client {idx}"))?;
}
Err(err) => {
return Err(eyre::eyre!(
Expand Down Expand Up @@ -594,12 +624,13 @@ where
Box::pin(async move {
for _ in 0..self.num_blocks {
// create a fresh sequence for each block to avoid state pollution
// Note: This produces blocks but does NOT make them canonical
// Use MakeCanonical action explicitly if canonicalization is needed
let mut sequence = Sequence::new(vec![
Box::new(PickNextBlockProducer::default()),
Box::new(GeneratePayloadAttributes::default()),
Box::new(GenerateNextPayload::default()),
Box::new(BroadcastNextNewPayload::default()),
Box::new(BroadcastLatestForkchoice::default()),
Box::new(UpdateBlockInfo::default()),
]);
sequence.execute(env).await?;
Expand Down Expand Up @@ -1004,6 +1035,31 @@ where
}
}

/// Action that makes the current latest block canonical by broadcasting a forkchoice update
#[derive(Debug, Default)]
pub struct MakeCanonical {}

impl MakeCanonical {
/// Create a new `MakeCanonical` action
pub const fn new() -> Self {
Self {}
}
}

impl<Engine> Action<Engine> for MakeCanonical
where
Engine: EngineTypes + PayloadTypes,
Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
Engine::ExecutionPayloadEnvelopeV3: Into<ExecutionPayloadEnvelopeV3>,
{
fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let mut broadcast_action = BroadcastLatestForkchoice::default();
broadcast_action.execute(env).await
})
}
}

/// Action that captures the current block and tags it with a name for later reference
#[derive(Debug)]
pub struct CaptureBlock {
Expand Down
50 changes: 45 additions & 5 deletions crates/e2e-test-utils/src/testsuite/examples.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Example tests using the test suite framework.

use crate::testsuite::{
actions::{AssertMineBlock, CaptureBlock, CreateFork, ProduceBlocks, ReorgTo},
actions::{AssertMineBlock, CaptureBlock, CreateFork, MakeCanonical, ProduceBlocks, ReorgTo},
setup::{NetworkSetup, Setup},
TestBuilder,
};
Expand Down Expand Up @@ -63,8 +63,10 @@ async fn test_testsuite_produce_blocks() -> Result<()> {
))
.with_network(NetworkSetup::single_node());

let test =
TestBuilder::new().with_setup(setup).with_action(ProduceBlocks::<EthEngineTypes>::new(5));
let test = TestBuilder::new()
.with_setup(setup)
.with_action(ProduceBlocks::<EthEngineTypes>::new(5))
.with_action(MakeCanonical::new());

test.run::<EthereumNode>().await?;

Expand All @@ -88,6 +90,7 @@ async fn test_testsuite_create_fork() -> Result<()> {
let test = TestBuilder::new()
.with_setup(setup)
.with_action(ProduceBlocks::<EthEngineTypes>::new(2))
.with_action(MakeCanonical::new())
.with_action(CreateFork::<EthEngineTypes>::new(1, 3));

test.run::<EthereumNode>().await?;
Expand All @@ -112,9 +115,46 @@ async fn test_testsuite_reorg_with_tagging() -> Result<()> {
let test = TestBuilder::new()
.with_setup(setup)
.with_action(ProduceBlocks::<EthEngineTypes>::new(3)) // produce blocks 1, 2, 3
.with_action(CaptureBlock::new("main_chain_tip")) // tag block 3 as "main_chain_tip"
.with_action(MakeCanonical::new()) // make main chain tip canonical
.with_action(CreateFork::<EthEngineTypes>::new(1, 2)) // fork from block 1, produce blocks 2', 3'
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("main_chain_tip")); // reorg back to tagged block 3
.with_action(CaptureBlock::new("fork_tip")) // tag fork tip
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("fork_tip")); // reorg to fork tip

test.run::<EthereumNode>().await?;

Ok(())
}

#[tokio::test]
async fn test_testsuite_deep_reorg() -> Result<()> {
reth_tracing::init_test_tracing();

let setup = Setup::default()
.with_chain_spec(Arc::new(
ChainSpecBuilder::default()
.chain(MAINNET.chain)
.genesis(serde_json::from_str(include_str!("assets/genesis.json")).unwrap())
.cancun_activated()
.build(),
))
.with_network(NetworkSetup::single_node());

let test = TestBuilder::new()
.with_setup(setup)
// receive newPayload and forkchoiceUpdated with block height 1
.with_action(ProduceBlocks::<EthEngineTypes>::new(1))
.with_action(MakeCanonical::new())
.with_action(CaptureBlock::new("block1"))
// receive forkchoiceUpdated with block hash A as head (block A at height 2)
.with_action(CreateFork::<EthEngineTypes>::new(1, 1))
.with_action(CaptureBlock::new("blockA_height2"))
.with_action(MakeCanonical::new())
// receive newPayload with block hash B and height 2
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("block1"))
.with_action(CreateFork::<EthEngineTypes>::new(1, 1))
.with_action(CaptureBlock::new("blockB_height2"))
// receive forkchoiceUpdated with block hash B as head
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("blockB_height2"));

test.run::<EthereumNode>().await?;

Expand Down
Loading