reth_e2e_test_utils/testsuite/actions/
mod.rs

1//! Actions that can be performed in tests.
2
3use 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
34/// An action that can be performed on an instance.
35///
36/// Actions execute operations and potentially make assertions in a single step.
37/// The action name indicates what it does (e.g., `AssertMineBlock` would both
38/// mine a block and assert it worked).
39pub trait Action<I>: Send + 'static
40where
41    I: EngineTypes,
42{
43    /// Executes the action
44    fn execute<'a>(&'a mut self, env: &'a mut Environment<I>) -> BoxFuture<'a, Result<()>>;
45}
46
47/// Simplified action container for storage in tests
48#[expect(missing_debug_implementations)]
49pub struct ActionBox<I>(Box<dyn Action<I>>);
50
51impl<I> ActionBox<I>
52where
53    I: EngineTypes + 'static,
54{
55    /// Constructor for [`ActionBox`].
56    pub fn new<A: Action<I>>(action: A) -> Self {
57        Self(Box::new(action))
58    }
59
60    /// Executes an [`ActionBox`] with the given [`Environment`] reference.
61    pub async fn execute(mut self, env: &mut Environment<I>) -> Result<()> {
62        self.0.execute(env).await
63    }
64}
65
66/// Implementation of `Action` for any function/closure that takes an Environment
67/// reference and returns a Future resolving to Result<()>.
68///
69/// This allows using closures directly as actions with `.with_action(async move |env| {...})`.
70impl<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/// Run a sequence of actions in series.
82#[expect(missing_debug_implementations)]
83pub struct Sequence<I> {
84    /// Actions to execute in sequence
85    pub actions: Vec<Box<dyn Action<I>>>,
86}
87
88impl<I> Sequence<I> {
89    /// Create a new sequence of actions
90    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            // Execute each action in sequence
102            for action in &mut self.actions {
103                action.execute(env).await?;
104            }
105
106            Ok(())
107        })
108    }
109}
110
111/// Action that makes the current latest block canonical by broadcasting a forkchoice update
112#[derive(Debug, Default)]
113pub struct MakeCanonical {
114    /// If true, only send to the active node. If false, broadcast to all nodes.
115    active_node_only: bool,
116}
117
118impl MakeCanonical {
119    /// Create a new `MakeCanonical` action
120    pub const fn new() -> Self {
121        Self { active_node_only: false }
122    }
123
124    /// Create a new `MakeCanonical` action that only applies to the active node
125    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                // Only update the active node
141                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                // Original broadcast behavior
171                let mut actions: Vec<Box<dyn Action<Engine>>> = vec![
172                    Box::new(BroadcastLatestForkchoice::default()),
173                    Box::new(UpdateBlockInfo::default()),
174                ];
175
176                // if we're on a fork, validate it now that it's canonical
177                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                    // clear the fork base since we're now canonical
183                    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/// Action that captures the current block and tags it with a name for later reference
194#[derive(Debug)]
195pub struct CaptureBlock {
196    /// Tag name to associate with the current block
197    pub tag: String,
198}
199
200impl CaptureBlock {
201    /// Create a new `CaptureBlock` action
202    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
229/// Validates a forkchoice update response and returns an error if invalid
230pub 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
250/// Expects that the `ForkchoiceUpdated` response status is VALID.
251pub 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
263/// Expects that the `ForkchoiceUpdated` response status is INVALID.
264pub 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
276/// Expects that the `ForkchoiceUpdated` response status is either SYNCING or ACCEPTED.
277pub 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
295/// Expects that the `ForkchoiceUpdated` response status is not SYNCING and not ACCEPTED.
296pub 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}