Skip to main content

reth_e2e_test_utils/testsuite/actions/
mod.rs

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