reth_e2e_test_utils/testsuite/actions/
fork.rs

1//! Fork creation actions for the e2e testing framework.
2
3use 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/// Fork base target for fork creation
18#[derive(Debug, Clone)]
19pub enum ForkBase {
20    /// Block number
21    Number(u64),
22    /// Tagged block reference
23    Tag(String),
24}
25
26/// Action to create a fork from a specified block and produce blocks on top
27#[derive(Debug)]
28pub struct CreateFork<Engine> {
29    /// Fork base specification (either block number or tag)
30    pub fork_base: ForkBase,
31    /// Number of blocks to produce on top of the fork base
32    pub num_blocks: u64,
33    /// Tracks engine type
34    _phantom: PhantomData<Engine>,
35}
36
37impl<Engine> CreateFork<Engine> {
38    /// Create a new `CreateFork` action from a block number
39    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    /// Create a new `CreateFork` action from a tagged block
48    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            // resolve the fork base and execute the appropriate sequence
63            match &self.fork_base {
64                ForkBase::Number(block_number) => {
65                    // store the fork base for later validation on the active node
66                    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                    // store the fork base for later validation on the active node
81                    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/// Sub-action to set the fork base block in the environment
95#[derive(Debug)]
96pub struct SetForkBase {
97    /// Block number to use as the base of the fork
98    pub fork_base_block: u64,
99}
100
101/// Sub-action to set the fork base block from existing block info
102#[derive(Debug)]
103pub struct SetForkBaseFromBlockInfo {
104    /// Complete block info to use as the base of the fork
105    pub fork_base_info: BlockInfo,
106}
107
108impl SetForkBase {
109    /// Create a new `SetForkBase` action
110    pub const fn new(fork_base_block: u64) -> Self {
111        Self { fork_base_block }
112    }
113}
114
115impl SetForkBaseFromBlockInfo {
116    /// Create a new `SetForkBaseFromBlockInfo` action
117    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            // get the block at the fork base number to establish the fork point
133            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            // update active node state to point to the fork base block
150            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            // update fork choice state to the fork base
160            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            // update active node state to point to the fork base block
190            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            // update fork choice state to the fork base
195            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/// Sub-action to validate that a fork was created correctly
209#[derive(Debug)]
210pub struct ValidateFork {
211    /// Number of the fork base block (stored here since we need it for validation)
212    pub fork_base_number: u64,
213}
214
215impl ValidateFork {
216    /// Create a new `ValidateFork` action
217    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            // verify that the current tip is at or ahead of the fork base
233            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            // get the fork base hash from the environment's fork choice state
242            // we assume the fork choice state was set correctly by SetForkBase
243            let fork_base_hash =
244                env.active_node_state()?.latest_fork_choice_state.finalized_block_hash;
245
246            // trace back from current tip to verify it's a descendant of the fork base
247            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            // walk backwards through the chain until we reach the fork base
252            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            // verify we reached the expected fork base
271            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}