Skip to main content

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