reth_e2e_test_utils/testsuite/actions/
custom_fcu.rs1use 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#[derive(Debug, Clone)]
15pub enum BlockReference {
16 Hash(B256),
18 Tag(String),
20 Latest,
22}
23
24pub 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#[derive(Debug)]
49pub struct SendForkchoiceUpdate<Engine> {
50 pub finalized: BlockReference,
52 pub safe: BlockReference,
54 pub head: BlockReference,
56 pub expected_status: Option<PayloadStatusEnum>,
58 pub node_idx: Option<usize>,
60 _phantom: PhantomData<Engine>,
62}
63
64impl<Engine> SendForkchoiceUpdate<Engine> {
65 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 pub fn with_expected_status(mut self, status: PayloadStatusEnum) -> Self {
76 self.expected_status = Some(status);
77 self
78 }
79
80 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 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 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#[derive(Debug)]
165pub struct FinalizeBlock<Engine> {
166 pub block_to_finalize: BlockReference,
168 pub head: Option<BlockReference>,
170 pub node_idx: Option<usize>,
172 _phantom: PhantomData<Engine>,
174}
175
176impl<Engine> FinalizeBlock<Engine> {
177 pub const fn new(block_to_finalize: BlockReference) -> Self {
179 Self { block_to_finalize, head: None, node_idx: None, _phantom: PhantomData }
180 }
181
182 pub fn with_head(mut self, head: BlockReference) -> Self {
184 self.head = Some(head);
185 self
186 }
187
188 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 let mut fcu_action = SendForkchoiceUpdate::new(
210 BlockReference::Hash(finalized_hash),
211 BlockReference::Hash(finalized_hash), 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}