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
113 changes: 96 additions & 17 deletions crates/e2e-test-utils/src/testsuite/actions/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,39 @@ use reth_rpc_api::clients::EthApiClient;
use std::marker::PhantomData;
use tracing::debug;

/// Action to create a fork from a specified block number and produce blocks on top
/// Fork base target for fork creation
#[derive(Debug, Clone)]
pub enum ForkBase {
/// Block number
Number(u64),
/// Tagged block reference
Tag(String),
}

/// Action to create a fork from a specified block and produce blocks on top
#[derive(Debug)]
pub struct CreateFork<Engine> {
/// Block number to use as the base of the fork
pub fork_base_block: u64,
/// Fork base specification (either block number or tag)
pub fork_base: ForkBase,
/// Number of blocks to produce on top of the fork base
pub num_blocks: u64,
/// Tracks engine type
_phantom: PhantomData<Engine>,
}

impl<Engine> CreateFork<Engine> {
/// Create a new `CreateFork` action
/// Create a new `CreateFork` action from a block number
pub fn new(fork_base_block: u64, num_blocks: u64) -> Self {
Self { fork_base_block, num_blocks, _phantom: Default::default() }
Self {
fork_base: ForkBase::Number(fork_base_block),
num_blocks,
_phantom: Default::default(),
}
}

/// Create a new `CreateFork` action from a tagged block
pub fn new_from_tag(tag: impl Into<String>, num_blocks: u64) -> Self {
Self { fork_base: ForkBase::Tag(tag.into()), num_blocks, _phantom: Default::default() }
}
}

Expand All @@ -40,18 +58,34 @@ where
{
fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
// store the fork base for later validation
env.current_fork_base = Some(self.fork_base_block);

let mut sequence = Sequence::new(vec![
Box::new(SetForkBase::new(self.fork_base_block)),
Box::new(ProduceBlocks::new(self.num_blocks)),
// Note: ValidateFork is not called here because fork blocks are not accessible
// via RPC until they are made canonical. Validation will be done automatically
// as part of MakeCanonical or ReorgTo actions.
]);

sequence.execute(env).await
// resolve the fork base and execute the appropriate sequence
match &self.fork_base {
ForkBase::Number(block_number) => {
// store the fork base for later validation
env.current_fork_base = Some(*block_number);

let mut sequence = Sequence::new(vec![
Box::new(SetForkBase::new(*block_number)),
Box::new(ProduceBlocks::new(self.num_blocks)),
]);
sequence.execute(env).await
}
ForkBase::Tag(tag) => {
let block_info =
env.block_registry.get(tag).copied().ok_or_else(|| {
eyre::eyre!("Block tag '{}' not found in registry", tag)
})?;

// store the fork base for later validation
env.current_fork_base = Some(block_info.number);

let mut sequence = Sequence::new(vec![
Box::new(SetForkBaseFromBlockInfo::new(block_info)),
Box::new(ProduceBlocks::new(self.num_blocks)),
]);
sequence.execute(env).await
}
}
})
}
}
Expand All @@ -63,13 +97,27 @@ pub struct SetForkBase {
pub fork_base_block: u64,
}

/// Sub-action to set the fork base block from existing block info
#[derive(Debug)]
pub struct SetForkBaseFromBlockInfo {
/// Complete block info to use as the base of the fork
pub fork_base_info: BlockInfo,
}

impl SetForkBase {
/// Create a new `SetForkBase` action
pub const fn new(fork_base_block: u64) -> Self {
Self { fork_base_block }
}
}

impl SetForkBaseFromBlockInfo {
/// Create a new `SetForkBaseFromBlockInfo` action
pub const fn new(fork_base_info: BlockInfo) -> Self {
Self { fork_base_info }
}
}

impl<Engine> Action<Engine> for SetForkBase
where
Engine: EngineTypes,
Expand Down Expand Up @@ -117,6 +165,37 @@ where
}
}

impl<Engine> Action<Engine> for SetForkBaseFromBlockInfo
where
Engine: EngineTypes,
{
fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let block_info = self.fork_base_info;

debug!(
"Set fork base from block info: block {} (hash: {})",
block_info.number, block_info.hash
);

// update environment to point to the fork base block
env.current_block_info = Some(block_info);
env.latest_header_time = block_info.timestamp;

// update fork choice state to the fork base
env.latest_fork_choice_state = ForkchoiceState {
head_block_hash: block_info.hash,
safe_block_hash: block_info.hash,
finalized_block_hash: block_info.hash,
};

debug!("Set fork base to block {} (hash: {})", block_info.number, block_info.hash);

Ok(())
})
}
}

