1use alloy_rpc_types_engine::ForkchoiceUpdated;
4use eyre::{eyre, Result};
5use futures_util::future::BoxFuture;
6use reth_e2e_test_utils::testsuite::{
7 actions::{expect_fcu_valid, Action},
8 BlockInfo, Environment,
9};
10use reth_node_api::EngineTypes;
11use serde_json::Value;
12use std::path::Path;
13use tracing::{debug, info};
14
15#[derive(Debug, Clone)]
17pub struct RpcTestCase {
18 pub name: String,
20 pub request: Value,
22 pub expected_response: Value,
24 pub spec_only: bool,
26}
27
28#[derive(Debug)]
30pub struct RunRpcCompatTests {
31 pub methods: Vec<String>,
33 pub test_data_path: String,
35 pub fail_fast: bool,
37}
38
39impl RunRpcCompatTests {
40 pub fn new(methods: Vec<String>, test_data_path: impl Into<String>) -> Self {
42 Self { methods, test_data_path: test_data_path.into(), fail_fast: false }
43 }
44
45 pub const fn with_fail_fast(mut self, fail_fast: bool) -> Self {
47 self.fail_fast = fail_fast;
48 self
49 }
50
51 fn parse_io_file(content: &str) -> Result<RpcTestCase> {
53 let mut lines = content.lines();
54 let mut spec_only = false;
55 let mut request_line = None;
56 let mut response_line = None;
57
58 for line in lines.by_ref() {
60 let line = line.trim();
61 if line.starts_with("//") {
62 if line.contains("speconly:") {
63 spec_only = true;
64 }
65 } else if let Some(stripped) = line.strip_prefix(">>") {
66 request_line = Some(stripped.trim());
67 break;
68 }
69 }
70
71 for line in lines {
73 let line = line.trim();
74 if let Some(stripped) = line.strip_prefix("<<") {
75 response_line = Some(stripped.trim());
76 break;
77 }
78 }
79
80 let request_str =
81 request_line.ok_or_else(|| eyre!("No request found in test file (>> marker)"))?;
82 let response_str =
83 response_line.ok_or_else(|| eyre!("No response found in test file (<< marker)"))?;
84
85 let request: Value = serde_json::from_str(request_str)
87 .map_err(|e| eyre!("Failed to parse request: {}", e))?;
88
89 let expected_response: Value = serde_json::from_str(response_str)
91 .map_err(|e| eyre!("Failed to parse response: {}", e))?;
92
93 Ok(RpcTestCase { name: String::new(), request, expected_response, spec_only })
94 }
95
96 fn compare_json_values(actual: &Value, expected: &Value, path: &str) -> Result<()> {
99 let mut work_stack = vec![(actual, expected, path.to_string())];
101
102 while let Some((actual, expected, current_path)) = work_stack.pop() {
103 match (actual, expected) {
104 (Value::Number(a), Value::Number(b)) => {
106 let a_f64 = a.as_f64().ok_or_else(|| eyre!("Invalid number"))?;
107 let b_f64 = b.as_f64().ok_or_else(|| eyre!("Invalid number"))?;
108 const EPSILON: f64 = 1e-10;
110 if (a_f64 - b_f64).abs() > EPSILON {
111 return Err(eyre!("Number mismatch at {}: {} != {}", current_path, a, b));
112 }
113 }
114 (Value::Array(a), Value::Array(b)) => {
116 if a.len() != b.len() {
117 return Err(eyre!(
118 "Array length mismatch at {}: {} != {}",
119 current_path,
120 a.len(),
121 b.len()
122 ));
123 }
124 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate().rev() {
127 work_stack.push((av, bv, format!("{current_path}[{i}]")));
128 }
129 }
130 (Value::Object(a), Value::Object(b)) => {
132 for (key, expected_val) in b {
134 if let Some(actual_val) = a.get(key) {
135 work_stack.push((
136 actual_val,
137 expected_val,
138 format!("{current_path}.{key}"),
139 ));
140 } else {
141 return Err(eyre!("Missing key at {}.{}", current_path, key));
142 }
143 }
144 }
145 (a, b) => {
147 if a != b {
148 return Err(eyre!("Value mismatch at {}: {:?} != {:?}", current_path, a, b));
149 }
150 }
151 }
152 }
153 Ok(())
154 }
155
156 async fn execute_test_case<Engine: EngineTypes>(
158 &self,
159 test_case: &RpcTestCase,
160 env: &Environment<Engine>,
161 ) -> Result<()> {
162 let node_client = &env.node_clients[env.active_node_idx];
163
164 let method = test_case
166 .request
167 .get("method")
168 .and_then(|v| v.as_str())
169 .ok_or_else(|| eyre!("Request missing method field"))?;
170
171 let params = test_case.request.get("params").cloned().unwrap_or(Value::Array(vec![]));
172
173 let params = match params {
175 Value::Array(_) => params,
176 _ => Value::Array(vec![params]),
177 };
178 let response_result = node_client.rpc.request::<_, Value>(method.to_owned(), params).await;
179
180 let actual_response = match response_result {
182 Ok(response) => {
183 serde_json::json!({
184 "jsonrpc": "2.0",
185 "id": test_case.request.get("id").cloned().unwrap_or(Value::Null),
186 "result": response
187 })
188 }
189 Err(err) => {
190 serde_json::json!({
192 "jsonrpc": "2.0",
193 "id": test_case.request.get("id").cloned().unwrap_or(Value::Null),
194 "error": {
195 "code": -32000, "message": err.to_string()
197 }
198 })
199 }
200 };
201
202 let expected_result = test_case.expected_response.get("result");
204 let expected_error = test_case.expected_response.get("error");
205 let actual_result = actual_response.get("result");
206 let actual_error = actual_response.get("error");
207
208 match (expected_result, expected_error) {
209 (Some(expected), None) => {
210 if let Some(actual) = actual_result {
212 Self::compare_json_values(actual, expected, "result")?;
213 } else if let Some(error) = actual_error {
214 return Err(eyre!("Expected success response but got error: {}", error));
215 } else {
216 return Err(eyre!("Expected success response but got neither result nor error"));
217 }
218 }
219 (None, Some(_)) => {
220 if actual_error.is_none() {
222 return Err(eyre!("Expected error response but got success"));
223 }
224 debug!("Both responses are errors (expected behavior)");
225 }
226 _ => {
227 return Err(eyre!("Invalid expected response format"));
228 }
229 }
230
231 Ok(())
232 }
233}
234
235impl<Engine> Action<Engine> for RunRpcCompatTests
236where
237 Engine: EngineTypes,
238{
239 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
240 Box::pin(async move {
241 let mut total_tests = 0;
242 let mut passed_tests = 0;
243
244 for method in &self.methods {
245 info!("Running RPC compatibility tests for {}", method);
246
247 let method_dir = Path::new(&self.test_data_path).join(method);
248 if !method_dir.exists() {
249 return Err(eyre!("Test directory does not exist: {}", method_dir.display()));
250 }
251
252 let entries = std::fs::read_dir(&method_dir)
254 .map_err(|e| eyre!("Failed to read directory: {}", e))?;
255
256 for entry in entries {
257 let entry = entry?;
258 let path = entry.path();
259
260 if path.extension().and_then(|s| s.to_str()) == Some("io") {
261 let test_name = path
262 .file_stem()
263 .and_then(|s| s.to_str())
264 .unwrap_or("unknown")
265 .to_string();
266
267 let content = std::fs::read_to_string(&path)
268 .map_err(|e| eyre!("Failed to read test file: {}", e))?;
269
270 match Self::parse_io_file(&content) {
271 Ok(mut test_case) => {
272 test_case.name = test_name.clone();
273 total_tests += 1;
274
275 match self.execute_test_case(&test_case, env).await {
276 Ok(_) => {
277 info!("✓ {}/{}: PASS", method, test_name);
278 passed_tests += 1;
279 }
280 Err(e) => {
281 info!("✗ {}/{}: FAIL - {}", method, test_name, e);
282
283 if self.fail_fast {
284 return Err(eyre!("Test failed (fail-fast enabled)"));
285 }
286 }
287 }
288 }
289 Err(e) => {
290 info!("✗ {}/{}: PARSE ERROR - {}", method, test_name, e);
291 if self.fail_fast {
292 return Err(e);
293 }
294 }
295 }
296 }
297 }
298 }
299
300 info!("RPC compatibility test results: {}/{} passed", passed_tests, total_tests);
301
302 if passed_tests < total_tests {
303 return Err(eyre!("Some tests failed: {}/{} passed", passed_tests, total_tests));
304 }
305
306 Ok(())
307 })
308 }
309}
310
311#[derive(Debug)]
313pub struct InitializeFromExecutionApis {
314 pub chain_rlp_path: Option<String>,
316 pub fcu_json_path: Option<String>,
318}
319
320impl Default for InitializeFromExecutionApis {
321 fn default() -> Self {
322 Self::new()
323 }
324}
325
326impl InitializeFromExecutionApis {
327 pub const fn new() -> Self {
329 Self { chain_rlp_path: None, fcu_json_path: None }
330 }
331
332 pub fn with_chain_rlp(mut self, path: impl Into<String>) -> Self {
334 self.chain_rlp_path = Some(path.into());
335 self
336 }
337
338 pub fn with_fcu_json(mut self, path: impl Into<String>) -> Self {
340 self.fcu_json_path = Some(path.into());
341 self
342 }
343}
344
345impl<Engine> Action<Engine> for InitializeFromExecutionApis
346where
347 Engine: EngineTypes,
348{
349 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
350 Box::pin(async move {
351 let fcu_path = self
353 .fcu_json_path
354 .as_ref()
355 .map(Path::new)
356 .ok_or_else(|| eyre!("FCU JSON path is required"))?;
357
358 let fcu_state = reth_e2e_test_utils::setup_import::load_forkchoice_state(fcu_path)?;
359
360 info!(
361 "Applying forkchoice state - head: {}, safe: {}, finalized: {}",
362 fcu_state.head_block_hash,
363 fcu_state.safe_block_hash,
364 fcu_state.finalized_block_hash
365 );
366
367 for (idx, client) in env.node_clients.iter().enumerate() {
370 debug!("Applying forkchoice update to node {}", idx);
371
372 let response: ForkchoiceUpdated = client
373 .engine
374 .http_client()
375 .request("engine_forkchoiceUpdatedV3", (fcu_state, None::<()>))
376 .await
377 .map_err(|e| eyre!("Failed to update forkchoice on node {}: {}", idx, e))?;
378 expect_fcu_valid(&response, &format!("Forkchoice update on node {idx}"))?;
379 }
380
381 env.active_node_state_mut()?.current_block_info = Some(BlockInfo {
383 hash: fcu_state.head_block_hash,
384 number: 0, timestamp: 0,
386 });
387
388 info!("Successfully initialized chain from execution-apis test data");
389 Ok(())
390 })
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use serde_json::json;
398
399 #[test]
400 fn test_compare_json_values_deeply_nested() {
401 let mut nested = json!({"value": 0});
404 let mut expected = json!({"value": 0});
405
406 for i in 1..1000 {
408 nested = json!({"level": i, "nested": nested});
409 expected = json!({"level": i, "nested": expected});
410 }
411
412 RunRpcCompatTests::compare_json_values(&nested, &expected, "root").unwrap();
414 }
415
416 #[test]
417 fn test_compare_json_values_arrays() {
418 let actual = json!([1, 2, 3, 4, 5]);
420 let expected = json!([1, 2, 3, 4, 5]);
421
422 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
423
424 let actual = json!([1, 2, 3]);
426 let expected = json!([1, 2, 3, 4, 5]);
427
428 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
429 assert!(result.is_err());
430 assert!(result.unwrap_err().to_string().contains("Array length mismatch"));
431 }
432
433 #[test]
434 fn test_compare_json_values_objects() {
435 let actual = json!({"a": 1, "b": 2, "c": 3});
437 let expected = json!({"a": 1, "b": 2, "c": 3});
438
439 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
440
441 let actual = json!({"a": 1, "b": 2});
443 let expected = json!({"a": 1, "b": 2, "c": 3});
444
445 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
446 assert!(result.is_err());
447 assert!(result.unwrap_err().to_string().contains("Missing key"));
448 }
449
450 #[test]
451 fn test_compare_json_values_numbers() {
452 let actual = json!({"value": 1.00000000001});
454 let expected = json!({"value": 1.0});
455
456 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
458
459 let actual = json!({"value": 1.1});
461 let expected = json!({"value": 1.0});
462
463 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
464 assert!(result.is_err());
465 assert!(result.unwrap_err().to_string().contains("Number mismatch"));
466 }
467}