reth_e2e_test_utils/testsuite/actions/
mod.rs1use crate::testsuite::Environment;
4use alloy_primitives::B256;
5use alloy_rpc_types_engine::{ForkchoiceState, ForkchoiceUpdated, PayloadStatusEnum};
6use eyre::Result;
7use futures_util::future::BoxFuture;
8use reth_node_api::EngineTypes;
9use reth_rpc_api::clients::EngineApiClient;
10use std::future::Future;
11use tracing::debug;
12
13pub mod custom_fcu;
14pub mod engine_api;
15pub mod fork;
16pub mod node_ops;
17pub mod produce_blocks;
18pub mod reorg;
19
20pub use custom_fcu::{BlockReference, FinalizeBlock, SendForkchoiceUpdate};
21pub use engine_api::{ExpectedPayloadStatus, SendNewPayload, SendNewPayloads};
22pub use fork::{CreateFork, ForkBase, SetForkBase, SetForkBaseFromBlockInfo, ValidateFork};
23pub use node_ops::{
24 AssertChainTip, CaptureBlockOnNode, CompareNodeChainTips, SelectActiveNode, ValidateBlockTag,
25 WaitForSync,
26};
27pub use produce_blocks::{
28 AssertMineBlock, BroadcastLatestForkchoice, BroadcastNextNewPayload, CheckPayloadAccepted,
29 ExpectFcuStatus, GenerateNextPayload, GeneratePayloadAttributes, PickNextBlockProducer,
30 ProduceBlocks, ProduceBlocksLocally, ProduceInvalidBlocks, TestFcuToTag, UpdateBlockInfo,
31 UpdateBlockInfoToLatestPayload, ValidateCanonicalTag,
32};
33pub use reorg::{ReorgTarget, ReorgTo, SetReorgTarget};
34
35pub trait Action<I>: Send + 'static
41where
42 I: EngineTypes,
43{
44 fn execute<'a>(&'a mut self, env: &'a mut Environment<I>) -> BoxFuture<'a, Result<()>>;
46}
47
48#[expect(missing_debug_implementations)]
50pub struct ActionBox<I>(Box<dyn Action<I>>);
51
52impl<I> ActionBox<I>
53where
54 I: EngineTypes + 'static,
55{
56 pub fn new<A: Action<I>>(action: A) -> Self {
58 Self(Box::new(action))
59 }
60
61 pub async fn execute(mut self, env: &mut Environment<I>) -> Result<()> {
63 self.0.execute(env).await
64 }
65}
66
67impl<I, F, Fut> Action<I> for F
72where
73 I: EngineTypes,
74 F: FnMut(&Environment<I>) -> Fut + Send + 'static,
75 Fut: Future<Output = Result<()>> + Send + 'static,
76{
77 fn execute<'a>(&'a mut self, env: &'a mut Environment<I>) -> BoxFuture<'a, Result<()>> {
78 Box::pin(self(env))
79 }
80}
81
82#[expect(missing_debug_implementations)]
84pub struct Sequence<I> {
85 pub actions: Vec<Box<dyn Action<I>>>,
87}
88
89impl<I> Sequence<I> {
90 pub fn new(actions: Vec<Box<dyn Action<I>>>) -> Self {
92 Self { actions }
93 }
94}
95
96impl<I> Action<I> for Sequence<I>
97where
98 I: EngineTypes + Sync + Send + 'static,
99{
100 fn execute<'a>(&'a mut self, env: &'a mut Environment<I>) -> BoxFuture<'a, Result<()>> {
101 Box::pin(async move {
102 for action in &mut self.actions {
104 action.execute(env).await?;
105 }
106
107 Ok(())
108 })
109 }
110}
111
112#[derive(Debug, Default)]
114pub struct MakeCanonical {
115 active_node_only: bool,
117}
118
119impl MakeCanonical {
120 pub const fn new() -> Self {
122 Self { active_node_only: false }
123 }
124
125 pub const fn with_active_node() -> Self {
127 Self { active_node_only: true }
128 }
129}
130
131impl<Engine> Action<Engine> for MakeCanonical
132where
133 Engine: EngineTypes + reth_node_api::PayloadTypes,
134 Engine::PayloadAttributes: From<alloy_rpc_types_engine::PayloadAttributes> + Clone,
135 Engine::ExecutionPayloadEnvelopeV3:
136 Into<alloy_rpc_types_engine::payload::ExecutionPayloadEnvelopeV3>,
137{
138 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
139 Box::pin(async move {
140 if self.active_node_only {
141 let latest_block = env
143 .current_block_info()
144 .ok_or_else(|| eyre::eyre!("No latest block information available"))?;
145
146 let fork_choice_state = ForkchoiceState {
147 head_block_hash: latest_block.hash,
148 safe_block_hash: latest_block.hash,
149 finalized_block_hash: B256::ZERO,
153 };
154
155 let active_idx = env.active_node_idx;
156 let engine = env.node_clients[active_idx].engine.http_client();
157
158 let fcu_response = EngineApiClient::<Engine>::fork_choice_updated_v3(
159 &engine,
160 fork_choice_state,
161 None,
162 )
163 .await?;
164
165 debug!(
166 "Active node {}: Forkchoice update status: {:?}",
167 active_idx, fcu_response.payload_status.status
168 );
169
170 validate_fcu_response(&fcu_response, &format!("Active node {active_idx}"))?;
171
172 Ok(())
173 } else {
174 let mut actions: Vec<Box<dyn Action<Engine>>> = vec![
176 Box::new(BroadcastLatestForkchoice::default()),
177 Box::new(UpdateBlockInfo::default()),
178 ];
179
180 if let Ok(active_state) = env.active_node_state() &&
182 let Some(fork_base) = active_state.current_fork_base
183 {
184 debug!("MakeCanonical: Adding fork validation from base block {}", fork_base);
185 actions.push(Box::new(ValidateFork::new(fork_base)));
186 env.active_node_state_mut()?.current_fork_base = None;
188 }
189
190 let mut sequence = Sequence::new(actions);
191 sequence.execute(env).await
192 }
193 })
194 }
195}
196
197#[derive(Debug)]
199pub struct CaptureBlock {
200 pub tag: String,
202}
203
204impl CaptureBlock {
205 pub fn new(tag: impl Into<String>) -> Self {
207 Self { tag: tag.into() }
208 }
209}
210
211impl<Engine> Action<Engine> for CaptureBlock
212where
213 Engine: EngineTypes,
214{
215 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
216 Box::pin(async move {
217 let current_block = env
218 .current_block_info()
219 .ok_or_else(|| eyre::eyre!("No current block information available"))?;
220
221 env.block_registry.insert(self.tag.clone(), (current_block, env.active_node_idx));
222
223 debug!(
224 "Captured block {} (hash: {}) from active node {} with tag '{}'",
225 current_block.number, current_block.hash, env.active_node_idx, self.tag
226 );
227
228 Ok(())
229 })
230 }
231}
232
233pub fn validate_fcu_response(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
235 match &response.payload_status.status {
236 PayloadStatusEnum::Valid => {
237 debug!("{}: FCU accepted as valid", context);
238 Ok(())
239 }
240 PayloadStatusEnum::Invalid { validation_error } => {
241 Err(eyre::eyre!("{}: FCU rejected as invalid: {:?}", context, validation_error))
242 }
243 PayloadStatusEnum::Syncing => {
244 debug!("{}: FCU accepted, node is syncing", context);
245 Ok(())
246 }
247 PayloadStatusEnum::Accepted => {
248 debug!("{}: FCU accepted for processing", context);
249 Ok(())
250 }
251 }
252}
253
254pub fn expect_fcu_valid(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
256 match &response.payload_status.status {
257 PayloadStatusEnum::Valid => {
258 debug!("{}: FCU status is VALID as expected.", context);
259 Ok(())
260 }
261 other_status => {
262 Err(eyre::eyre!("{}: Expected FCU status VALID, but got {:?}", context, other_status))
263 }
264 }
265}
266
267pub fn expect_fcu_invalid(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
269 match &response.payload_status.status {
270 PayloadStatusEnum::Invalid { validation_error } => {
271 debug!("{}: FCU status is INVALID as expected: {:?}", context, validation_error);
272 Ok(())
273 }
274 other_status => {
275 Err(eyre::eyre!("{}: Expected FCU status INVALID, but got {:?}", context, other_status))
276 }
277 }
278}
279
280pub fn expect_fcu_syncing_or_accepted(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
282 match &response.payload_status.status {
283 PayloadStatusEnum::Syncing => {
284 debug!("{}: FCU status is SYNCING as expected (SYNCING or ACCEPTED).", context);
285 Ok(())
286 }
287 PayloadStatusEnum::Accepted => {
288 debug!("{}: FCU status is ACCEPTED as expected (SYNCING or ACCEPTED).", context);
289 Ok(())
290 }
291 other_status => Err(eyre::eyre!(
292 "{}: Expected FCU status SYNCING or ACCEPTED, but got {:?}",
293 context,
294 other_status
295 )),
296 }
297}
298
299pub fn expect_fcu_not_syncing_or_accepted(
301 response: &ForkchoiceUpdated,
302 context: &str,
303) -> Result<()> {
304 match &response.payload_status.status {
305 PayloadStatusEnum::Valid => {
306 debug!("{}: FCU status is VALID as expected (not SYNCING or ACCEPTED).", context);
307 Ok(())
308 }
309 PayloadStatusEnum::Invalid { validation_error } => {
310 debug!(
311 "{}: FCU status is INVALID as expected (not SYNCING or ACCEPTED): {:?}",
312 context, validation_error
313 );
314 Ok(())
315 }
316 syncing_or_accepted_status @ (PayloadStatusEnum::Syncing | PayloadStatusEnum::Accepted) => {
317 Err(eyre::eyre!(
318 "{}: Expected FCU status not SYNCING or ACCEPTED (i.e., VALID or INVALID), but got {:?}",
319 context,
320 syncing_or_accepted_status
321 ))
322 }
323 }
324}