Skip to content

Commit a04bd71

Browse files
authored
feat(test): rewrite test_engine_tree_valid_forks_with_older_canonical_head using e2e framework (#16699)
1 parent e869762 commit a04bd71

File tree

4 files changed

+132
-78
lines changed

4 files changed

+132
-78
lines changed

crates/e2e-test-utils/src/testsuite/actions/fork.rs

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,39 @@ use reth_rpc_api::clients::EthApiClient;
1313
use std::marker::PhantomData;
1414
use tracing::debug;
1515

16-
/// Action to create a fork from a specified block number and produce blocks on top
16+
/// Fork base target for fork creation
17+
#[derive(Debug, Clone)]
18+
pub enum ForkBase {
19+
/// Block number
20+
Number(u64),
21+
/// Tagged block reference
22+
Tag(String),
23+
}
24+
25+
/// Action to create a fork from a specified block and produce blocks on top
1726
#[derive(Debug)]
1827
pub struct CreateFork<Engine> {
19-
/// Block number to use as the base of the fork
20-
pub fork_base_block: u64,
28+
/// Fork base specification (either block number or tag)
29+
pub fork_base: ForkBase,
2130
/// Number of blocks to produce on top of the fork base
2231
pub num_blocks: u64,
2332
/// Tracks engine type
2433
_phantom: PhantomData<Engine>,
2534
}
2635

2736
impl<Engine> CreateFork<Engine> {
28-
/// Create a new `CreateFork` action
37+
/// Create a new `CreateFork` action from a block number
2938
pub fn new(fork_base_block: u64, num_blocks: u64) -> Self {
30-
Self { fork_base_block, num_blocks, _phantom: Default::default() }
39+
Self {
40+
fork_base: ForkBase::Number(fork_base_block),
41+
num_blocks,
42+
_phantom: Default::default(),
43+
}
44+
}
45+
46+
/// Create a new `CreateFork` action from a tagged block
47+
pub fn new_from_tag(tag: impl Into<String>, num_blocks: u64) -> Self {
48+
Self { fork_base: ForkBase::Tag(tag.into()), num_blocks, _phantom: Default::default() }
3149
}
3250
}
3351

@@ -40,18 +58,34 @@ where
4058
{
4159
fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
4260
Box::pin(async move {
43-
// store the fork base for later validation
44-
env.current_fork_base = Some(self.fork_base_block);
45-
46-
let mut sequence = Sequence::new(vec![
47-
Box::new(SetForkBase::new(self.fork_base_block)),
48-
Box::new(ProduceBlocks::new(self.num_blocks)),
49-
// Note: ValidateFork is not called here because fork blocks are not accessible
50-
// via RPC until they are made canonical. Validation will be done automatically
51-
// as part of MakeCanonical or ReorgTo actions.
52-
]);
53-
54-
sequence.execute(env).await
61+
// resolve the fork base and execute the appropriate sequence
62+
match &self.fork_base {
63+
ForkBase::Number(block_number) => {
64+
// store the fork base for later validation
65+
env.current_fork_base = Some(*block_number);
66+
67+
let mut sequence = Sequence::new(vec![
68+
Box::new(SetForkBase::new(*block_number)),
69+
Box::new(ProduceBlocks::new(self.num_blocks)),
70+
]);
71+
sequence.execute(env).await
72+
}
73+
ForkBase::Tag(tag) => {
74+
let block_info =
75+
env.block_registry.get(tag).copied().ok_or_else(|| {
76+
eyre::eyre!("Block tag '{}' not found in registry", tag)
77+
})?;
78+
79+
// store the fork base for later validation
80+
env.current_fork_base = Some(block_info.number);
81+
82+
let mut sequence = Sequence::new(vec![
83+
Box::new(SetForkBaseFromBlockInfo::new(block_info)),
84+
Box::new(ProduceBlocks::new(self.num_blocks)),
85+
]);
86+
sequence.execute(env).await
87+
}
88+
}
5589
})
5690
}
5791
}
@@ -63,13 +97,27 @@ pub struct SetForkBase {
6397
pub fork_base_block: u64,
6498
}
6599

