1use eyre::{eyre, Result};
4use futures_util::future::BoxFuture;
5use jsonrpsee::core::client::ClientT;
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 use jsonrpsee::core::params::ArrayParams;
176
177 let response_result: Result<Value, jsonrpsee::core::client::Error> = match params {
178 Value::Array(ref arr) => {
179 let mut array_params = ArrayParams::new();
181 for param in arr {
182 array_params
183 .insert(param.clone())
184 .map_err(|e| eyre!("Failed to insert param: {}", e))?;
185 }
186 node_client.rpc.request(method, array_params).await
187 }
188 _ => {
189 let mut array_params = ArrayParams::new();
191 array_params.insert(params).map_err(|e| eyre!("Failed to insert param: {}", e))?;
192 node_client.rpc.request(method, array_params).await
193 }
194 };
195
196 let actual_response = match response_result {
198 Ok(response) => {
199 serde_json::json!({
200 "jsonrpc": "2.0",
201 "id": test_case.request.get("id").cloned().unwrap_or(Value::Null),
202 "result": response
203 })
204 }
205 Err(err) => {
206 serde_json::json!({
208 "jsonrpc": "2.0",
209 "id": test_case.request.get("id").cloned().unwrap_or(Value::Null),
210 "error": {
211 "code": -32000, "message": err.to_string()
213 }
214 })
215 }
216 };
217
218 let expected_result = test_case.expected_response.get("result");
220 let expected_error = test_case.expected_response.get("error");
221 let actual_result = actual_response.get("result");
222 let actual_error = actual_response.get("error");
223
224 match (expected_result, expected_error) {
225 (Some(expected), None) => {
226 if let Some(actual) = actual_result {
228 Self::compare_json_values(actual, expected, "result")?;
229 } else if let Some(error) = actual_error {
230 return Err(eyre!("Expected success response but got error: {}", error));
231 } else {
232 return Err(eyre!("Expected success response but got neither result nor error"));
233 }
234 }
235 (None, Some(_)) => {
236 if actual_error.is_none() {
238 return Err(eyre!("Expected error response but got success"));
239 }
240 debug!("Both responses are errors (expected behavior)");
241 }
242 _ => {
243 return Err(eyre!("Invalid expected response format"));
244 }
245 }
246
247 Ok(())
248 }
249}
250
251impl<Engine> Action<Engine> for RunRpcCompatTests
252where
253 Engine: EngineTypes,
254{
255 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
256 Box::pin(async move {
257 let mut total_tests = 0;
258 let mut passed_tests = 0;
259
260 for method in &self.methods {
261 info!("Running RPC compatibility tests for {}", method);
262
263 let method_dir = Path::new(&self.test_data_path).join(method);
264 if !method_dir.exists() {
265 return Err(eyre!("Test directory does not exist: {}", method_dir.display()));
266 }
267
268 let entries = std::fs::read_dir(&method_dir)
270 .map_err(|e| eyre!("Failed to read directory: {}", e))?;
271
272 for entry in entries {
273 let entry = entry?;
274 let path = entry.path();
275
276 if path.extension().and_then(|s| s.to_str()) == Some("io") {
277 let test_name = path
278 .file_stem()
279 .and_then(|s| s.to_str())
280 .unwrap_or("unknown")
281 .to_string();
282
283 let content = std::fs::read_to_string(&path)
284 .map_err(|e| eyre!("Failed to read test file: {}", e))?;
285
286 match Self::parse_io_file(&content) {
287 Ok(mut test_case) => {
288 test_case.name = test_name.clone();
289 total_tests += 1;
290
291 match self.execute_test_case(&test_case, env).await {
292 Ok(_) => {
293 info!("✓ {}/{}: PASS", method, test_name);
294 passed_tests += 1;
295 }
296 Err(e) => {
297 info!("✗ {}/{}: FAIL - {}", method, test_name, e);
298
299 if self.fail_fast {
300 return Err(eyre!("Test failed (fail-fast enabled)"));
301 }
302 }
303 }
304 }
305 Err(e) => {
306 info!("✗ {}/{}: PARSE ERROR - {}", method, test_name, e);
307 if self.fail_fast {
308 return Err(e);
309 }
310 }
311 }
312 }
313 }
314 }
315
316 info!("RPC compatibility test results: {}/{} passed", passed_tests, total_tests);
317
318 if passed_tests < total_tests {
319 return Err(eyre!("Some tests failed: {}/{} passed", passed_tests, total_tests));
320 }
321
322 Ok(())
323 })
324 }
325}
326
327#[derive(Debug)]
329pub struct InitializeFromExecutionApis {
330 pub chain_rlp_path: Option<String>,
332 pub fcu_json_path: Option<String>,
334}
335
336impl Default for InitializeFromExecutionApis {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342impl InitializeFromExecutionApis {
343 pub const fn new() -> Self {
345 Self { chain_rlp_path: None, fcu_json_path: None }
346 }
347
348 pub fn with_chain_rlp(mut self, path: impl Into<String>) -> Self {
350 self.chain_rlp_path = Some(path.into());
351 self
352 }
353
354 pub fn with_fcu_json(mut self, path: impl Into<String>) -> Self {
356 self.fcu_json_path = Some(path.into());
357 self
358 }
359}
360
361impl<Engine> Action<Engine> for InitializeFromExecutionApis
362where
363 Engine: EngineTypes,
364{
365 fn execute<'a>(&'a mut self, env: &'a mut Environment<Engine>) -> BoxFuture<'a, Result<()>> {
366 Box::pin(async move {
367 let fcu_path = self
369 .fcu_json_path
370 .as_ref()
371 .map(Path::new)
372 .ok_or_else(|| eyre!("FCU JSON path is required"))?;
373
374 let fcu_state = reth_e2e_test_utils::setup_import::load_forkchoice_state(fcu_path)?;
375
376 info!(
377 "Applying forkchoice state - head: {}, safe: {}, finalized: {}",
378 fcu_state.head_block_hash,
379 fcu_state.safe_block_hash,
380 fcu_state.finalized_block_hash
381 );
382
383 for (idx, client) in env.node_clients.iter().enumerate() {
386 debug!("Applying forkchoice update to node {}", idx);
387
388 let response =
389 reth_rpc_api::clients::EngineApiClient::<Engine>::fork_choice_updated_v3(
390 &client.engine.http_client(),
391 fcu_state,
392 None,
393 )
394 .await
395 .map_err(|e| eyre!("Failed to update forkchoice on node {}: {}", idx, e))?;
396 expect_fcu_valid(&response, &format!("Forkchoice update on node {idx}"))?;
397 }
398
399 env.active_node_state_mut()?.current_block_info = Some(BlockInfo {
401 hash: fcu_state.head_block_hash,
402 number: 0, timestamp: 0,
404 });
405
406 info!("Successfully initialized chain from execution-apis test data");
407 Ok(())
408 })
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use serde_json::json;
416
417 #[test]
418 fn test_compare_json_values_deeply_nested() {
419 let mut nested = json!({"value": 0});
422 let mut expected = json!({"value": 0});
423
424 for i in 1..1000 {
426 nested = json!({"level": i, "nested": nested});
427 expected = json!({"level": i, "nested": expected});
428 }
429
430 RunRpcCompatTests::compare_json_values(&nested, &expected, "root").unwrap();
432 }
433
434 #[test]
435 fn test_compare_json_values_arrays() {
436 let actual = json!([1, 2, 3, 4, 5]);
438 let expected = json!([1, 2, 3, 4, 5]);
439
440 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
441
442 let actual = json!([1, 2, 3]);
444 let expected = json!([1, 2, 3, 4, 5]);
445
446 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
447 assert!(result.is_err());
448 assert!(result.unwrap_err().to_string().contains("Array length mismatch"));
449 }
450
451 #[test]
452 fn test_compare_json_values_objects() {
453 let actual = json!({"a": 1, "b": 2, "c": 3});
455 let expected = json!({"a": 1, "b": 2, "c": 3});
456
457 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
458
459 let actual = json!({"a": 1, "b": 2});
461 let expected = json!({"a": 1, "b": 2, "c": 3});
462
463 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
464 assert!(result.is_err());
465 assert!(result.unwrap_err().to_string().contains("Missing key"));
466 }
467
468 #[test]
469 fn test_compare_json_values_numbers() {
470 let actual = json!({"value": 1.00000000001});
472 let expected = json!({"value": 1.0});
473
474 RunRpcCompatTests::compare_json_values(&actual, &expected, "root").unwrap();
476
477 let actual = json!({"value": 1.1});
479 let expected = json!({"value": 1.0});
480
481 let result = RunRpcCompatTests::compare_json_values(&actual, &expected, "root");
482 assert!(result.is_err());
483 assert!(result.unwrap_err().to_string().contains("Number mismatch"));
484 }
485}