reth_e2e_test_utils/testsuite/actions/
custom_fcu.rs1use 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#[derive(Debug, Clone)]
16pub enum BlockReference {
17 Hash(B256),
19 Tag(String),
21 Latest,
23}
24
25pub 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#[derive(Debug)]
50pub struct SendForkchoiceUpdate<Engine> {
51 pub finalized: BlockReference,
53 pub safe: BlockReference,
55 pub head: BlockReference,
57 pub expected_status: Option<PayloadStatusEnum>,
59 pub node_idx: Option<usize>,
61 _phantom: PhantomData<Engine>,
63}
64
65impl<Engine> SendForkchoiceUpdate<Engine> {
66 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 pub fn with_expected_status(mut self, status: PayloadStatusEnum) -> Self {
77 self.expected_status = Some(status);
78 self
79 }
80
81 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 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 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#[derive(Debug)]
169pub struct FinalizeBlock<Engine> {
170 pub block_to_finalize: BlockReference,
172 pub head: Option<BlockReference>,
174 pub node_idx: Option<usize>,
176 _phantom: PhantomData<Engine>,
178}
179
180impl<Engine> FinalizeBlock<Engine> {
181 pub const fn new(block_to_finalize: BlockReference) -> Self {
183 Self { block_to_finalize, head: None, node_idx: None, _phantom: PhantomData }
184 }
185
186 pub fn with_head(mut self, head: BlockReference) -> Self {
188 self.head = Some(head);
189 self
190 }
191
192 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 let mut fcu_action = SendForkchoiceUpdate::new(
214 BlockReference::Hash(finalized_hash),
215 BlockReference::Hash(finalized_hash), 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}