100+
/// Sub-action to set the fork base block from existing block info
101+
#[derive(Debug)]
102+
pub struct SetForkBaseFromBlockInfo {
103+
/// Complete block info to use as the base of the fork
104+
pub fork_base_info: BlockInfo,
105+
}
106+
66107
impl SetForkBase {
67108
/// Create a new `SetForkBase` action
68109
pub const fn new(fork_base_block: u64) -> Self {
69110
Self { fork_base_block }
70111
}
71112
}
72113

114+
impl SetForkBaseFromBlockInfo {
115+
/// Create a new `SetForkBaseFromBlockInfo` action
116+
pub const fn new(fork_base_info: BlockInfo) -> Self {
117+
Self { fork_base_info }
118+
}
119+
}
120+
73121
impl<Engine> Action<Engine> for SetForkBase
74122
where
75123
Engine: EngineTypes,
@@ -117,6 +165,37 @@ where
117165
}
118166
}
119167

168+
impl<Engine> Action<Engine> for SetForkBaseFromBlockInfo
169+
where
170+
Engine: EngineTypes,
171+
{
172+
fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
173+
Box::pin(async move {
174+
let block_info = self.fork_base_info;
175+
176+
debug!(
177+
"Set fork base from block info: block {} (hash: {})",
178+
block_info.number, block_info.hash
179+
);
180+
181+
// update environment to point to the fork base block
182+
env.current_block_info = Some(block_info);
183+
env.latest_header_time = block_info.timestamp;
184+
185+
// update fork choice state to the fork base
186+
env.latest_fork_choice_state = ForkchoiceState {
187+
head_block_hash: block_info.hash,
188+
safe_block_hash: block_info.hash,
189+
finalized_block_hash: block_info.hash,
190+
};
191+
192+
debug!("Set fork base to block {} (hash: {})", block_info.number, block_info.hash);
193+
194+
Ok(())
195+
})
196+
}
197+
}
198+
120199
/// Sub-action to validate that a fork was created correctly
121200
#[derive(Debug)]
122201
pub struct ValidateFork {

crates/e2e-test-utils/src/testsuite/actions/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub mod fork;
1212
pub mod produce_blocks;
1313
pub mod reorg;
1414

15-
pub use fork::{CreateFork, SetForkBase, ValidateFork};
15+
pub use fork::{CreateFork, ForkBase, SetForkBase, SetForkBaseFromBlockInfo, ValidateFork};
1616
pub use produce_blocks::{
1717
AssertMineBlock, BroadcastLatestForkchoice, BroadcastNextNewPayload, CheckPayloadAccepted,
1818
GenerateNextPayload, GeneratePayloadAttributes, PickNextBlockProducer, ProduceBlocks,

crates/engine/tree/src/tree/e2e_tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,38 @@ async fn test_engine_tree_fcu_reorg_with_all_blocks_e2e() -> Result<()> {
7474

7575
Ok(())
7676
}
77+
78+
/// Test that verifies valid forks with an older canonical head.
79+
///
80+
/// This test creates two competing fork chains starting from a common ancestor,
81+
/// then switches between them using forkchoice updates, verifying that the engine
82+
/// correctly handles chains where the canonical head is older than fork tips.
83+
#[tokio::test]
84+
async fn test_engine_tree_valid_forks_with_older_canonical_head_e2e() -> Result<()> {
85+
reth_tracing::init_test_tracing();
86+
87+
let test = TestBuilder::new()
88+
.with_setup(default_engine_tree_setup())
89+
// create base chain with 1 block (this will be our old head)
90+
.with_action(ProduceBlocks::<EthEngineTypes>::new(1))
91+
.with_action(CaptureBlock::new("old_head"))
92+
.with_action(MakeCanonical::new())
93+
// extend base chain with 5 more blocks to establish a fork point
94+
.with_action(ProduceBlocks::<EthEngineTypes>::new(5))
95+
.with_action(CaptureBlock::new("fork_point"))
96+
.with_action(MakeCanonical::new())
97+
// revert to old head to simulate scenario where canonical head is older
98+
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("old_head"))
99+
// create first competing chain (chain A) from fork point with 10 blocks
100+
.with_action(CreateFork::<EthEngineTypes>::new_from_tag("fork_point", 10))
101+
.with_action(CaptureBlock::new("chain_a_tip"))
102+
// create second competing chain (chain B) from same fork point with 10 blocks
103+
.with_action(CreateFork::<EthEngineTypes>::new_from_tag("fork_point", 10))
104+
.with_action(CaptureBlock::new("chain_b_tip"))
105+
// switch to chain B via forkchoice update - this should become canonical
106+
.with_action(ReorgTo::<EthEngineTypes>::new_from_tag("chain_b_tip"));
107+
108+
test.run::<EthereumNode>().await?;
109+
110+
Ok(())
111+
}

crates/engine/tree/src/tree/tests.rs

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,66 +1135,6 @@ async fn test_engine_tree_live_sync_fcu_extends_canon_chain() {
11351135
test_harness.check_canon_head(main_last_hash);
11361136
}
11371137

1138-
#[tokio::test]
1139-
async fn test_engine_tree_valid_forks_with_older_canonical_head() {
1140-
reth_tracing::init_test_tracing();
1141-
1142-
let chain_spec = MAINNET.clone();
1143-
let mut test_harness = TestHarness::new(chain_spec.clone());
1144-
1145-
// create base chain and setup test harness with it
1146-
let base_chain: Vec<_> = test_harness.block_builder.get_executed_blocks(0..1).collect();
1147-
test_harness = test_harness.with_blocks(base_chain.clone());
1148-
1149-
let old_head = base_chain.first().unwrap().recovered_block();
1150-
1151-
// extend base chain
1152-
let extension_chain = test_harness.block_builder.create_fork(old_head, 5);
1153-
let fork_block = extension_chain.last().unwrap().clone_sealed_block();
1154-
1155-
test_harness.setup_range_insertion_for_valid_chain(extension_chain.clone());
1156-
test_harness.insert_chain(extension_chain).await;
1157-
1158-
// fcu to old_head
1159-
test_harness.fcu_to(old_head.hash(), ForkchoiceStatus::Valid).await;
1160-
1161-
// create two competing chains starting from fork_block
1162-
let chain_a = test_harness.block_builder.create_fork(&fork_block, 10);
1163-
let chain_b = test_harness.block_builder.create_fork(&fork_block, 10);
1164-
1165-
// insert chain A blocks using newPayload
1166-
test_harness.setup_range_insertion_for_valid_chain(chain_a.clone());
1167-
for block in &chain_a {
1168-
test_harness.send_new_payload(block.clone()).await;
1169-
}
1170-
1171-
test_harness.check_canon_chain_insertion(chain_a.clone()).await;
1172-
1173-
// insert chain B blocks using newPayload
1174-
test_harness.setup_range_insertion_for_valid_chain(chain_b.clone());
1175-
for block in &chain_b {
1176-
test_harness.send_new_payload(block.clone()).await;
1177-
}
1178-
1179-
test_harness.check_canon_chain_insertion(chain_b.clone()).await;
1180-
1181-
// send FCU to make the tip of chain B the new head
1182-
let chain_b_tip_hash = chain_b.last().unwrap().hash();
1183-
test_harness.send_fcu(chain_b_tip_hash, ForkchoiceStatus::Valid).await;
1184-
1185-
// check for CanonicalChainCommitted event
1186-
test_harness.check_canon_commit(chain_b_tip_hash).await;
1187-
1188-
// verify FCU was processed
1189-
test_harness.check_fcu(chain_b_tip_hash, ForkchoiceStatus::Valid).await;
1190-
1191-
// verify the new canonical head
1192-
test_harness.check_canon_head(chain_b_tip_hash);
1193-
1194-
// verify that chain A is now considered a fork
1195-
assert!(test_harness.tree.is_fork(chain_a.last().unwrap().sealed_header()).unwrap());
1196-
}
1197-
11981138
#[tokio::test]
11991139
async fn test_engine_tree_buffered_blocks_are_eventually_connected() {
12001140
let chain_spec = MAINNET.clone();

0 commit comments

Comments
 (0)