/// Sub-action to validate that a fork was created correctly
#[derive(Debug)]
pub struct ValidateFork {
Expand Down
2 changes: 1 addition & 1 deletion crates/e2e-test-utils/src/testsuite/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub mod fork;
pub mod produce_blocks;
pub mod reorg;

pub use fork::{CreateFork, SetForkBase, ValidateFork};
pub use fork::{CreateFork, ForkBase, SetForkBase, SetForkBaseFromBlockInfo, ValidateFork};
pub use produce_blocks::{
AssertMineBlock, BroadcastLatestForkchoice, BroadcastNextNewPayload, CheckPayloadAccepted,
GenerateNextPayload, GeneratePayloadAttributes, PickNextBlockProducer, ProduceBlocks,
Expand Down
35 changes: 35 additions & 0 deletions crates/engine/tree/src/tree/e2e_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,38 @@ async fn test_engine_tree_fcu_reorg_with_all_blocks_e2e() -> Result<()> {

Ok(())
}

/// Test that verifies valid forks with an older canonical head.
///
/// This test creates two competing fork chains starting from a common ancestor,
/// then switches between them using forkchoice updates, verifying that the engine
/// correctly handles chains where the canonical head is older than fork tips.
#[tokio::test]
async fn test_engine_tree_valid_forks_with_older_canonical_head_e2e() -> Result<()> {
reth_tracing::init_test_tracing();

let test = TestBuilder::new()
.with_setup(default_engine_tree_setup())
// create base chain with 1 block (this will be our old head)
.with_action(ProduceBlocks::<EthEngineTypes>::new(1))
.with_action(CaptureBlock::new("old_head"))
.with_action(MakeCanonical::new())
// extend base chain with 5 more blocks to establish a fork point
.with_action(ProduceBlocks::<EthEngineTypes>::new(5))
.with_action(CaptureBlock::new("fork_point"))
.with_action(MakeCanonical::new())
// revert to old head to simulate scenario where canonical head is older
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("old_head"))
// create first competing chain (chain A) from fork point with 10 blocks
.with_action(CreateFork::<EthEngineTypes>::new_from_tag("fork_point", 10))
.with_action(CaptureBlock::new("chain_a_tip"))
// create second competing chain (chain B) from same fork point with 10 blocks
.with_action(CreateFork::<EthEngineTypes>::new_from_tag("fork_point", 10))
.with_action(CaptureBlock::new("chain_b_tip"))
// switch to chain B via forkchoice update - this should become canonical
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("chain_b_tip"));
Comment on lines +91 to +106
Copy link
Collaborator

Choose a reason for hiding this comment

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

pretty cool


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

Ok(())
}
60 changes: 0 additions & 60 deletions crates/engine/tree/src/tree/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,66 +1135,6 @@ async fn test_engine_tree_live_sync_fcu_extends_canon_chain() {
test_harness.check_canon_head(main_last_hash);
}

#[tokio::test]
async fn test_engine_tree_valid_forks_with_older_canonical_head() {
reth_tracing::init_test_tracing();

let chain_spec = MAINNET.clone();
let mut test_harness = TestHarness::new(chain_spec.clone());

// create base chain and setup test harness with it
let base_chain: Vec<_> = test_harness.block_builder.get_executed_blocks(0..1).collect();
test_harness = test_harness.with_blocks(base_chain.clone());

let old_head = base_chain.first().unwrap().recovered_block();

// extend base chain
let extension_chain = test_harness.block_builder.create_fork(old_head, 5);
let fork_block = extension_chain.last().unwrap().clone_sealed_block();

test_harness.setup_range_insertion_for_valid_chain(extension_chain.clone());
test_harness.insert_chain(extension_chain).await;

// fcu to old_head
test_harness.fcu_to(old_head.hash(), ForkchoiceStatus::Valid).await;

// create two competing chains starting from fork_block
let chain_a = test_harness.block_builder.create_fork(&fork_block, 10);
let chain_b = test_harness.block_builder.create_fork(&fork_block, 10);

// insert chain A blocks using newPayload
test_harness.setup_range_insertion_for_valid_chain(chain_a.clone());
for block in &chain_a {
test_harness.send_new_payload(block.clone()).await;
}

test_harness.check_canon_chain_insertion(chain_a.clone()).await;

// insert chain B blocks using newPayload
test_harness.setup_range_insertion_for_valid_chain(chain_b.clone());
for block in &chain_b {
test_harness.send_new_payload(block.clone()).await;
}

test_harness.check_canon_chain_insertion(chain_b.clone()).await;

// send FCU to make the tip of chain B the new head
let chain_b_tip_hash = chain_b.last().unwrap().hash();
test_harness.send_fcu(chain_b_tip_hash, ForkchoiceStatus::Valid).await;

// check for CanonicalChainCommitted event
test_harness.check_canon_commit(chain_b_tip_hash).await;

// verify FCU was processed
test_harness.check_fcu(chain_b_tip_hash, ForkchoiceStatus::Valid).await;

// verify the new canonical head
test_harness.check_canon_head(chain_b_tip_hash);

// verify that chain A is now considered a fork
assert!(test_harness.tree.is_fork(chain_a.last().unwrap().sealed_header()).unwrap());
}

#[tokio::test]
async fn test_engine_tree_buffered_blocks_are_eventually_connected() {
let chain_spec = MAINNET.clone();
Expand Down
Loading