1use eyre::{eyre, Result};
4use futures_util::future::BoxFuture;
5use jsonrpsee::core::client::ClientT;
6use reth_e2e_test_utils::testsuite::{actions::Action, BlockInfo, Environment};
7use reth_node_api::EngineTypes;
8use serde_json::Value;
9use std::path::Path;
10use tracing::{debug, info};
11
12#[derive(Debug, Clone)]
14pub struct RpcTestCase {
15 pub name: String,
17 pub request: Value,
19 pub expected_response: Value,
21 pub spec_only: bool,
23}
24
25#[derive(Debug)]
27pub struct RunRpcCompatTests {
28 pub methods: Vec<String>,
30 pub test_data_path: String,
32 pub fail_fast: bool,
34}
35
36impl RunRpcCompatTests {
37 pub fn new(methods: Vec<String>, test_data_path: impl Into<String>) -> Self {
39 Self { methods, test_data_path: test_data_path.into(), fail_fast: false }
40 }
41
42 pub const fn with_fail_fast(mut self, fail_fast: bool) -> Self {
44 self.fail_fast = fail_fast;
45 self
46 }
47
48 fn parse_io_file(content: &str) -> Result<RpcTestCase> {
50 let mut lines = content.lines();
51 let mut spec_only = false;
52 let mut request_line = None;
53 let mut response_line = None;
54
55 for line in lines.by_ref() {
57 let line = line.trim();
58 if line.starts_with("//") {
59 if line.contains("speconly:") {
60 spec_only = true;
61 }
62 } else if let Some(stripped) = line.strip_prefix(">>") {
63 request_line = Some(stripped.trim());
64 break;
65 }
66 }
67
68 for line in lines {
70 let line = line.trim();
71 if let Some(stripped) = line.strip_prefix("<<") {
72 response_line = Some(stripped.trim());
73 break;
74 }
75 }
76
77 let request_str =
78 request_line.ok_or_else(|| eyre!("No request found in test file (>> marker)"))?;
79 let response_str =
80 response_line.ok_or_else(|| eyre!("No response found in test file (<< marker)"))?;
81
82 let request: Value = serde_json::from_str(request_str)
84 .map_err(|e| eyre!("Failed to parse request: {}", e))?;
85
86 let expected_response: Value = serde_json::from_str(response_str)
88 .map_err(|e| eyre!("Failed to parse response: {}", e))?;
89
90 Ok(RpcTestCase { name: String::new(), request, expected_response, spec_only })
91 }
92
93 fn compare_json_values(actual: &Value, expected: &Value, path: &str) -> Result<()> {
96 let mut work_stack = vec![(actual, expected, path.to_string())];
98
99 while let Some((actual, expected, current_path)) = work_stack.pop() {
100 match (actual, expected) {
101 (Value::Number(a), Value::Number(b)) => {
103 let a_f64 = a.as_f64().ok_or_else(|| eyre!("Invalid number"))?;
104 let b_f64 = b.as_f64().ok_or_else(|| eyre!("Invalid number"))?;
105 const EPSILON: f64 = 1e-10;
107 if (a_f64 - b_f64).abs() > EPSILON {
108 return Err(eyre!("Number mismatch at {}: {} != {}", current_path, a, b));
109 }
110 }
111 (Value::Array(a), Value::Array(b)) => {
113 if a.len() != b.len() {
114 return Err(eyre!(
115 "Array length mismatch at {}: {} != {}",
116 current_path,
117 a.len(),
118 b.len()
119 ));
120 }
121 for (i, (av, bv)) in a.iter().zip(b.iter()).enumerate().rev() {
124 work_stack.push((av, bv, format!("{current_path}[{i}]")));
125 }
126 }
127 (Value::Object(a), Value::Object(b)) => {
129 for (key, expected_val) in b {
131 if let Some(actual_val) = a.get(key) {
132 work_stack.push((
133 actual_val,
134 expected_val,
135 format!("{current_path}.{key}"),
136 ));
137 } else {
138 return Err(eyre!("Missing key at {}.{}", current_path, key));
139 }
140 }
141 }
142 (a, b) => {
144 if a != b {
145 return Err(eyre!("Value mismatch at {}: {:?} != {:?}", current_path, a, b));
146 }
147 }
148 }
149 }
150 Ok(())
151 }
152
153 async fn execute_test_case<Engine: EngineTypes>(
155 &self,
156 test_case: &RpcTestCase,
157 env: &Environment<Engine>,
158 ) -> Result<()> {
159 let node_client = &env.node_clients[env.active_node_idx];
160
161 let method = test_case
163 .request
164 .get("method")
165 .and_then(|v| v.as_str())
166 .ok_or_else(|| eyre!("Request missing method field"))?;
167
168 let params = test_case.request.get("params").cloned().unwrap_or(Value::Array(vec![]));
169
170 use jsonrpsee::core::params::ArrayParams;
173
174 let response_result: Result<Value, jsonrpsee::core::client::Error> = match params {
175 Value::Array(ref arr) => {
176 let mut array_params = ArrayParams::new();
178 for param in arr {
179 array_params
180 .insert(param.clone())
181 .map_err(|e| eyre!("Failed to insert param: {}", e))?;
182 }
183 node_client.rpc.request(method, array_params).await
184 }
185 _ => {
186 let mut array_params = ArrayParams::new();
188 array_params.insert(params).map_err(|e| eyre!("Failed to insert param: {}", e))?;
189 node_client.rpc.request(method, array_params).await
190 }
191 };
192
193 let actual_response = match response_result {
195 Ok(response) => {
196 serde_json::json!({
197 "jsonrpc": "2.0",
198 "id": test_case.request.get("id").cloned().unwrap_or(Value::Null),
199 "result": response
200 })
201 }
202 Err(err) => {
203 serde_json::json!({
205 "jsonrpc": "2.0",
206 "id": test_case.request.get("id").cloned().unwrap_or(Value::Null),
207 "error": {
208 "code": -32000, "message": err.to_string()
210 }
211 })
212 }
213 };
214
215 let expected_result = test_case.expected_response.get("result");
217 let expected_error = test_case.expected_response.get("error");
218 let actual_result = actual_response.get("result");
219 let actual_error = actual_response.get("error");
220
221 match (expected_result, expected_error) {
222 (Some(expected), None) => {
223 if let Some(actual) = actual_result {
225 Self::compare_json_values(actual, expected, "result")?;
226 } else if let Some(error) = actual_error {
227 return Err(eyre!("Expected success response but got error: {}", error));
228 } else {
229 return Err(eyre!("Expected success response but got neither result nor error"));
230 }
231 }
232 (None, Some(_)) => {
233 if actual_error.is_none() {
235 return Err(eyre!("Expected error response but got success"));
236 }
237 debug!("Both responses are errors (expected behavior)");
238 }
239 _ => {
240 return Err(eyre!("Invalid expected response format"));
241 }
242 }
243
244 Ok(())
245 }
246}
247
248impl<Engine> Action<Engine> for RunRpcCompatTests
249where
250 Engine: EngineTypes,
251{
252 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
253 Box::pin(async move {
254 let mut total_tests = 0;
255 let mut passed_tests = 0;
256
257 for method in &self.methods {
258 info!("Running RPC compatibility tests for {}", method);
259
260 let method_dir = Path::new(&self.test_data_path).join(method);
261 if !method_dir.exists() {
262 return Err(eyre!("Test directory does not exist: {}", method_dir.display()));
263 }
264
265 let entries = std::fs::read_dir(&method_dir)
267 .map_err(|e| eyre!("Failed to read directory: {}", e))?;
268
269 for entry in entries {
270 let entry = entry?;
271 let path = entry.path();
272
273 if path.extension().and_then(|s| s.to_str()) == Some("io") {
274 let test_name = path
275 .file_stem()
276 .and_then(|s| s.to_str())
277 .unwrap_or("unknown")
278 .to_string();
279
280 let content = std::fs::read_to_string(&path)
281 .map_err(|e| eyre!("Failed to read test file: {}", e))?;
282
283 match Self::parse_io_file(&content) {
284 Ok(mut test_case) => {
285 test_case.name = test_name.clone();
286 total_tests += 1;
287
288 match self.execute_test_case(&test_case, env).await {
289 Ok(_) => {
290 info!("✓ {}/{}: PASS", method, test_name);
291 passed_tests += 1;
292 }
293 Err(e) => {
294 info!("✗ {}/{}: FAIL - {}", method, test_name, e);
295
296 if self.fail_fast {
297 return Err(eyre!("Test failed (fail-fast enabled)"));
298 }
299 }
300 }
301 }
302 Err(e) => {
303 info!("✗ {}/{}: PARSE ERROR - {}", method, test_name, e);
304 if self.fail_fast {
305 return Err(e);
306 }
307 }
308 }
309 }
310 }
311 }
312
313 info!("RPC compatibility test results: {}/{} passed", passed_tests, total_tests);
314
315 if passed_tests < total_tests {
316 return Err(eyre!("Some tests failed: {}/{} passed", passed_tests, total_tests));
317 }
318
319 Ok(())
320 })
321 }
322}
323
324#[derive(Debug)]
326pub struct InitializeFromExecutionApis {
327 pub chain_rlp_path: Option<String>,
329 pub fcu_json_path: Option<String>,
331}
332
333impl Default for InitializeFromExecutionApis {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339impl InitializeFromExecutionApis {
340 pub const fn new() -> Self {
342 Self { chain_rlp_path: None, fcu_json_path: None }
343 }
344
345 pub fn with_chain_rlp(mut self, path: impl Into<String>) -> Self {
347 self.chain_rlp_path = Some(path.into());
348 self
349 }
350
351 pub fn with_fcu_json(mut self, path: impl Into<String>) -> Self {
353 self.fcu_json_path = Some(path.into());
354 self
355 }
356}
357
358impl<Engine> Action<Engine> for InitializeFromExecutionApis
359where
360 Engine: EngineTypes,
361{
362 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
363 Box::pin(async move {
364 let fcu_path = self
366 .fcu_json_path
367 .as_ref()
368 .map(Path::new)
369 .ok_or_else(|| eyre!("FCU JSON path is required"))?;
370
371 let fcu_state = reth_e2e_test_utils::setup_import::load_forkchoice_state(fcu_path)?;
372
373 info!(
374 "Applying forkchoice state - head: {}, safe: {}, finalized: {}",
375 fcu_state.head_block_hash,
376 fcu_state.safe_block_hash,
377 fcu_state.finalized_block_hash
378 );
379
380 for (idx, client) in env.node_clients.iter().enumerate() {
382 debug!("Applying forkchoice update to node {}", idx);
383
384 let mut retries = 0;
386 const MAX_RETRIES: u32 = 10;
387 const RETRY_DELAY_MS: u64 = 500;
388
389 loop {
390 let response =
391 reth_rpc_api::clients::EngineApiClient::<Engine>::fork_choice_updated_v3(
392 &client.engine.http_client(),
393 fcu_state,
394 None,
395 )
396 .await
397 .map_err(|e| eyre!("Failed to update forkchoice on node {}: {}", idx, e))?;
398
399 match response.payload_status.status {
400 alloy_rpc_types_engine::PayloadStatusEnum::Valid => {
401 debug!("Forkchoice update successful on node {}", idx);
402 break;
403 }
404 alloy_rpc_types_engine::PayloadStatusEnum::Syncing => {
405 if retries >= MAX_RETRIES {
406 return Err(eyre!(
407 "Node {} still syncing after {} retries",
408 idx,
409 MAX_RETRIES
410 ));
411 }
412 debug!("Node {} is syncing, retrying in {}ms...", idx, RETRY_DELAY_MS);
413 tokio::time::sleep(std::time::Duration::from_millis(RETRY_DELAY_MS))
414 .await;
415 retries += 1;
416 }
417 _ => {
418 return Err(eyre!(
419 "Invalid forkchoice state on node {}: {:?}",
420 idx,
421 response.payload_status
422 ));
423 }
424 }
425 }
426 }
427
428 env.active_node_state_mut()?.current_block_info = Some(BlockInfo {
430 hash: fcu_state.head_block_hash,
431 number: 0, timestamp: 0,
433 });
434
435 info!("Successfully initialized chain from execution-apis test data");
436 Ok(())
437 })
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use serde_json::json;
445
446 #[test]
447 fn test_compare_json_values_deeply_nested() {
448 let mut nested = json!({"value": 0});
451 let mut expected = json!({"value": 0});
452
453 for i in 1..1000 {
455 nested = json!({"level": i, "nested": nested});
456 expected = json!({"level": i, "nested": expected});
457 }
458
459 RunRpcCompatTests::compare_json_values(&nested, &expected, "root").unwrap();
461 }
462
463 #[test]
464 fn test_compare_json_values_arrays() {
465 let actual = json!([1, 2, 3, 4, 5]);
467 let expected = json!([1, 2, 3, 4, 5]);
468
469 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
470
471 let actual = json!([1, 2, 3]);
473 let expected = json!([1, 2, 3, 4, 5]);
474
475 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
476 assert!(result.is_err());
477 assert!(result.unwrap_err().to_string().contains("Array length mismatch"));
478 }
479
480 #[test]
481 fn test_compare_json_values_objects() {
482 let actual = json!({"a": 1, "b": 2, "c": 3});
484 let expected = json!({"a": 1, "b": 2, "c": 3});
485
486 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
487
488 let actual = json!({"a": 1, "b": 2});
490 let expected = json!({"a": 1, "b": 2, "c": 3});
491
492 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
493 assert!(result.is_err());
494 assert!(result.unwrap_err().to_string().contains("Missing key"));
495 }
496
497 #[test]
498 fn test_compare_json_values_numbers() {
499 let actual = json!({"value": 1.00000000001});
501 let expected = json!({"value": 1.0});
502
503 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
505
506 let actual = json!({"value": 1.1});
508 let expected = json!({"value": 1.0});
509
510 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
511 assert!(result.is_err());
512 assert!(result.unwrap_err().to_string().contains("Number mismatch"));
513 }
514}