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