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_node_api::{EngineTypes, PayloadTypes};
12use reth_rpc_api::clients::EthApiClient;
13use std::marker::PhantomData;
14use tracing::debug;
15
16/// Fork base target for fork creation
17#[derive(Debug, Clone)]
18pub enum ForkBase {
19    /// Block number
20    Number(u64),
21    /// Tagged block reference
22    Tag(String),
23}
24
25/// Action to create a fork from a specified block and produce blocks on top
26#[derive(Debug)]
27pub struct CreateFork<Engine> {
28    /// Fork base specification (either block number or tag)
29    pub fork_base: ForkBase,
30    /// Number of blocks to produce on top of the fork base
31    pub num_blocks: u64,
32    /// Tracks engine type
33    _phantom: PhantomData<Engine>,
34}
35
36impl<Engine> CreateFork<Engine> {
37    /// Create a new `CreateFork` action from a block number
38    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    /// Create a new `CreateFork` action from a tagged block
47    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            // resolve the fork base and execute the appropriate sequence
62            match &self.fork_base {
63                ForkBase::Number(block_number) => {
64                    // store the fork base for later validation on the active node
65                    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                    // store the fork base for later validation on the active node
80                    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/// Sub-action to set the fork base block in the environment
94#[derive(Debug)]
95pub struct SetForkBase {
96    /// Block number to use as the base of the fork
97    pub fork_base_block: u64,
98}
99
100/// Sub-action to set the fork base block from existing block info
101#[derive(Debug)]
102pub struct SetForkBaseFromBlockInfo {
103    /// Complete block info to use as the base of the fork
104    pub fork_base_info: BlockInfo,
105}
106
107impl SetForkBase {
108    /// Create a new `SetForkBase` action
109    pub const fn new(fork_base_block: u64) -> Self {
110        Self { fork_base_block }
111    }
112}
113
114impl SetForkBaseFromBlockInfo {
115    /// Create a new `SetForkBaseFromBlockInfo` action
116    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            // get the block at the fork base number to establish the fork point
132            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            // update active node state to point to the fork base block
148            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            // update fork choice state to the fork base
158            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            // update active node state to point to the fork base block
188            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            // update fork choice state to the fork base
193            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/// Sub-action to validate that a fork was created correctly
207#[derive(Debug)]
208pub struct ValidateFork {
209    /// Number of the fork base block (stored here since we need it for validation)
210    pub fork_base_number: u64,
211}
212
213impl ValidateFork {
214    /// Create a new `ValidateFork` action
215    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            // verify that the current tip is at or ahead of the fork base
231            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            // get the fork base hash from the environment's fork choice state
240            // we assume the fork choice state was set correctly by SetForkBase
241            let fork_base_hash =
242                env.active_node_state()?.latest_fork_choice_state.finalized_block_hash;
243
244            // trace back from current tip to verify it's a descendant of the fork base
245            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            // walk backwards through the chain until we reach the fork base
250            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            // verify we reached the expected fork base
266            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}