test: add deep reorg e2e test (#16531)

This commit is contained in:
Federico Gimenez
2025-05-28 23:42:00 +02:00
committed by GitHub
parent f9f340ac77
commit aedb6b41ea
2 changed files with 103 additions and 7 deletions

View File

@@ -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;
@@ -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(())
}
}
}
/// An action that can be performed on an instance.
///
/// Actions execute operations and potentially make assertions in a single step.
@@ -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
@@ -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 {
@@ -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!(
@@ -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?;
@@ -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 {

View File

@@ -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,
};
@@ -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?;
@@ -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?;
@@ -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?;