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