reth_e2e_test_utils/testsuite/actions/
custom_fcu.rs

1//! Custom forkchoice update actions for testing specific FCU scenarios.
2
3use crate::testsuite::{Action, Environment};
4use alloy_primitives::B256;
5use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};
6use eyre::Result;
7use futures_util::future::BoxFuture;
8use reth_node_api::EngineTypes;
9use reth_rpc_api::clients::EngineApiClient;
10use std::marker::PhantomData;
11use tracing::debug;
12
13/// Reference to a block for forkchoice update
14#[derive(Debug, Clone)]
15pub enum BlockReference {
16    /// Direct block hash
17    Hash(B256),
18    /// Tagged block reference
19    Tag(String),
20    /// Latest block on the active node
21    Latest,
22}
23
24/// Helper function to resolve a block reference to a hash
25pub fn resolve_block_reference<Engine: EngineTypes>(
26    reference: &BlockReference,
27    env: &Environment<Engine>,
28) -> Result<B256> {
29    match reference {
30        BlockReference::Hash(hash) => Ok(*hash),
31        BlockReference::Tag(tag) => {
32            let (block_info, _) = env
33                .block_registry
34                .get(tag)
35                .ok_or_else(|| eyre::eyre!("Block tag '{tag}' not found in registry"))?;
36            Ok(block_info.hash)
37        }
38        BlockReference::Latest => {
39            let block_info = env
40                .current_block_info()
41                .ok_or_else(|| eyre::eyre!("No current block information available"))?;
42            Ok(block_info.hash)
43        }
44    }
45}
46
47/// Action to send a custom forkchoice update with specific finalized, safe, and head blocks
48#[derive(Debug)]
49pub struct SendForkchoiceUpdate<Engine> {
50    /// The finalized block reference
51    pub finalized: BlockReference,
52    /// The safe block reference
53    pub safe: BlockReference,
54    /// The head block reference
55    pub head: BlockReference,
56    /// Expected payload status (None means accept any non-error)
57    pub expected_status: Option<PayloadStatusEnum>,
58    /// Node index to send to (None means active node)
59    pub node_idx: Option<usize>,
60    /// Tracks engine type
61    _phantom: PhantomData<Engine>,
62}
63
64impl<Engine> SendForkchoiceUpdate<Engine> {
65    /// Create a new custom forkchoice update action
66    pub const fn new(
67        finalized: BlockReference,
68        safe: BlockReference,
69        head: BlockReference,
70    ) -> Self {
71        Self { finalized, safe, head, expected_status: None, node_idx: None, _phantom: PhantomData }
72    }
73
74    /// Set expected status for the FCU response
75    pub fn with_expected_status(mut self, status: PayloadStatusEnum) -> Self {
76        self.expected_status = Some(status);
77        self
78    }
79
80    /// Set the target node index
81    pub const fn with_node_idx(mut self, idx: usize) -> Self {
82        self.node_idx = Some(idx);
83        self
84    }
85}
86
87impl<Engine> Action<Engine> for SendForkchoiceUpdate<Engine>
88where
89    Engine: EngineTypes,
90{
91    fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
92        Box::pin(async move {
93            let finalized_hash = resolve_block_reference(&self.finalized, env)?;
94            let safe_hash = resolve_block_reference(&self.safe, env)?;
95            let head_hash = resolve_block_reference(&self.head, env)?;
96
97            let fork_choice_state = ForkchoiceState {
98                head_block_hash: head_hash,
99                safe_block_hash: safe_hash,
100                finalized_block_hash: finalized_hash,
101            };
102
103            debug!(
104                "Sending FCU - finalized: {finalized_hash}, safe: {safe_hash}, head: {head_hash}"
105            );
106
107            let node_idx = self.node_idx.unwrap_or(env.active_node_idx);
108            if node_idx >= env.node_clients.len() {
109                return Err(eyre::eyre!("Node index {node_idx} out of bounds"));
110            }
111
112            let engine = env.node_clients[node_idx].engine.http_client();
113            let fcu_response =
114                EngineApiClient::<Engine>::fork_choice_updated_v3(&engine, fork_choice_state, None)
115                    .await?;
116
117            debug!(
118                "Node {node_idx}: FCU response - status: {:?}, latest_valid_hash: {:?}",
119                fcu_response.payload_status.status, fcu_response.payload_status.latest_valid_hash
120            );
121
122            // If we have an expected status, validate it
123            if let Some(expected) = &self.expected_status {
124                match (&fcu_response.payload_status.status, expected) {
125                    (PayloadStatusEnum::Valid, PayloadStatusEnum::Valid) => {
126                        debug!("Node {node_idx}: FCU returned VALID as expected");
127                    }
128                    (
129                        PayloadStatusEnum::Invalid { validation_error },
130                        PayloadStatusEnum::Invalid { .. },
131                    ) => {
132                        debug!(
133                            "Node {node_idx}: FCU returned INVALID as expected: {validation_error:?}"
134                        );
135                    }
136                    (PayloadStatusEnum::Syncing, PayloadStatusEnum::Syncing) => {
137                        debug!("Node {node_idx}: FCU returned SYNCING as expected");
138                    }
139                    (PayloadStatusEnum::Accepted, PayloadStatusEnum::Accepted) => {
140                        debug!("Node {node_idx}: FCU returned ACCEPTED as expected");
141                    }
142                    (actual, expected) => {
143                        return Err(eyre::eyre!(
144                            "Node {node_idx}: FCU status mismatch. Expected {expected:?}, got {actual:?}"
145                        ));
146                    }
147                }
148            } else {
149                // Just validate it's not an error
150                if matches!(fcu_response.payload_status.status, PayloadStatusEnum::Invalid { .. }) {
151                    return Err(eyre::eyre!(
152                        "Node {node_idx}: FCU returned unexpected INVALID status: {:?}",
153                        fcu_response.payload_status.status
154                    ));
155                }
156            }
157
158            Ok(())
159        })
160    }
161}
162
163/// Action to finalize a specific block with a given head
164#[derive(Debug)]
165pub struct FinalizeBlock<Engine> {
166    /// Block to finalize
167    pub block_to_finalize: BlockReference,
168    /// Current head block (if None, uses the finalized block)
169    pub head: Option<BlockReference>,
170    /// Node index to send to (None means active node)
171    pub node_idx: Option<usize>,
172    /// Tracks engine type
173    _phantom: PhantomData<Engine>,
174}
175
176impl<Engine> FinalizeBlock<Engine> {
177    /// Create a new finalize block action
178    pub const fn new(block_to_finalize: BlockReference) -> Self {
179        Self { block_to_finalize, head: None, node_idx: None, _phantom: PhantomData }
180    }
181
182    /// Set the head block (if different from finalized)
183    pub fn with_head(mut self, head: BlockReference) -> Self {
184        self.head = Some(head);
185        self
186    }
187
188    /// Set the target node index
189    pub const fn with_node_idx(mut self, idx: usize) -> Self {
190        self.node_idx = Some(idx);
191        self
192    }
193}
194
195impl<Engine> Action<Engine> for FinalizeBlock<Engine>
196where
197    Engine: EngineTypes,
198{
199    fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
200        Box::pin(async move {
201            let finalized_hash = resolve_block_reference(&self.block_to_finalize, env)?;
202            let head_hash = if let Some(ref head_ref) = self.head {
203                resolve_block_reference(head_ref, env)?
204            } else {
205                finalized_hash
206            };
207
208            // Use SendForkchoiceUpdate to do the actual work
209            let mut fcu_action = SendForkchoiceUpdate::new(
210                BlockReference::Hash(finalized_hash),
211                BlockReference::Hash(finalized_hash), // safe = finalized
212                BlockReference::Hash(head_hash),
213            );
214
215            if let Some(idx) = self.node_idx {
216                fcu_action = fcu_action.with_node_idx(idx);
217            }
218
219            fcu_action.execute(env).await?;
220
221            debug!("Block {finalized_hash} successfully finalized with head at {head_hash}");
222
223            Ok(())
224        })
225    }
226}