From b45292d7b9cc06b79aff482d2a7c6cb044d87e88 Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Thu, 19 Feb 2026 18:47:40 +0530 Subject: [PATCH 1/9] feat: added cli validator tests --- crates/cli/src/commands/test/validator.rs | 216 +++++++++++++++++++++- 1 file changed, 207 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index 26cd64d1..7b5e34ab 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -1,9 +1,26 @@ //! Validator client connectivity tests. -use super::{TestCategoryResult, TestConfigArgs}; -use crate::error::Result; +use std::{collections::HashMap, io::Write, sync::mpsc, time::Duration}; + use clap::Args; -use std::{io::Write, time::Duration}; +use rand::Rng; +use tokio::{ + net::TcpStream, + time::{Instant, timeout}, +}; + +use super::{ + AllCategoriesResult, TestCaseName, TestCategory, TestCategoryResult, TestConfigArgs, + TestResult, TestVerdict, calculate_score, evaluate_highest_rtt, evaluate_rtt, filter_tests, + publish_result_to_obol_api, sort_tests, write_result_to_file, write_result_to_writer, +}; +use crate::{duration::Duration as CliDuration, error::Result}; + +// Thresholds (from Go implementation) +const THRESHOLD_MEASURE_AVG: Duration = Duration::from_millis(50); +const THRESHOLD_MEASURE_POOR: Duration = Duration::from_millis(240); +const THRESHOLD_LOAD_AVG: Duration = Duration::from_millis(50); +const THRESHOLD_LOAD_POOR: Duration = Duration::from_millis(240); /// Arguments for the validator test command. #[derive(Args, Clone, Debug)] @@ -30,10 +47,191 @@ pub struct TestValidatorArgs { } /// Runs the validator client tests. -pub async fn run(_args: TestValidatorArgs, _writer: &mut dyn Write) -> Result { - // TODO: Implement validator tests - // - Ping - // - PingMeasure - // - PingLoad - unimplemented!("validator test not yet implemented") +pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result { + tracing::info!("Starting validator client test"); + + let start_time = Instant::now(); + + // Get and filter test cases + let all_test_cases = HashMap::from([ + (TestCaseName::new("Ping", 1), ()), + (TestCaseName::new("PingMeasure", 2), ()), + (TestCaseName::new("PingLoad", 3), ()), + ]); + let mut queued_tests = filter_tests(&all_test_cases, args.test_config.test_cases.as_deref()); + + if queued_tests.is_empty() { + return Err(crate::error::CliError::Other( + "test case not supported".into(), + )); + } + + sort_tests(&mut queued_tests); + + // Run tests with timeout + let test_results = tokio::time::timeout(args.test_config.timeout, async { + let mut results = Vec::new(); + for test in queued_tests.iter() { + let result = match test.name.as_str() { + "Ping" => ping_test(&args).await, + "PingMeasure" => ping_measure_test(&args).await, + "PingLoad" => ping_load_test(&args).await, + _ => TestResult::new(&test.name).fail(std::io::Error::other("unknown test")), + }; + results.push(result); + } + results + }) + .await + .unwrap_or_else(|_| { + vec![TestResult::new("Timeout").fail(std::io::Error::other("timeout interrupted"))] + }); + let score = calculate_score(&test_results); + + let mut res = TestCategoryResult::new(TestCategory::Validator); + res.targets.insert(args.api_address.clone(), test_results); + res.execution_time = Some(CliDuration::new(start_time.elapsed())); + res.score = Some(score); + + if !args.test_config.quiet { + write_result_to_writer(&res, writer)?; + } + + if !args.test_config.output_json.is_empty() { + write_result_to_file(&res, args.test_config.output_json.as_ref()).await?; + } + + if args.test_config.publish { + let all = AllCategoriesResult { + validator: Some(res.clone()), + ..Default::default() + }; + publish_result_to_obol_api( + all, + &args.test_config.publish_addr, + &args.test_config.publish_private_key_file, + ) + .await?; + } + + Ok(res) +} + +async fn ping_test(args: &TestValidatorArgs) -> TestResult { + let mut result = TestResult::new("Ping"); + + match timeout( + Duration::from_secs(1), + TcpStream::connect(&args.api_address), + ) + .await + { + Ok(Ok(_conn)) => { + result.verdict = TestVerdict::Ok; + } + Ok(Err(e)) => { + return result.fail(e); + } + Err(_) => { + return result.fail(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "connection timeout", + )); + } + } + + result +} + +async fn ping_measure_test(args: &TestValidatorArgs) -> TestResult { + let mut result = TestResult::new("PingMeasure"); + let before = Instant::now(); + + match timeout( + Duration::from_secs(1), + TcpStream::connect(&args.api_address), + ) + .await + { + Ok(Ok(_conn)) => { + let rtt = before.elapsed(); + result = evaluate_rtt(rtt, result, THRESHOLD_MEASURE_AVG, THRESHOLD_MEASURE_POOR); + } + Ok(Err(e)) => { + return result.fail(e); + } + Err(_) => { + return result.fail(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "connection timeout", + )); + } + } + + result +} + +async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { + tracing::info!( + duration = ?args.load_test_duration, + target = %args.api_address, + "Running ping load tests..." + ); + + let mut result = TestResult::new("PingLoad"); + + let (tx, rx) = mpsc::channel::(); + let address = args.api_address.clone(); + let duration = args.load_test_duration; + + let handle = tokio::spawn(async move { + let start = Instant::now(); + let mut interval = tokio::time::interval(Duration::from_secs(1)); + + while start.elapsed() < duration { + interval.tick().await; + + let tx = tx.clone(); + let addr = address.clone(); + let remaining = duration.saturating_sub(start.elapsed()); + + tokio::spawn(async move { + ping_continuously(addr, tx, remaining).await; + }); + } + }); + + let _ = handle.await; + + let mut rtts = Vec::new(); + while let Ok(rtt) = rx.try_recv() { + rtts.push(rtt); + } + + tracing::info!(target = %args.api_address, "Ping load tests finished"); + + result = evaluate_highest_rtt(rtts, result, THRESHOLD_LOAD_AVG, THRESHOLD_LOAD_POOR); + + result +} + +async fn ping_continuously(address: String, tx: mpsc::Sender, max_duration: Duration) { + let start = Instant::now(); + + while start.elapsed() < max_duration { + let before = Instant::now(); + + match timeout(Duration::from_secs(1), TcpStream::connect(&address)).await { + Ok(Ok(_conn)) => { + let rtt = before.elapsed(); + if tx.send(rtt).is_err() { + return; + } + } + _ => return, + } + + let sleep_ms = rand::thread_rng().gen_range(0..100); + tokio::time::sleep(Duration::from_millis(sleep_ms)).await; + } } From 2ee355bfe9b12deb0bb4507fb0c8743b9044fa92 Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Thu, 26 Feb 2026 21:22:30 +0530 Subject: [PATCH 2/9] suggestions --- crates/cli/src/commands/test/mod.rs | 13 +- crates/cli/src/commands/test/validator.rs | 143 ++++++++++++++++------ 2 files changed, 110 insertions(+), 46 deletions(-) diff --git a/crates/cli/src/commands/test/mod.rs b/crates/cli/src/commands/test/mod.rs index 800d571c..d16f097d 100644 --- a/crates/cli/src/commands/test/mod.rs +++ b/crates/cli/src/commands/test/mod.rs @@ -120,18 +120,13 @@ pub struct TestConfigArgs { } /// Lists available test case names for a given test category. -/// TODO: Fill with enums TestCases of each category fn list_test_cases(category: TestCategory) -> Vec { // Returns available test case names for each category. match category { - TestCategory::Validator => { - // From validator::supported_validator_test_cases() - vec![ - "Ping".to_string(), - "PingMeasure".to_string(), - "PingLoad".to_string(), - ] - } + TestCategory::Validator => validator::ValidatorTestCase::all() + .iter() + .map(|tc| tc.name().to_string()) + .collect(), TestCategory::Beacon => { // TODO: Extract from beacon::supported_beacon_test_cases() vec![] diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index 7b5e34ab..82bcfb18 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -1,18 +1,19 @@ //! Validator client connectivity tests. -use std::{collections::HashMap, io::Write, sync::mpsc, time::Duration}; +use std::{io::Write, time::Duration}; use clap::Args; use rand::Rng; use tokio::{ net::TcpStream, + sync::mpsc, time::{Instant, timeout}, }; use super::{ - AllCategoriesResult, TestCaseName, TestCategory, TestCategoryResult, TestConfigArgs, - TestResult, TestVerdict, calculate_score, evaluate_highest_rtt, evaluate_rtt, filter_tests, - publish_result_to_obol_api, sort_tests, write_result_to_file, write_result_to_writer, + AllCategoriesResult, TestCategory, TestCategoryResult, TestConfigArgs, TestResult, TestVerdict, + calculate_score, evaluate_highest_rtt, evaluate_rtt, publish_result_to_obol_api, + write_result_to_file, write_result_to_writer, }; use crate::{duration::Duration as CliDuration, error::Result}; @@ -22,6 +23,34 @@ const THRESHOLD_MEASURE_POOR: Duration = Duration::from_millis(240); const THRESHOLD_LOAD_AVG: Duration = Duration::from_millis(50); const THRESHOLD_LOAD_POOR: Duration = Duration::from_millis(240); +/// Validator test cases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ValidatorTestCase { + Ping, + PingMeasure, + PingLoad, +} + +impl ValidatorTestCase { + /// Returns all validator test cases. + pub fn all() -> &'static [ValidatorTestCase] { + &[ + ValidatorTestCase::Ping, + ValidatorTestCase::PingMeasure, + ValidatorTestCase::PingLoad, + ] + } + + /// Returns the test name as a string. + pub fn name(&self) -> &'static str { + match self { + ValidatorTestCase::Ping => "Ping", + ValidatorTestCase::PingMeasure => "PingMeasure", + ValidatorTestCase::PingLoad => "PingLoad", + } + } +} + /// Arguments for the validator test command. #[derive(Args, Clone, Debug)] pub struct TestValidatorArgs { @@ -53,12 +82,16 @@ pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result = if let Some(ref filter) = args.test_config.test_cases + { + ValidatorTestCase::all() + .iter() + .filter(|tc| filter.contains(&tc.name().to_string())) + .copied() + .collect() + } else { + ValidatorTestCase::all().to_vec() + }; if queued_tests.is_empty() { return Err(crate::error::CliError::Other( @@ -66,26 +99,9 @@ pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result ping_test(&args).await, - "PingMeasure" => ping_measure_test(&args).await, - "PingLoad" => ping_load_test(&args).await, - _ => TestResult::new(&test.name).fail(std::io::Error::other("unknown test")), - }; - results.push(result); - } - results - }) - .await - .unwrap_or_else(|_| { - vec![TestResult::new("Timeout").fail(std::io::Error::other("timeout interrupted"))] - }); + let test_results = run_tests_with_timeout(&args, &queued_tests).await; + let score = calculate_score(&test_results); let mut res = TestCategoryResult::new(TestCategory::Validator); @@ -117,8 +133,55 @@ pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result Vec { + let (tx, mut rx) = mpsc::channel::(100); + let mut test_iter = tests.iter().peekable(); + + let timeout_result = tokio::time::timeout(args.test_config.timeout, async { + while let Some(&test_case) = test_iter.next() { + let result = run_single_test(args, test_case).await; + let _ = tx.send(result); + } + }) + .await; + + // Collect all completed results + drop(tx); + let mut results = Vec::new(); + while let Ok(result) = rx.try_recv() { + results.push(result); + } + + if timeout_result.is_err() { + if let Some(&interrupted_test) = test_iter.peek() { + results.push( + TestResult::new(interrupted_test.name()) + .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), + ); + } + } + + results +} + +/// Runs a single test case. +async fn run_single_test(args: &TestValidatorArgs, test_case: ValidatorTestCase) -> TestResult { + match test_case { + ValidatorTestCase::Ping => ping_test(args).await, + ValidatorTestCase::PingMeasure => ping_measure_test(args).await, + ValidatorTestCase::PingLoad => ping_load_test(args).await, + } +} + async fn ping_test(args: &TestValidatorArgs) -> TestResult { - let mut result = TestResult::new("Ping"); + let mut result = TestResult::new(ValidatorTestCase::Ping.name()); match timeout( Duration::from_secs(1), @@ -144,7 +207,7 @@ async fn ping_test(args: &TestValidatorArgs) -> TestResult { } async fn ping_measure_test(args: &TestValidatorArgs) -> TestResult { - let mut result = TestResult::new("PingMeasure"); + let mut result = TestResult::new(ValidatorTestCase::PingMeasure.name()); let before = Instant::now(); match timeout( @@ -178,9 +241,9 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { "Running ping load tests..." ); - let mut result = TestResult::new("PingLoad"); + let mut result = TestResult::new(ValidatorTestCase::PingLoad.name()); - let (tx, rx) = mpsc::channel::(); + let (tx, mut rx) = mpsc::channel::(100); let address = args.api_address.clone(); let duration = args.load_test_duration; @@ -188,6 +251,7 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { let start = Instant::now(); let mut interval = tokio::time::interval(Duration::from_secs(1)); + interval.tick().await; while start.elapsed() < duration { interval.tick().await; @@ -222,15 +286,20 @@ async fn ping_continuously(address: String, tx: mpsc::Sender, max_dura let before = Instant::now(); match timeout(Duration::from_secs(1), TcpStream::connect(&address)).await { - Ok(Ok(_conn)) => { + Ok(Ok(conn)) => { let rtt = before.elapsed(); - if tx.send(rtt).is_err() { + if tx.send(rtt).await.is_err() { + drop(conn); return; } } - _ => return, + Ok(Err(e)) => { + tracing::warn!(target = %address, error = ?e, "Ping connection attempt failed during load test"); + } + Err(e) => { + tracing::warn!(target = %address, error = ?e, "Ping connection attempt timed out during load test"); + } } - let sleep_ms = rand::thread_rng().gen_range(0..100); tokio::time::sleep(Duration::from_millis(sleep_ms)).await; } From 073a39d37f60866fed1ae6aaf45984e3bee2ff9e Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Thu, 26 Feb 2026 21:27:41 +0530 Subject: [PATCH 3/9] clippy --- crates/cli/src/commands/test/validator.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index 82bcfb18..ded915f2 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -145,9 +145,9 @@ async fn run_tests_with_timeout( let mut test_iter = tests.iter().peekable(); let timeout_result = tokio::time::timeout(args.test_config.timeout, async { - while let Some(&test_case) = test_iter.next() { + for &test_case in test_iter.by_ref() { let result = run_single_test(args, test_case).await; - let _ = tx.send(result); + let _ = tx.send(result).await; } }) .await; @@ -159,13 +159,13 @@ async fn run_tests_with_timeout( results.push(result); } - if timeout_result.is_err() { - if let Some(&interrupted_test) = test_iter.peek() { - results.push( - TestResult::new(interrupted_test.name()) - .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), - ); - } + if timeout_result.is_err() + && let Some(&interrupted_test) = test_iter.peek() + { + results.push( + TestResult::new(interrupted_test.name()) + .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), + ); } results From 3553cc50179f9c40a2d879df320a1941a90f8836 Mon Sep 17 00:00:00 2001 From: PoulavBhowmick03 Date: Wed, 18 Mar 2026 01:30:26 +0530 Subject: [PATCH 4/9] simplify timeout iteration --- crates/cli/src/commands/test/validator.rs | 56 ++++++++++++----------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index ded915f2..13095f22 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -141,31 +141,24 @@ async fn run_tests_with_timeout( args: &TestValidatorArgs, tests: &[ValidatorTestCase], ) -> Vec { - let (tx, mut rx) = mpsc::channel::(100); - let mut test_iter = tests.iter().peekable(); - - let timeout_result = tokio::time::timeout(args.test_config.timeout, async { - for &test_case in test_iter.by_ref() { - let result = run_single_test(args, test_case).await; - let _ = tx.send(result).await; - } - }) - .await; - - // Collect all completed results - drop(tx); let mut results = Vec::new(); - while let Ok(result) = rx.try_recv() { - results.push(result); - } - - if timeout_result.is_err() - && let Some(&interrupted_test) = test_iter.peek() - { - results.push( - TestResult::new(interrupted_test.name()) - .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), - ); + let timeout_deadline = tokio::time::Instant::now() + .checked_add(args.test_config.timeout) + .expect("timeout overflow"); + + for &test_case in tests { + let remaining = timeout_deadline.saturating_duration_since(tokio::time::Instant::now()); + + match tokio::time::timeout(remaining, run_single_test(args, test_case)).await { + Ok(result) => results.push(result), + Err(_) => { + results.push( + TestResult::new(test_case.name()) + .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), + ); + break; + } + } } results @@ -243,13 +236,14 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { let mut result = TestResult::new(ValidatorTestCase::PingLoad.name()); - let (tx, mut rx) = mpsc::channel::(100); + let (tx, mut rx) = mpsc::channel::(i16::MAX as usize); let address = args.api_address.clone(); let duration = args.load_test_duration; let handle = tokio::spawn(async move { let start = Instant::now(); let mut interval = tokio::time::interval(Duration::from_secs(1)); + let mut workers = tokio::task::JoinSet::new(); interval.tick().await; while start.elapsed() < duration { @@ -259,16 +253,24 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { let addr = address.clone(); let remaining = duration.saturating_sub(start.elapsed()); - tokio::spawn(async move { + workers.spawn(async move { ping_continuously(addr, tx, remaining).await; }); } + + // Drop the scheduler's clone so only workers hold senders + drop(tx); + + // Wait for all spawned ping workers to finish + while workers.join_next().await.is_some() {} }); let _ = handle.await; + // All senders dropped, collect all RTTs + rx.close(); let mut rtts = Vec::new(); - while let Ok(rtt) = rx.try_recv() { + while let Some(rtt) = rx.recv().await { rtts.push(rtt); } From 3d9cb4a9f1134e5a3cd8c9f6b95726177c5b00f0 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:42:48 +0200 Subject: [PATCH 5/9] fix(cli): address validator test PR review comments - Replace `name()` method with `fmt::Display` impl on `ValidatorTestCase` - Initialize tracing with `pluto_tracing::init` in `run` - Move `start_time` immediately before `run_tests_with_timeout` - Fix `ERR_TIMEOUT_INTERRUPTED` message to `"timeout/interrupted"` - Replace `checked_add`/`expect` with `Duration::saturating_sub` for timeout tracking - Remove outer `tokio::spawn` in `ping_load_test`; use a scoped block so the `JoinSet` is dropped (aborting workers) on timeout cancellation - Use `workers.join_all().await` instead of manual `join_next` loop - Change `ping_continuously` signature to `impl AsRef` - Remove unnecessary `drop(conn)` before `return` - Qualify `super::` helper functions instead of importing them directly Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/cli/src/commands/test/mod.rs | 2 +- crates/cli/src/commands/test/validator.rs | 91 ++++++++++++----------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/crates/cli/src/commands/test/mod.rs b/crates/cli/src/commands/test/mod.rs index f5e7ef7e..bebe8519 100644 --- a/crates/cli/src/commands/test/mod.rs +++ b/crates/cli/src/commands/test/mod.rs @@ -125,7 +125,7 @@ fn list_test_cases(category: TestCategory) -> Vec { match category { TestCategory::Validator => validator::ValidatorTestCase::all() .iter() - .map(|tc| tc.name().to_string()) + .map(|tc| tc.to_string()) .collect(), TestCategory::Beacon => { // TODO: Extract from beacon::supported_beacon_test_cases() diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index 13095f22..eca838a6 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -1,6 +1,6 @@ //! Validator client connectivity tests. -use std::{io::Write, time::Duration}; +use std::{fmt, io::Write, time::Duration}; use clap::Args; use rand::Rng; @@ -12,8 +12,6 @@ use tokio::{ use super::{ AllCategoriesResult, TestCategory, TestCategoryResult, TestConfigArgs, TestResult, TestVerdict, - calculate_score, evaluate_highest_rtt, evaluate_rtt, publish_result_to_obol_api, - write_result_to_file, write_result_to_writer, }; use crate::{duration::Duration as CliDuration, error::Result}; @@ -26,8 +24,11 @@ const THRESHOLD_LOAD_POOR: Duration = Duration::from_millis(240); /// Validator test cases. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ValidatorTestCase { + /// TCP connectivity check. Ping, + /// TCP round-trip time measurement. PingMeasure, + /// Sustained TCP load test. PingLoad, } @@ -40,14 +41,15 @@ impl ValidatorTestCase { ValidatorTestCase::PingLoad, ] } +} - /// Returns the test name as a string. - pub fn name(&self) -> &'static str { - match self { +impl fmt::Display for ValidatorTestCase { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { ValidatorTestCase::Ping => "Ping", ValidatorTestCase::PingMeasure => "PingMeasure", ValidatorTestCase::PingLoad => "PingLoad", - } + }) } } @@ -77,16 +79,21 @@ pub struct TestValidatorArgs { /// Runs the validator client tests. pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result { - tracing::info!("Starting validator client test"); + pluto_tracing::init( + &pluto_tracing::TracingConfig::builder() + .with_default_console() + .build(), + ) + .expect("Failed to initialize tracing"); - let start_time = Instant::now(); + tracing::info!("Starting validator client test"); // Get and filter test cases let queued_tests: Vec = if let Some(ref filter) = args.test_config.test_cases { ValidatorTestCase::all() .iter() - .filter(|tc| filter.contains(&tc.name().to_string())) + .filter(|tc| filter.contains(&tc.to_string())) .copied() .collect() } else { @@ -99,22 +106,23 @@ pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result Result Result Vec { let mut results = Vec::new(); - let timeout_deadline = tokio::time::Instant::now() - .checked_add(args.test_config.timeout) - .expect("timeout overflow"); + let start = Instant::now(); for &test_case in tests { - let remaining = timeout_deadline.saturating_duration_since(tokio::time::Instant::now()); + let remaining = args.test_config.timeout.saturating_sub(start.elapsed()); match tokio::time::timeout(remaining, run_single_test(args, test_case)).await { Ok(result) => results.push(result), Err(_) => { results.push( - TestResult::new(test_case.name()) + TestResult::new(test_case.to_string()) .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), ); break; @@ -174,7 +180,7 @@ async fn run_single_test(args: &TestValidatorArgs, test_case: ValidatorTestCase) } async fn ping_test(args: &TestValidatorArgs) -> TestResult { - let mut result = TestResult::new(ValidatorTestCase::Ping.name()); + let mut result = TestResult::new(ValidatorTestCase::Ping.to_string()); match timeout( Duration::from_secs(1), @@ -200,7 +206,7 @@ async fn ping_test(args: &TestValidatorArgs) -> TestResult { } async fn ping_measure_test(args: &TestValidatorArgs) -> TestResult { - let mut result = TestResult::new(ValidatorTestCase::PingMeasure.name()); + let mut result = TestResult::new(ValidatorTestCase::PingMeasure.to_string()); let before = Instant::now(); match timeout( @@ -211,7 +217,8 @@ async fn ping_measure_test(args: &TestValidatorArgs) -> TestResult { { Ok(Ok(_conn)) => { let rtt = before.elapsed(); - result = evaluate_rtt(rtt, result, THRESHOLD_MEASURE_AVG, THRESHOLD_MEASURE_POOR); + result = + super::evaluate_rtt(rtt, result, THRESHOLD_MEASURE_AVG, THRESHOLD_MEASURE_POOR); } Ok(Err(e)) => { return result.fail(e); @@ -234,13 +241,12 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { "Running ping load tests..." ); - let mut result = TestResult::new(ValidatorTestCase::PingLoad.name()); - + let mut result = TestResult::new(ValidatorTestCase::PingLoad.to_string()); let (tx, mut rx) = mpsc::channel::(i16::MAX as usize); let address = args.api_address.clone(); let duration = args.load_test_duration; - let handle = tokio::spawn(async move { + { let start = Instant::now(); let mut interval = tokio::time::interval(Duration::from_secs(1)); let mut workers = tokio::task::JoinSet::new(); @@ -253,19 +259,17 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { let addr = address.clone(); let remaining = duration.saturating_sub(start.elapsed()); - workers.spawn(async move { - ping_continuously(addr, tx, remaining).await; - }); + workers.spawn(ping_continuously(addr, tx, remaining)); } // Drop the scheduler's clone so only workers hold senders drop(tx); // Wait for all spawned ping workers to finish - while workers.join_next().await.is_some() {} - }); + workers.join_all().await; + } - let _ = handle.await; + tracing::info!(target = %args.api_address, "Ping load tests finished"); // All senders dropped, collect all RTTs rx.close(); @@ -274,24 +278,27 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { rtts.push(rtt); } - tracing::info!(target = %args.api_address, "Ping load tests finished"); - - result = evaluate_highest_rtt(rtts, result, THRESHOLD_LOAD_AVG, THRESHOLD_LOAD_POOR); + result = + super::evaluate_highest_rtt(rtts, result, THRESHOLD_LOAD_AVG, THRESHOLD_LOAD_POOR); result } -async fn ping_continuously(address: String, tx: mpsc::Sender, max_duration: Duration) { +async fn ping_continuously( + address: impl AsRef, + tx: mpsc::Sender, + max_duration: Duration, +) { + let address = address.as_ref(); let start = Instant::now(); while start.elapsed() < max_duration { let before = Instant::now(); - match timeout(Duration::from_secs(1), TcpStream::connect(&address)).await { - Ok(Ok(conn)) => { + match timeout(Duration::from_secs(1), TcpStream::connect(address)).await { + Ok(Ok(_conn)) => { let rtt = before.elapsed(); if tx.send(rtt).await.is_err() { - drop(conn); return; } } From 88fb5cd8498b50fe0c3e7875cc5ec63d24ff98f8 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:47:50 +0200 Subject: [PATCH 6/9] fix(deny): add advisory ignores for transitive deps All entries are transitive dependencies that cannot be immediately upgraded. None are reachable from Pluto's production code paths. Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/cli/src/commands/test/validator.rs | 3 +-- deny.toml | 27 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index eca838a6..3e1511ed 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -278,8 +278,7 @@ async fn ping_load_test(args: &TestValidatorArgs) -> TestResult { rtts.push(rtt); } - result = - super::evaluate_highest_rtt(rtts, result, THRESHOLD_LOAD_AVG, THRESHOLD_LOAD_POOR); + result = super::evaluate_highest_rtt(rtts, result, THRESHOLD_LOAD_AVG, THRESHOLD_LOAD_POOR); result } diff --git a/deny.toml b/deny.toml index 20794394..b7012771 100644 --- a/deny.toml +++ b/deny.toml @@ -23,7 +23,32 @@ feature-depth = 1 db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] yanked = "deny" -ignore = [] +ignore = [ + # Unsoundness in `rand` 0.7.3 and 0.8.5 reachable only via a custom `log` + # logger that calls `rand::rng()` and hits a reseed during the log event. + # The only affected pulls are transitive: `cuckoofilter` -> `libp2p-floodsub` + # (stuck on 0.7.3) and `alloy-signer-local` (stuck on 0.8.5). Neither is + # reachable from Pluto's loggers. Remove once upstream bumps to >=0.9.3. + { id = "RUSTSEC-2026-0097", reason = "transitive rand <0.9.3 via cuckoofilter and alloy-signer-local; not triggerable from our code" }, + # CN validation logic error in aws-lc-sys <=0.38.0. Transitive dep via + # libp2p-quic, libp2p-websocket, reqwest, tracing-loki; can't upgrade yet. + { id = "RUSTSEC-2026-0044", reason = "transitive aws-lc-sys 0.38.0; upgrade blocked by transitive constraints" }, + # CRL distribution point matching logic error in aws-lc-sys <=0.38.0. + # Same transitive path as RUSTSEC-2026-0044 above. + { id = "RUSTSEC-2026-0048", reason = "transitive aws-lc-sys 0.38.0; upgrade blocked by transitive constraints" }, + # CRL distribution point incomplete check in rustls-webpki 0.103.9. + # Transitive via rustls-platform-verifier. Remove once upstream upgrades. + { id = "RUSTSEC-2026-0049", reason = "transitive rustls-webpki 0.103.9 via rustls-platform-verifier; not directly used" }, + # URI name constraints ignored in rustls-webpki 0.103.9. Same path. + { id = "RUSTSEC-2026-0098", reason = "transitive rustls-webpki 0.103.9 via rustls-platform-verifier; not directly used" }, + # tar-rs <=0.4.44 archive unpacking PAX handling issues (both advisories). + # Transitive via pluto-app. Remove once upstream bumps to >=0.4.45. + { id = "RUSTSEC-2026-0067", reason = "transitive tar 0.4.44 via pluto-app; upgrade blocked by upstream" }, + { id = "RUSTSEC-2026-0068", reason = "transitive tar 0.4.44 via pluto-app; upgrade blocked by upstream" }, + # astral-tokio-tar <=0.5.6 malformed PAX extension issue. Dev-only dep + # via testcontainers -> pluto-eth2api (test only). Not in production builds. + { id = "RUSTSEC-2026-0066", reason = "dev-only transitive astral-tokio-tar 0.5.6 via testcontainers; not in production" }, +] unmaintained = "workspace" [licenses] From 10b4dfa5d366bbe68c7fe75332ab2ef6a8310dcc Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:36:52 +0200 Subject: [PATCH 7/9] fix(cli): address emlautarom1 review comments on validator tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CancellationToken to validator::run and run_tests_with_timeout; use tokio::select! so Ctrl+C (SIGINT) cancels in-progress tests - Replace ERR_TIMEOUT_INTERRUPTED string const with CliError::TimeoutInterrupted and CliError::Other("test case not supported") with CliError::TestCaseNotSupported; remove leading underscore from both variants in the error enum - Remove RUSTSEC advisory ignores added to deny.toml; run cargo update instead — all seven advisories are resolved by updated transitive deps Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- Cargo.lock | 193 +++++++++++----------- crates/cli/src/commands/test/validator.rs | 36 ++-- crates/cli/src/error.rs | 4 +- crates/cli/src/main.rs | 2 +- deny.toml | 18 -- 5 files changed, 125 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e16f158..9d162211 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,9 +104,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e9e31d834fe25fe991b8884e4b9f0e59db4a97d86e05d1464d6899c013cd62" +checksum = "84e0378e959aa6a885897522080a990e80eb317f1e9a222a604492ea50e13096" dependencies = [ "alloy-primitives", "num_enum", @@ -373,13 +373,13 @@ dependencies = [ "derive_more", "foldhash 0.2.0", "hashbrown 0.16.1", - "indexmap 2.13.1", + "indexmap 2.14.0", "itoa", "k256", "keccak-asm", "paste", "proptest", - "rand 0.9.3", + "rand 0.9.4", "rapidhash", "ruint", "rustc-hash", @@ -581,7 +581,7 @@ dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.13.1", + "indexmap 2.14.0", "proc-macro-error2", "proc-macro2", "quote", @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -1326,9 +1326,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -1398,7 +1398,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.3", + "rand 0.9.4", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -1592,9 +1592,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -1855,15 +1855,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2187,7 +2178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2527,9 +2518,9 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2128a84f7a3850d54ee343334e3392cca61f9f6aa9441eec481b9394b43c238b" +checksum = "368a4a4e4273b0135111fe9464e35465067766a8f664615b5a86338b73864407" dependencies = [ "alloy-primitives", "ethereum_serde_utils", @@ -2607,7 +2598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" dependencies = [ "portable-atomic", - "rand 0.9.3", + "rand 0.9.4", "web-time", ] @@ -2999,7 +2990,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.1", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3054,6 +3045,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.9.1" @@ -3136,7 +3133,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.3", + "rand 0.9.4", "ring", "socket2 0.5.10", "thiserror 2.0.18", @@ -3159,7 +3156,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "resolv-conf", "smallvec", "thiserror 2.0.18", @@ -3284,15 +3281,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -3554,7 +3550,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.9.3", + "rand 0.9.4", "tokio", "url", "xmltree", @@ -3593,12 +3589,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -3736,9 +3732,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -3794,9 +3790,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libgit2-sys" @@ -4530,14 +4526,14 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.4", ] [[package]] @@ -4591,9 +4587,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -4750,11 +4746,11 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c" dependencies = [ - "core2", + "no_std_io2", "serde", "unsigned-varint 0.8.0", ] @@ -4856,6 +4852,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" +dependencies = [ + "memchr", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -5046,7 +5051,7 @@ dependencies = [ "eventsource-stream", "futures-core", "http", - "indexmap 2.13.1", + "indexmap 2.14.0", "oas3", "prettyplease", "proc-macro2", @@ -5103,9 +5108,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" dependencies = [ "bitflags", "cfg-if", @@ -5135,9 +5140,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" dependencies = [ "cc", "libc", @@ -5319,7 +5324,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.1", + "indexmap 2.14.0", ] [[package]] @@ -5366,9 +5371,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -5995,7 +6000,7 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.3", + "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -6156,7 +6161,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.3", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -6236,9 +6241,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6332,9 +6337,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -6374,9 +6379,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -6589,7 +6594,7 @@ dependencies = [ "primitive-types", "proptest", "rand 0.8.5", - "rand 0.9.3", + "rand 0.9.4", "rlp", "ruint-macro", "serde_core", @@ -6657,9 +6662,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", "log", @@ -6722,9 +6727,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", @@ -6974,7 +6979,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -7035,7 +7040,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.1", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -7062,7 +7067,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -7628,9 +7633,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.0" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd1c4c0fc4a7ab90fc15ef6daaa3ec3b893f004f915f2392557ed23237820cd" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "bytes", "libc", @@ -7735,7 +7740,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -7749,7 +7754,7 @@ version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.1", + "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.1", @@ -7818,7 +7823,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.1", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -8311,9 +8316,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -8324,9 +8329,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -8334,9 +8339,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8344,9 +8349,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -8357,9 +8362,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -8381,7 +8386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.1", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -8407,7 +8412,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.1", + "indexmap 2.14.0", "semver 1.0.28", ] @@ -8427,9 +8432,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -8974,7 +8979,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.1", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -9005,7 +9010,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.13.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -9024,7 +9029,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.1", + "indexmap 2.14.0", "log", "semver 1.0.28", "serde", @@ -9129,7 +9134,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.9.3", + "rand 0.9.4", "static_assertions", "web-time", ] diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index 3e1511ed..e61b5443 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -9,11 +9,12 @@ use tokio::{ sync::mpsc, time::{Instant, timeout}, }; +use tokio_util::sync::CancellationToken; use super::{ AllCategoriesResult, TestCategory, TestCategoryResult, TestConfigArgs, TestResult, TestVerdict, }; -use crate::{duration::Duration as CliDuration, error::Result}; +use crate::{duration::Duration as CliDuration, error::{CliError, Result}}; // Thresholds (from Go implementation) const THRESHOLD_MEASURE_AVG: Duration = Duration::from_millis(50); @@ -78,7 +79,11 @@ pub struct TestValidatorArgs { } /// Runs the validator client tests. -pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result { +pub async fn run( + args: TestValidatorArgs, + writer: &mut dyn Write, + ct: CancellationToken, +) -> Result { pluto_tracing::init( &pluto_tracing::TracingConfig::builder() .with_default_console() @@ -101,13 +106,11 @@ pub async fn run(args: TestValidatorArgs, writer: &mut dyn Write) -> Result Result Vec { let mut results = Vec::new(); let start = Instant::now(); @@ -155,12 +156,21 @@ async fn run_tests_with_timeout( for &test_case in tests { let remaining = args.test_config.timeout.saturating_sub(start.elapsed()); - match tokio::time::timeout(remaining, run_single_test(args, test_case)).await { - Ok(result) => results.push(result), - Err(_) => { + tokio::select! { + result = run_single_test(args, test_case) => { + results.push(result); + } + _ = tokio::time::sleep(remaining) => { + results.push( + TestResult::new(test_case.to_string()) + .fail(CliError::TimeoutInterrupted), + ); + break; + } + _ = ct.cancelled() => { results.push( TestResult::new(test_case.to_string()) - .fail(std::io::Error::other(ERR_TIMEOUT_INTERRUPTED)), + .fail(CliError::TimeoutInterrupted), ); break; } diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index d59ac1b9..d7163982 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -64,11 +64,11 @@ pub enum CliError { /// Test timeout or interrupted. #[error("timeout/interrupted")] - _TimeoutInterrupted, + TimeoutInterrupted, /// Test case not supported. #[error("test case not supported")] - _TestCaseNotSupported, + TestCaseNotSupported, /// Relay P2P error. #[error("Relay P2P error: {0}")] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 65551bc0..74387402 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -72,7 +72,7 @@ async fn run() -> std::result::Result<(), CliError> { .map(|_| ()) } TestCommands::Validator(args) => { - commands::test::validator::run(args, &mut stdout) + commands::test::validator::run(args, &mut stdout, ct.clone()) .await .map(|_| ()) } diff --git a/deny.toml b/deny.toml index 86ee3b5c..93786586 100644 --- a/deny.toml +++ b/deny.toml @@ -30,24 +30,6 @@ ignore = [ # (stuck on 0.7.3) and `alloy-signer-local` (stuck on 0.8.5). Neither is # reachable from Pluto's loggers. Remove once upstream bumps to >=0.9.3. { id = "RUSTSEC-2026-0097", reason = "transitive rand <0.9.3 via cuckoofilter and alloy-signer-local; not triggerable from our code" }, - # CN validation logic error in aws-lc-sys <=0.38.0. Transitive dep via - # libp2p-quic, libp2p-websocket, reqwest, tracing-loki; can't upgrade yet. - { id = "RUSTSEC-2026-0044", reason = "transitive aws-lc-sys 0.38.0; upgrade blocked by transitive constraints" }, - # CRL distribution point matching logic error in aws-lc-sys <=0.38.0. - # Same transitive path as RUSTSEC-2026-0044 above. - { id = "RUSTSEC-2026-0048", reason = "transitive aws-lc-sys 0.38.0; upgrade blocked by transitive constraints" }, - # CRL distribution point incomplete check in rustls-webpki 0.103.9. - # Transitive via rustls-platform-verifier. Remove once upstream upgrades. - { id = "RUSTSEC-2026-0049", reason = "transitive rustls-webpki 0.103.9 via rustls-platform-verifier; not directly used" }, - # URI name constraints ignored in rustls-webpki 0.103.9. Same path. - { id = "RUSTSEC-2026-0098", reason = "transitive rustls-webpki 0.103.9 via rustls-platform-verifier; not directly used" }, - # tar-rs <=0.4.44 archive unpacking PAX handling issues (both advisories). - # Transitive via pluto-app. Remove once upstream bumps to >=0.4.45. - { id = "RUSTSEC-2026-0067", reason = "transitive tar 0.4.44 via pluto-app; upgrade blocked by upstream" }, - { id = "RUSTSEC-2026-0068", reason = "transitive tar 0.4.44 via pluto-app; upgrade blocked by upstream" }, - # astral-tokio-tar <=0.5.6 malformed PAX extension issue. Dev-only dep - # via testcontainers -> pluto-eth2api (test only). Not in production builds. - { id = "RUSTSEC-2026-0066", reason = "dev-only transitive astral-tokio-tar 0.5.6 via testcontainers; not in production" }, ] unmaintained = "workspace" From 47abf92c8c11808220f769c952dc7e4c64e784ce Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:40:48 +0200 Subject: [PATCH 8/9] fix linter --- crates/cli/src/commands/test/validator.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index e61b5443..077e13fd 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -14,7 +14,10 @@ use tokio_util::sync::CancellationToken; use super::{ AllCategoriesResult, TestCategory, TestCategoryResult, TestConfigArgs, TestResult, TestVerdict, }; -use crate::{duration::Duration as CliDuration, error::{CliError, Result}}; +use crate::{ + duration::Duration as CliDuration, + error::{CliError, Result}, +}; // Thresholds (from Go implementation) const THRESHOLD_MEASURE_AVG: Duration = Duration::from_millis(50); From 8ec9c69d6c4a425c341cd92851565f101355199d Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:26:22 +0200 Subject: [PATCH 9/9] fix: stop ping_continuously loop on dial error to match charon behavior In Charon's `pingValidatorContinuously`, both dial errors and timeouts cause an immediate `return`, stopping the loop. Port this behavior to Rust so a failing connection doesn't inflate the RTT sample set with absent readings. Co-Authored-By: varex83 --- crates/cli/src/commands/test/validator.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/cli/src/commands/test/validator.rs b/crates/cli/src/commands/test/validator.rs index 077e13fd..e727cae3 100644 --- a/crates/cli/src/commands/test/validator.rs +++ b/crates/cli/src/commands/test/validator.rs @@ -314,11 +314,8 @@ async fn ping_continuously( return; } } - Ok(Err(e)) => { - tracing::warn!(target = %address, error = ?e, "Ping connection attempt failed during load test"); - } - Err(e) => { - tracing::warn!(target = %address, error = ?e, "Ping connection attempt timed out during load test"); + Ok(Err(_)) | Err(_) => { + return; } } let sleep_ms = rand::thread_rng().gen_range(0..100);