reth_e2e_test_utils/testsuite/actions/
fork.rs1use crate::testsuite::{
4 actions::{produce_blocks::ProduceBlocks, Sequence},
5 Action, BlockInfo, Environment,
6};
7use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes};
8use alloy_rpc_types_eth::Block;
9use eyre::Result;
10use futures_util::future::BoxFuture;
11use reth_node_api::EngineTypes;
12use std::marker::PhantomData;
13use tracing::debug;
14
15#[derive(Debug, Clone)]
17pub enum ForkBase {
18 Number(u64),
20 Tag(String),
22}
23
24#[derive(Debug)]
26pub struct CreateFork<Engine> {
27 pub fork_base: ForkBase,
29 pub num_blocks: u64,
31 _phantom: PhantomData<Engine>,
33}
34
35impl<Engine> CreateFork<Engine> {
36 pub fn new(fork_base_block: u64, num_blocks: u64) -> Self {
38 Self {
39 fork_base: ForkBase::Number(fork_base_block),
40 num_blocks,
41 _phantom: Default::default(),
42 }
43 }
44
45 pub fn new_from_tag(tag: impl Into<String>, num_blocks: u64) -> Self {
47 Self { fork_base: ForkBase::Tag(tag.into()), num_blocks, _phantom: Default::default() }
48 }
49}
50
51impl<Engine> Action<Engine> for CreateFork<Engine>
52where
53 Engine: EngineTypes,
54 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
55 Engine::ExecutionPayloadEnvelopeV3:
56 Into<alloy_rpc_types_engine::payload::ExecutionPayloadEnvelopeV3>,
57{
58 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
59 Box::pin(async move {
60 match &self.fork_base {
62 ForkBase::Number(block_number) => {
63 env.active_node_state_mut()?.current_fork_base = Some(*block_number);
65
66 let mut sequence = Sequence::new(vec![
67 Box::new(SetForkBase::new(*block_number)),
68 Box::new(ProduceBlocks::new(self.num_blocks)),
69 ]);
70 sequence.execute(env).await
71 }
72 ForkBase::Tag(tag) => {
73 let (block_info, _node_idx) =
74 env.block_registry.get(tag).copied().ok_or_else(|| {
75 eyre::eyre!("Block tag '{}' not found in registry", tag)
76 })?;
77
78 env.active_node_state_mut()?.current_fork_base = Some(block_info.number);
80
81 let mut sequence = Sequence::new(vec![
82 Box::new(SetForkBaseFromBlockInfo::new(block_info)),
83 Box::new(ProduceBlocks::new(self.num_blocks)),
84 ]);
85 sequence.execute(env).await
86 }
87 }
88 })
89 }
90}
91
92#[derive(Debug)]
94pub struct SetForkBase {
95 pub fork_base_block: u64,
97}
98
99#[derive(Debug)]
101pub struct SetForkBaseFromBlockInfo {
102 pub fork_base_info: BlockInfo,
104}
105
106impl SetForkBase {
107 pub const fn new(fork_base_block: u64) -> Self {
109 Self { fork_base_block }
110 }
111}
112
113impl SetForkBaseFromBlockInfo {
114 pub const fn new(fork_base_info: BlockInfo) -> Self {
116 Self { fork_base_info }
117 }
118}
119
120impl<Engine> Action<Engine> for SetForkBase
121where
122 Engine: EngineTypes,
123{
124 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
125 Box::pin(async move {
126 if env.node_clients.is_empty() {
127 return Err(eyre::eyre!("No node clients available"));
128 }
129
130 let rpc_client = &env.node_clients[0].rpc;
132 let fork_base_block = rpc_client
133 .request::<_, Option<Block>>(
134 "eth_getBlockByNumber",
135 (alloy_eips::BlockNumberOrTag::Number(self.fork_base_block), false),
136 )
137 .await?
138 .ok_or_else(|| eyre::eyre!("Fork base block {} not found", self.fork_base_block))?;
139
140 let active_node_state = env.active_node_state_mut()?;
142 active_node_state.current_block_info = Some(BlockInfo {
143 hash: fork_base_block.header.hash,
144 number: fork_base_block.header.number,
145 timestamp: fork_base_block.header.timestamp,
146 });
147
148 active_node_state.latest_header_time = fork_base_block.header.timestamp;
149
150 active_node_state.latest_fork_choice_state =
152 ForkchoiceState::same_hash(fork_base_block.header.hash);
153
154 debug!(
155 "Set fork base to block {} (hash: {})",
156 self.fork_base_block, fork_base_block.header.hash
157 );
158
159 Ok(())
160 })
161 }
162}
163
164impl<Engine> Action<Engine> for SetForkBaseFromBlockInfo
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 block_info = self.fork_base_info;
171
172 debug!(
173 "Set fork base from block info: block {} (hash: {})",
174 block_info.number, block_info.hash
175 );
176
177 let active_node_state = env.active_node_state_mut()?;
179 active_node_state.current_block_info = Some(block_info);
180 active_node_state.latest_header_time = block_info.timestamp;
181
182 active_node_state.latest_fork_choice_state =
184 ForkchoiceState::same_hash(block_info.hash);
185
186 debug!("Set fork base to block {} (hash: {})", block_info.number, block_info.hash);
187
188 Ok(())
189 })
190 }
191}
192
193#[derive(Debug)]
195pub struct ValidateFork {
196 pub fork_base_number: u64,
198}
199
200impl ValidateFork {
201 pub const fn new(fork_base_number: u64) -> Self {
203 Self { fork_base_number }
204 }
205}
206
207impl<Engine> Action<Engine> for ValidateFork
208where
209 Engine: EngineTypes,
210{
211 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
212 Box::pin(async move {
213 let current_block_info = env
214 .current_block_info()
215 .ok_or_else(|| eyre::eyre!("No current block information available"))?;
216
217 if current_block_info.number < self.fork_base_number {
219 return Err(eyre::eyre!(
220 "Fork validation failed: current block number {} is behind fork base {}",
221 current_block_info.number,
222 self.fork_base_number
223 ));
224 }
225
226 let fork_base_hash =
229 env.active_node_state()?.latest_fork_choice_state.finalized_block_hash;
230
231 let rpc_client = &env.node_clients[0].rpc;
233 let mut current_hash = current_block_info.hash;
234 let mut current_number = current_block_info.number;
235
236 while current_number > self.fork_base_number {
238 let block = rpc_client
239 .request::<_, Option<Block>>("eth_getBlockByHash", (current_hash, false))
240 .await?
241 .ok_or_else(|| {
242 eyre::eyre!(
243 "Block with hash {} not found during fork validation",
244 current_hash
245 )
246 })?;
247
248 current_hash = block.header.parent_hash;
249 current_number = block.header.number.saturating_sub(1);
250 }
251
252 if current_hash != fork_base_hash {
254 return Err(eyre::eyre!(
255 "Fork validation failed: expected fork base hash {}, but found {} at block {}",
256 fork_base_hash,
257 current_hash,
258 current_number
259 ));
260 }
261
262 debug!(
263 "Fork validation successful: tip block {} is descendant of fork base {} ({})",
264 current_block_info.number, self.fork_base_number, fork_base_hash
265 );
266
267 Ok(())
268 })
269 }
270}