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