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, Header, Receipt, Transaction, TransactionRequest};
9use eyre::Result;
10use futures_util::future::BoxFuture;
11use reth_node_api::{EngineTypes, PayloadTypes};
12use reth_rpc_api::clients::EthApiClient;
13use std::marker::PhantomData;
14use tracing::debug;
15
16#[derive(Debug, Clone)]
18pub enum ForkBase {
19 Number(u64),
21 Tag(String),
23}
24
25#[derive(Debug)]
27pub struct CreateFork<Engine> {
28 pub fork_base: ForkBase,
30 pub num_blocks: u64,
32 _phantom: PhantomData<Engine>,
34}
35
36impl<Engine> CreateFork<Engine> {
37 pub fn new(fork_base_block: u64, num_blocks: u64) -> Self {
39 Self {
40 fork_base: ForkBase::Number(fork_base_block),
41 num_blocks,
42 _phantom: Default::default(),
43 }
44 }
45
46 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() }
49 }
50}
51
52impl<Engine> Action<Engine> for CreateFork<Engine>
53where
54 Engine: EngineTypes + PayloadTypes,
55 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
56 Engine::ExecutionPayloadEnvelopeV3:
57 Into<alloy_rpc_types_engine::payload::ExecutionPayloadEnvelopeV3>,
58{
59 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
60 Box::pin(async move {
61 match &self.fork_base {
63 ForkBase::Number(block_number) => {
64 env.active_node_state_mut()?.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, _node_idx) =
75 env.block_registry.get(tag).copied().ok_or_else(|| {
76 eyre::eyre!("Block tag '{}' not found in registry", tag)
77 })?;
78
79 env.active_node_state_mut()?.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 }
89 })
90 }
91}
92
93#[derive(Debug)]
95pub struct SetForkBase {
96 pub fork_base_block: u64,
98}
99
100#[derive(Debug)]
102pub struct SetForkBaseFromBlockInfo {
103 pub fork_base_info: BlockInfo,
105}
106
107impl SetForkBase {
108 pub const fn new(fork_base_block: u64) -> Self {
110 Self { fork_base_block }
111 }
112}
113
114impl SetForkBaseFromBlockInfo {
115 pub const fn new(fork_base_info: BlockInfo) -> Self {
117 Self { fork_base_info }
118 }
119}
120
121impl<Engine> Action<Engine> for SetForkBase
122where
123 Engine: EngineTypes,
124{
125 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
126 Box::pin(async move {
127 if env.node_clients.is_empty() {
128 return Err(eyre::eyre!("No node clients available"));
129 }
130
131 let rpc_client = &env.node_clients[0].rpc;
133 let fork_base_block = EthApiClient::<
134 TransactionRequest,
135 Transaction,
136 Block,
137 Receipt,
138 Header,
139 >::block_by_number(
140 rpc_client,
141 alloy_eips::BlockNumberOrTag::Number(self.fork_base_block),
142 false,
143 )
144 .await?
145 .ok_or_else(|| eyre::eyre!("Fork base block {} not found", self.fork_base_block))?;
146
147 let active_node_state = env.active_node_state_mut()?;
149 active_node_state.current_block_info = Some(BlockInfo {
150 hash: fork_base_block.header.hash,
151 number: fork_base_block.header.number,
152 timestamp: fork_base_block.header.timestamp,
153 });
154
155 active_node_state.latest_header_time = fork_base_block.header.timestamp;
156
157 active_node_state.latest_fork_choice_state = ForkchoiceState {
159 head_block_hash: fork_base_block.header.hash,
160 safe_block_hash: fork_base_block.header.hash,
161 finalized_block_hash: fork_base_block.header.hash,
162 };
163
164 debug!(
165 "Set fork base to block {} (hash: {})",
166 self.fork_base_block, fork_base_block.header.hash
167 );
168
169 Ok(())
170 })
171 }
172}
173
174impl<Engine> Action<Engine> for SetForkBaseFromBlockInfo
175where
176 Engine: EngineTypes,
177{
178 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
179 Box::pin(async move {
180 let block_info = self.fork_base_info;
181
182 debug!(
183 "Set fork base from block info: block {} (hash: {})",
184 block_info.number, block_info.hash
185 );
186
187 let active_node_state = env.active_node_state_mut()?;
189 active_node_state.current_block_info = Some(block_info);
190 active_node_state.latest_header_time = block_info.timestamp;
191
192 active_node_state.latest_fork_choice_state = ForkchoiceState {
194 head_block_hash: block_info.hash,
195 safe_block_hash: block_info.hash,
196 finalized_block_hash: block_info.hash,
197 };
198
199 debug!("Set fork base to block {} (hash: {})", block_info.number, block_info.hash);
200
201 Ok(())
202 })
203 }
204}
205
206#[derive(Debug)]
208pub struct ValidateFork {
209 pub fork_base_number: u64,
211}
212
213impl ValidateFork {
214 pub const fn new(fork_base_number: u64) -> Self {
216 Self { fork_base_number }
217 }
218}
219
220impl<Engine> Action<Engine> for ValidateFork
221where
222 Engine: EngineTypes,
223{
224 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
225 Box::pin(async move {
226 let current_block_info = env
227 .current_block_info()
228 .ok_or_else(|| eyre::eyre!("No current block information available"))?;
229
230 if current_block_info.number < self.fork_base_number {
232 return Err(eyre::eyre!(
233 "Fork validation failed: current block number {} is behind fork base {}",
234 current_block_info.number,
235 self.fork_base_number
236 ));
237 }
238
239 let fork_base_hash =
242 env.active_node_state()?.latest_fork_choice_state.finalized_block_hash;
243
244 let rpc_client = &env.node_clients[0].rpc;
246 let mut current_hash = current_block_info.hash;
247 let mut current_number = current_block_info.number;
248
249 while current_number > self.fork_base_number {
251 let block = EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::block_by_hash(
252 rpc_client,
253 current_hash,
254 false,
255 )
256 .await?
257 .ok_or_else(|| {
258 eyre::eyre!("Block with hash {} not found during fork validation", current_hash)
259 })?;
260
261 current_hash = block.header.parent_hash;
262 current_number = block.header.number.saturating_sub(1);
263 }
264
265 if current_hash != fork_base_hash {
267 return Err(eyre::eyre!(
268 "Fork validation failed: expected fork base hash {}, but found {} at block {}",
269 fork_base_hash,
270 current_hash,
271 current_number
272 ));
273 }
274
275 debug!(
276 "Fork validation successful: tip block {} is descendant of fork base {} ({})",
277 current_block_info.number, self.fork_base_number, fork_base_hash
278 );
279
280 Ok(())
281 })
282 }
283}