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_ethereum_primitives::TransactionSigned;
12use reth_node_api::{EngineTypes, PayloadTypes};
13use reth_rpc_api::clients::EthApiClient;
14use std::marker::PhantomData;
15use tracing::debug;
16
17#[derive(Debug, Clone)]
19pub enum ForkBase {
20 Number(u64),
22 Tag(String),
24}
25
26#[derive(Debug)]
28pub struct CreateFork<Engine> {
29 pub fork_base: ForkBase,
31 pub num_blocks: u64,
33 _phantom: PhantomData<Engine>,
35}
36
37impl<Engine> CreateFork<Engine> {
38 pub fn new(fork_base_block: u64, num_blocks: u64) -> Self {
40 Self {
41 fork_base: ForkBase::Number(fork_base_block),
42 num_blocks,
43 _phantom: Default::default(),
44 }
45 }
46
47 pub fn new_from_tag(tag: impl Into<String>, num_blocks: u64) -> Self {
49 Self { fork_base: ForkBase::Tag(tag.into()), num_blocks, _phantom: Default::default() }
50 }
51}
52
53impl<Engine> Action<Engine> for CreateFork<Engine>
54where
55 Engine: EngineTypes + PayloadTypes,
56 Engine::PayloadAttributes: From<PayloadAttributes> + Clone,
57 Engine::ExecutionPayloadEnvelopeV3:
58 Into<alloy_rpc_types_engine::payload::ExecutionPayloadEnvelopeV3>,
59{
60 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
61 Box::pin(async move {
62 match &self.fork_base {
64 ForkBase::Number(block_number) => {
65 env.active_node_state_mut()?.current_fork_base = Some(*block_number);
67
68 let mut sequence = Sequence::new(vec![
69 Box::new(SetForkBase::new(*block_number)),
70 Box::new(ProduceBlocks::new(self.num_blocks)),
71 ]);
72 sequence.execute(env).await
73 }
74 ForkBase::Tag(tag) => {
75 let (block_info, _node_idx) =
76 env.block_registry.get(tag).copied().ok_or_else(|| {
77 eyre::eyre!("Block tag '{}' not found in registry", tag)
78 })?;
79
80 env.active_node_state_mut()?.current_fork_base = Some(block_info.number);
82
83 let mut sequence = Sequence::new(vec![
84 Box::new(SetForkBaseFromBlockInfo::new(block_info)),
85 Box::new(ProduceBlocks::new(self.num_blocks)),
86 ]);
87 sequence.execute(env).await
88 }
89 }
90 })
91 }
92}
93
94#[derive(Debug)]
96pub struct SetForkBase {
97 pub fork_base_block: u64,
99}
100
101#[derive(Debug)]
103pub struct SetForkBaseFromBlockInfo {
104 pub fork_base_info: BlockInfo,
106}
107
108impl SetForkBase {
109 pub const fn new(fork_base_block: u64) -> Self {
111 Self { fork_base_block }
112 }
113}
114
115impl SetForkBaseFromBlockInfo {
116 pub const fn new(fork_base_info: BlockInfo) -> Self {
118 Self { fork_base_info }
119 }
120}
121
122impl<Engine> Action<Engine> for SetForkBase
123where
124 Engine: EngineTypes,
125{
126 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
127 Box::pin(async move {
128 if env.node_clients.is_empty() {
129 return Err(eyre::eyre!("No node clients available"));
130 }
131
132 let rpc_client = &env.node_clients[0].rpc;
134 let fork_base_block = EthApiClient::<
135 TransactionRequest,
136 Transaction,
137 Block,
138 Receipt,
139 Header,
140 TransactionSigned,
141 >::block_by_number(
142 rpc_client,
143 alloy_eips::BlockNumberOrTag::Number(self.fork_base_block),
144 false,
145 )
146 .await?
147 .ok_or_else(|| eyre::eyre!("Fork base block {} not found", self.fork_base_block))?;
148
149 let active_node_state = env.active_node_state_mut()?;
151 active_node_state.current_block_info = Some(BlockInfo {
152 hash: fork_base_block.header.hash,
153 number: fork_base_block.header.number,
154 timestamp: fork_base_block.header.timestamp,
155 });
156
157 active_node_state.latest_header_time = fork_base_block.header.timestamp;
158
159 active_node_state.latest_fork_choice_state = ForkchoiceState {
161 head_block_hash: fork_base_block.header.hash,
162 safe_block_hash: fork_base_block.header.hash,
163 finalized_block_hash: fork_base_block.header.hash,
164 };
165
166 debug!(
167 "Set fork base to block {} (hash: {})",
168 self.fork_base_block, fork_base_block.header.hash
169 );
170
171 Ok(())
172 })
173 }
174}
175
176impl<Engine> Action<Engine> for SetForkBaseFromBlockInfo
177where
178 Engine: EngineTypes,
179{
180 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
181 Box::pin(async move {
182 let block_info = self.fork_base_info;
183
184 debug!(
185 "Set fork base from block info: block {} (hash: {})",
186 block_info.number, block_info.hash
187 );
188
189 let active_node_state = env.active_node_state_mut()?;
191 active_node_state.current_block_info = Some(block_info);
192 active_node_state.latest_header_time = block_info.timestamp;
193
194 active_node_state.latest_fork_choice_state = ForkchoiceState {
196 head_block_hash: block_info.hash,
197 safe_block_hash: block_info.hash,
198 finalized_block_hash: block_info.hash,
199 };
200
201 debug!("Set fork base to block {} (hash: {})", block_info.number, block_info.hash);
202
203 Ok(())
204 })
205 }
206}
207
208#[derive(Debug)]
210pub struct ValidateFork {
211 pub fork_base_number: u64,
213}
214
215impl ValidateFork {
216 pub const fn new(fork_base_number: u64) -> Self {
218 Self { fork_base_number }
219 }
220}
221
222impl<Engine> Action<Engine> for ValidateFork
223where
224 Engine: EngineTypes,
225{
226 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
227 Box::pin(async move {
228 let current_block_info = env
229 .current_block_info()
230 .ok_or_else(|| eyre::eyre!("No current block information available"))?;
231
232 if current_block_info.number < self.fork_base_number {
234 return Err(eyre::eyre!(
235 "Fork validation failed: current block number {} is behind fork base {}",
236 current_block_info.number,
237 self.fork_base_number
238 ));
239 }
240
241 let fork_base_hash =
244 env.active_node_state()?.latest_fork_choice_state.finalized_block_hash;
245
246 let rpc_client = &env.node_clients[0].rpc;
248 let mut current_hash = current_block_info.hash;
249 let mut current_number = current_block_info.number;
250
251 while current_number > self.fork_base_number {
253 let block = EthApiClient::<
254 TransactionRequest,
255 Transaction,
256 Block,
257 Receipt,
258 Header,
259 TransactionSigned,
260 >::block_by_hash(rpc_client, current_hash, false)
261 .await?
262 .ok_or_else(|| {
263 eyre::eyre!("Block with hash {} not found during fork validation", current_hash)
264 })?;
265
266 current_hash = block.header.parent_hash;
267 current_number = block.header.number.saturating_sub(1);
268 }
269
270 if current_hash != fork_base_hash {
272 return Err(eyre::eyre!(
273 "Fork validation failed: expected fork base hash {}, but found {} at block {}",
274 fork_base_hash,
275 current_hash,
276 current_number
277 ));
278 }
279
280 debug!(
281 "Fork validation successful: tip block {} is descendant of fork base {} ({})",
282 current_block_info.number, self.fork_base_number, fork_base_hash
283 );
284
285 Ok(())
286 })
287 }
288}