Skip to main content

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 =
161                ForkchoiceState::same_hash(fork_base_block.header.hash);
162
163            debug!(
164                "Set fork base to block {} (hash: {})",
165                self.fork_base_block, fork_base_block.header.hash
166            );
167
168            Ok(())
169        })
170    }
171}
172
173impl<Engine> Action<Engine> for SetForkBaseFromBlockInfo
174where
175    Engine: EngineTypes,
176{
177    fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
178        Box::pin(async move {
179            let block_info = self.fork_base_info;
180
181            debug!(
182                "Set fork base from block info: block {} (hash: {})",
183                block_info.number, block_info.hash
184            );
185
186            // update active node state to point to the fork base block
187            let active_node_state = env.active_node_state_mut()?;
188            active_node_state.current_block_info = Some(block_info);
189            active_node_state.latest_header_time = block_info.timestamp;
190
191            // update fork choice state to the fork base
192            active_node_state.latest_fork_choice_state =
193                ForkchoiceState::same_hash(block_info.hash);
194
195            debug!("Set fork base to block {} (hash: {})", block_info.number, block_info.hash);
196
197            Ok(())
198        })
199    }
200}
201
202/// Sub-action to validate that a fork was created correctly
203#[derive(Debug)]
204pub struct ValidateFork {
205    /// Number of the fork base block (stored here since we need it for validation)
206    pub fork_base_number: u64,
207}
208
209impl ValidateFork {
210    /// Create a new `ValidateFork` action
211    pub const fn new(fork_base_number: u64) -> Self {
212        Self { fork_base_number }
213    }
214}
215
216impl<Engine> Action<Engine> for ValidateFork
217where
218    Engine: EngineTypes,
219{
220    fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
221        Box::pin(async move {
222            let current_block_info = env
223                .current_block_info()
224                .ok_or_else(|| eyre::eyre!("No current block information available"))?;
225
226            // verify that the current tip is at or ahead of the fork base
227            if current_block_info.number < self.fork_base_number {
228                return Err(eyre::eyre!(
229                    "Fork validation failed: current block number {} is behind fork base {}",
230                    current_block_info.number,
231                    self.fork_base_number
232                ));
233            }
234
235            // get the fork base hash from the environment's fork choice state
236            // we assume the fork choice state was set correctly by SetForkBase
237            let fork_base_hash =
238                env.active_node_state()?.latest_fork_choice_state.finalized_block_hash;
239
240            // trace back from current tip to verify it's a descendant of the fork base
241            let rpc_client = &env.node_clients[0].rpc;
242            let mut current_hash = current_block_info.hash;
243            let mut current_number = current_block_info.number;
244
245            // walk backwards through the chain until we reach the fork base
246            while current_number > self.fork_base_number {
247                let block = EthApiClient::<
248                    TransactionRequest,
249                    Transaction,
250                    Block,
251                    Receipt,
252                    Header,
253                    TransactionSigned,
254                >::block_by_hash(rpc_client, current_hash, false)
255                .await?
256                .ok_or_else(|| {
257                    eyre::eyre!("Block with hash {} not found during fork validation", current_hash)
258                })?;
259
260                current_hash = block.header.parent_hash;
261                current_number = block.header.number.saturating_sub(1);
262            }
263
264            // verify we reached the expected fork base
265            if current_hash != fork_base_hash {
266                return Err(eyre::eyre!(
267                    "Fork validation failed: expected fork base hash {}, but found {} at block {}",
268                    fork_base_hash,
269                    current_hash,
270                    current_number
271                ));
272            }
273
274            debug!(
275                "Fork validation successful: tip block {} is descendant of fork base {} ({})",
276                current_block_info.number, self.fork_base_number, fork_base_hash
277            );
278
279            Ok(())
280        })
281    }
282}