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 if let Some(fork_base) = active_state.current_fork_base {
179 debug!(
180 "MakeCanonical: Adding fork validation from base block {}",
181 fork_base
182 );
183 actions.push(Box::new(ValidateFork::new(fork_base)));
184 env.active_node_state_mut()?.current_fork_base = None;
186 }
187 }
188
189 let mut sequence = Sequence::new(actions);
190 sequence.execute(env).await
191 }
192 })
193 }
194}
195
196#[derive(Debug)]
198pub struct CaptureBlock {
199 pub tag: String,
201}
202
203impl CaptureBlock {
204 pub fn new(tag: impl Into<String>) -> Self {
206 Self { tag: tag.into() }
207 }
208}
209
210impl<Engine> Action<Engine> for CaptureBlock
211where
212 Engine: EngineTypes,
213{
214 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
215 Box::pin(async move {
216 let current_block = env
217 .current_block_info()
218 .ok_or_else(|| eyre::eyre!("No current block information available"))?;
219
220 env.block_registry.insert(self.tag.clone(), (current_block, env.active_node_idx));
221
222 debug!(
223 "Captured block {} (hash: {}) from active node {} with tag '{}'",
224 current_block.number, current_block.hash, env.active_node_idx, self.tag
225 );
226
227 Ok(())
228 })
229 }
230}
231
232pub fn validate_fcu_response(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
234 match &response.payload_status.status {
235 PayloadStatusEnum::Valid => {
236 debug!("{}: FCU accepted as valid", context);
237 Ok(())
238 }
239 PayloadStatusEnum::Invalid { validation_error } => {
240 Err(eyre::eyre!("{}: FCU rejected as invalid: {:?}", context, validation_error))
241 }
242 PayloadStatusEnum::Syncing => {
243 debug!("{}: FCU accepted, node is syncing", context);
244 Ok(())
245 }
246 PayloadStatusEnum::Accepted => {
247 debug!("{}: FCU accepted for processing", context);
248 Ok(())
249 }
250 }
251}
252
253pub fn expect_fcu_valid(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
255 match &response.payload_status.status {
256 PayloadStatusEnum::Valid => {
257 debug!("{}: FCU status is VALID as expected.", context);
258 Ok(())
259 }
260 other_status => {
261 Err(eyre::eyre!("{}: Expected FCU status VALID, but got {:?}", context, other_status))
262 }
263 }
264}
265
266pub fn expect_fcu_invalid(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
268 match &response.payload_status.status {
269 PayloadStatusEnum::Invalid { validation_error } => {
270 debug!("{}: FCU status is INVALID as expected: {:?}", context, validation_error);
271 Ok(())
272 }
273 other_status => {
274 Err(eyre::eyre!("{}: Expected FCU status INVALID, but got {:?}", context, other_status))
275 }
276 }
277}
278
279pub fn expect_fcu_syncing_or_accepted(response: &ForkchoiceUpdated, context: &str) -> Result<()> {
281 match &response.payload_status.status {
282 PayloadStatusEnum::Syncing => {
283 debug!("{}: FCU status is SYNCING as expected (SYNCING or ACCEPTED).", context);
284 Ok(())
285 }
286 PayloadStatusEnum::Accepted => {
287 debug!("{}: FCU status is ACCEPTED as expected (SYNCING or ACCEPTED).", context);
288 Ok(())
289 }
290 other_status => Err(eyre::eyre!(
291 "{}: Expected FCU status SYNCING or ACCEPTED, but got {:?}",
292 context,
293 other_status
294 )),
295 }
296}
297
298pub fn expect_fcu_not_syncing_or_accepted(
300 response: &ForkchoiceUpdated,
301 context: &str,
302) -> Result<()> {
303 match &response.payload_status.status {
304 PayloadStatusEnum::Valid => {
305 debug!("{}: FCU status is VALID as expected (not SYNCING or ACCEPTED).", context);
306 Ok(())
307 }
308 PayloadStatusEnum::Invalid { validation_error } => {
309 debug!(
310 "{}: FCU status is INVALID as expected (not SYNCING or ACCEPTED): {:?}",
311 context, validation_error
312 );
313 Ok(())
314 }
315 syncing_or_accepted_status @ (PayloadStatusEnum::Syncing | PayloadStatusEnum::Accepted) => {
316 Err(eyre::eyre!(
317 "{}: Expected FCU status not SYNCING or ACCEPTED (i.e., VALID or INVALID), but got {:?}",
318 context,
319 syncing_or_accepted_status
320 ))
321 }
322 }
323}