From bacc38c3da2daed7fb92ab196a65440208dd1e8f Mon Sep 17 00:00:00 2001 From: Mac L Date: Thu, 6 May 2021 00:36:22 +0000 Subject: [PATCH] Add testing for beacon node and validator client CLI flags (#2311) ## Issue Addressed N/A ## Proposed Changes Add unit tests for the various CLI flags associated with the beacon node and validator client. These changes require the addition of two new flags: `dump-config` and `immediate-shutdown`. ## Additional Info Both `dump-config` and `immediate-shutdown` are marked as hidden since they should only be used in testing and other advanced use cases. **Note:** This requires changing `main.rs` so that the flags can adjust the program behavior as necessary. Co-authored-by: Paul Hauner --- Cargo.lock | 3 + beacon_node/beacon_chain/src/beacon_chain.rs | 10 +- beacon_node/beacon_chain/src/builder.rs | 5 +- beacon_node/beacon_chain/src/errors.rs | 3 +- beacon_node/beacon_chain/src/test_utils.rs | 3 +- beacon_node/eth2_libp2p/tests/common/mod.rs | 2 +- beacon_node/network/src/service.rs | 13 +- beacon_node/src/config.rs | 2 +- common/task_executor/src/lib.rs | 24 +- lighthouse/Cargo.toml | 8 + lighthouse/environment/src/lib.rs | 16 +- lighthouse/src/main.rs | 87 +- lighthouse/tests/account_manager.rs | 6 +- lighthouse/tests/beacon_node.rs | 841 +++++++++++++++++++ lighthouse/tests/main.rs | 5 + lighthouse/tests/validator_client.rs | 346 ++++++++ 16 files changed, 1327 insertions(+), 47 deletions(-) create mode 100644 lighthouse/tests/beacon_node.rs create mode 100644 lighthouse/tests/main.rs create mode 100644 lighthouse/tests/validator_client.rs diff --git a/Cargo.lock b/Cargo.lock index c3290ebc8..a0ced6cf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3719,6 +3719,7 @@ dependencies = [ "directory", "env_logger 0.8.3", "environment", + "eth2_libp2p", "eth2_network_config", "futures 0.3.14", "lazy_static", @@ -3726,11 +3727,13 @@ dependencies = [ "lighthouse_version", "logging", "remote_signer", + "serde_json", "slashing_protection", "slog", "slog-async", "slog-term", "sloggers", + "task_executor", "tempfile", "tokio 1.5.0", "types", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 94079f028..7fa306b09 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -58,6 +58,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use store::iter::{BlockRootsIterator, ParentRootBlockIterator, StateRootsIterator}; use store::{Error as DBError, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp}; +use task_executor::ShutdownReason; use types::beacon_state::CloneConfig; use types::*; @@ -254,7 +255,7 @@ pub struct BeaconChain { pub disabled_forks: Vec, /// Sender given to tasks, so that if they encounter a state in which execution cannot /// continue they can request that everything shuts down. - pub shutdown_sender: Sender<&'static str>, + pub shutdown_sender: Sender, /// Logging to CLI, etc. pub(crate) log: Logger, /// Arbitrary bytes included in the blocks. @@ -1699,7 +1700,10 @@ impl BeaconChain { "error" => ?e, ); crit!(self.log, "You must use the `--purge-db` flag to clear the database and restart sync. You may be on a hostile network."); - shutdown_sender.try_send("Weak subjectivity checkpoint verification failed. Provided block root is not a checkpoint.") + shutdown_sender + .try_send(ShutdownReason::Failure( + "Weak subjectivity checkpoint verification failed. Provided block root is not a checkpoint." + )) .map_err(|err| BlockError::BeaconChainError(BeaconChainError::WeakSubjectivtyShutdownError(err)))?; return Err(BlockError::WeakSubjectivityConflict); } @@ -2811,7 +2815,7 @@ impl BeaconChain { } /// Get a channel to request shutting down. - pub fn shutdown_sender(&self) -> Sender<&'static str> { + pub fn shutdown_sender(&self) -> Sender { self.shutdown_sender.clone() } diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 63d2918b5..f905ef0ae 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -25,6 +25,7 @@ use std::marker::PhantomData; use std::sync::Arc; use std::time::Duration; use store::{HotColdDB, ItemStore}; +use task_executor::ShutdownReason; use types::{ BeaconBlock, BeaconState, ChainSpec, EthSpec, Graffiti, Hash256, PublicKeyBytes, Signature, SignedBeaconBlock, Slot, @@ -75,7 +76,7 @@ pub struct BeaconChainBuilder { eth1_chain: Option>, event_handler: Option>, slot_clock: Option, - shutdown_sender: Option>, + shutdown_sender: Option>, head_tracker: Option, validator_pubkey_cache: Option>, spec: ChainSpec, @@ -349,7 +350,7 @@ where } /// Sets a `Sender` to allow the beacon chain to send shutdown signals. - pub fn shutdown_sender(mut self, sender: Sender<&'static str>) -> Self { + pub fn shutdown_sender(mut self, sender: Sender) -> Self { self.shutdown_sender = Some(sender); self } diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 1f5662a62..9c47904dd 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -21,6 +21,7 @@ use state_processing::{ BlockProcessingError, SlotProcessingError, }; use std::time::Duration; +use task_executor::ShutdownReason; use types::*; macro_rules! easy_from_to { @@ -96,7 +97,7 @@ pub enum BeaconChainError { head_block_epoch: Epoch, }, WeakSubjectivtyVerificationFailure, - WeakSubjectivtyShutdownError(TrySendError<&'static str>), + WeakSubjectivtyShutdownError(TrySendError), AttestingPriorToHead { head_slot: Slot, request_slot: Slot, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index c1b1ed75e..8f89fb63f 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -25,6 +25,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Duration; use store::{config::StoreConfig, BlockReplay, HotColdDB, ItemStore, LevelDB, MemoryStore}; +use task_executor::ShutdownReason; use tempfile::{tempdir, TempDir}; use tree_hash::TreeHash; use types::{ @@ -115,7 +116,7 @@ pub struct BeaconChainHarness { pub chain: BeaconChain, pub spec: ChainSpec, pub data_dir: TempDir, - pub shutdown_receiver: Receiver<&'static str>, + pub shutdown_receiver: Receiver, pub rng: Mutex, } diff --git a/beacon_node/eth2_libp2p/tests/common/mod.rs b/beacon_node/eth2_libp2p/tests/common/mod.rs index e0aba71f6..a09f800d0 100644 --- a/beacon_node/eth2_libp2p/tests/common/mod.rs +++ b/beacon_node/eth2_libp2p/tests/common/mod.rs @@ -44,7 +44,7 @@ pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { // A bit of hack to find an unused port. /// -/// Does not guarantee that the given port is unused after the function exists, just that it was +/// Does not guarantee that the given port is unused after the function exits, just that it was /// unused before the function started (i.e., it does not reserve a port). pub fn unused_port(transport: &str) -> Result { let local_addr = match transport { diff --git a/beacon_node/network/src/service.rs b/beacon_node/network/src/service.rs index a25ec1038..082c1353f 100644 --- a/beacon_node/network/src/service.rs +++ b/beacon_node/network/src/service.rs @@ -16,6 +16,7 @@ use futures::prelude::*; use slog::{debug, error, info, o, trace, warn}; use std::{net::SocketAddr, sync::Arc, time::Duration}; use store::HotColdDB; +use task_executor::ShutdownReason; use tokio::sync::mpsc; use tokio::time::Sleep; use types::{EthSpec, RelativeEpoch, SubnetId, Unsigned, ValidatorSubscription}; @@ -522,10 +523,14 @@ fn spawn_service( service.network_globals.listen_multiaddrs.write().push(multiaddr); } Libp2pEvent::ZeroListeners => { - let _ = shutdown_sender.send("All listeners are closed. Unable to listen").await.map_err(|e| { - warn!(service.log, "failed to send a shutdown signal"; "error" => %e - ) - }); + let _ = shutdown_sender + .send(ShutdownReason::Failure("All listeners are closed. Unable to listen")) + .await + .map_err(|e| warn!( + service.log, + "failed to send a shutdown signal"; + "error" => %e + )); } } } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 20e8e64d9..0550e8e08 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -643,7 +643,7 @@ pub fn get_eth2_network_config(cli_args: &ArgMatches) -> Result &'static str { + match self { + ShutdownReason::Success(msg) => msg, + ShutdownReason::Failure(msg) => msg, + } + } +} + /// A wrapper over a runtime handle which can spawn async and blocking tasks. #[derive(Clone)] pub struct TaskExecutor { @@ -17,7 +35,7 @@ pub struct TaskExecutor { /// continue they can request that everything shuts down. /// /// The task must provide a reason for shutting down. - signal_tx: Sender<&'static str>, + signal_tx: Sender, log: slog::Logger, } @@ -31,7 +49,7 @@ impl TaskExecutor { runtime: Weak, exit: exit_future::Exit, log: slog::Logger, - signal_tx: Sender<&'static str>, + signal_tx: Sender, ) -> Self { Self { runtime, @@ -255,7 +273,7 @@ impl TaskExecutor { } /// Get a channel to request shutting down. - pub fn shutdown_sender(&self) -> Sender<&'static str> { + pub fn shutdown_sender(&self) -> Sender { self.signal_tx.clone() } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index cffadf5d8..73321879a 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -3,6 +3,7 @@ name = "lighthouse" version = "1.3.0" authors = ["Sigma Prime "] edition = "2018" +autotests = false [features] # Writes debugging .ssz files to /tmp during block processing. @@ -43,8 +44,15 @@ account_utils = { path = "../common/account_utils" } remote_signer = { "path" = "../remote_signer" } lighthouse_metrics = { path = "../common/lighthouse_metrics" } lazy_static = "1.4.0" +serde_json = "1.0.59" +task_executor = { path = "../common/task_executor" } [dev-dependencies] tempfile = "3.1.0" validator_dir = { path = "../common/validator_dir" } slashing_protection = { path = "../validator_client/slashing_protection" } +eth2_libp2p = { path = "../beacon_node/eth2_libp2p" } + +[[test]] +name = "lighthouse_tests" +path = "tests/main.rs" diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index fee7369ac..7cde85991 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -23,7 +23,7 @@ use std::fs::{rename as FsRename, OpenOptions}; use std::path::PathBuf; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use task_executor::TaskExecutor; +use task_executor::{ShutdownReason, TaskExecutor}; use tokio::runtime::{Builder as RuntimeBuilder, Runtime}; use types::{EthSpec, MainnetEthSpec, MinimalEthSpec, V012LegacyEthSpec}; @@ -314,9 +314,9 @@ impl RuntimeContext { pub struct Environment { runtime: Arc, /// Receiver side of an internal shutdown signal. - signal_rx: Option>, + signal_rx: Option>, /// Sender to request shutting down. - signal_tx: Sender<&'static str>, + signal_tx: Sender, signal: Option, exit: exit_future::Exit, log: Logger, @@ -365,7 +365,7 @@ impl Environment { /// Block the current thread until a shutdown signal is received. /// /// This can be either the user Ctrl-C'ing or a task requesting to shutdown. - pub fn block_until_shutdown_requested(&mut self) -> Result<(), String> { + pub fn block_until_shutdown_requested(&mut self) -> Result { // future of a task requesting to shutdown let mut rx = self .signal_rx @@ -398,11 +398,13 @@ impl Environment { .block_on(future::select(inner_shutdown, ctrlc_oneshot)) { future::Either::Left((Ok(reason), _)) => { - info!(self.log, "Internal shutdown received"; "reason" => reason); - Ok(()) + info!(self.log, "Internal shutdown received"; "reason" => reason.message()); + Ok(reason) } future::Either::Left((Err(e), _)) => Err(e.into()), - future::Either::Right((x, _)) => x.map_err(|e| format!("Ctrlc oneshot failed: {}", e)), + future::Either::Right((x, _)) => x + .map(|()| ShutdownReason::Success("Received Ctrl+C")) + .map_err(|e| format!("Ctrlc oneshot failed: {}", e)), } } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index acbc80a84..636b9d81d 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -7,8 +7,10 @@ use environment::EnvironmentBuilder; use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK}; use lighthouse_version::VERSION; use slog::{crit, info, warn}; +use std::fs::File; use std::path::PathBuf; use std::process::exit; +use task_executor::ShutdownReason; use types::{EthSpec, EthSpecId}; use validator_client::ProductionValidatorClient; @@ -125,6 +127,23 @@ fn main() { .global(true) ) + .arg( + Arg::with_name("dump-config") + .long("dump-config") + .hidden(true) + .help("Dumps the config to a desired location. Used for testing only.") + .takes_value(true) + .global(true) + ) + .arg( + Arg::with_name("immediate-shutdown") + .long("immediate-shutdown") + .hidden(true) + .help( + "Shuts down immediately after the Beacon Node or Validator has successfully launched. \ + Used for testing only, DO NOT USE IN PRODUCTION.") + .global(true) + ) .subcommand(beacon_node::cli_app()) .subcommand(boot_node::cli_app()) .subcommand(validator_client::cli_app()) @@ -285,6 +304,15 @@ fn run( &context.eth2_config().spec, context.log().clone(), )?; + let shutdown_flag = matches.is_present("immediate-shutdown"); + if let Some(dump_path) = clap_utils::parse_optional::(matches, "dump-config")? + { + let mut file = File::create(dump_path) + .map_err(|e| format!("Failed to create dumped config: {:?}", e))?; + serde_json::to_writer(&mut file, &config) + .map_err(|e| format!("Error serializing config: {:?}", e))?; + }; + environment.runtime().spawn(async move { if let Err(e) = ProductionBeaconNode::new(context.clone(), config).await { crit!(log, "Failed to start beacon node"; "reason" => e); @@ -292,7 +320,11 @@ fn run( // shutting down. let _ = executor .shutdown_sender() - .try_send("Failed to start beacon node"); + .try_send(ShutdownReason::Failure("Failed to start beacon node")); + } else if shutdown_flag { + let _ = executor.shutdown_sender().try_send(ShutdownReason::Success( + "Beacon node immediate shutdown triggered.", + )); } }); } @@ -302,23 +334,34 @@ fn run( let executor = context.executor.clone(); let config = validator_client::Config::from_cli(&matches, context.log()) .map_err(|e| format!("Unable to initialize validator config: {}", e))?; - environment.runtime().spawn(async move { - let run = async { - ProductionValidatorClient::new(context, config) + let shutdown_flag = matches.is_present("immediate-shutdown"); + if let Some(dump_path) = clap_utils::parse_optional::(matches, "dump-config")? + { + let mut file = File::create(dump_path) + .map_err(|e| format!("Failed to create dumped config: {:?}", e))?; + serde_json::to_writer(&mut file, &config) + .map_err(|e| format!("Error serializing config: {:?}", e))?; + }; + if !shutdown_flag { + environment.runtime().spawn(async move { + if let Err(e) = ProductionValidatorClient::new(context, config) .await? - .start_service()?; - + .start_service() + { + crit!(log, "Failed to start validator client"; "reason" => e); + // Ignore the error since it always occurs during normal operation when + // shutting down. + let _ = executor + .shutdown_sender() + .try_send(ShutdownReason::Failure("Failed to start validator client")); + } Ok::<(), String>(()) - }; - if let Err(e) = run.await { - crit!(log, "Failed to start validator client"; "reason" => e); - // Ignore the error since it always occurs during normal operation when - // shutting down. - let _ = executor - .shutdown_sender() - .try_send("Failed to start validator client"); - } - }); + }); + } else { + let _ = executor.shutdown_sender().try_send(ShutdownReason::Success( + "Validator client immediate shutdown triggered.", + )); + } } ("remote_signer", Some(matches)) => { if let Err(e) = remote_signer::run(&mut environment, matches) { @@ -327,7 +370,7 @@ fn run( .core_context() .executor .shutdown_sender() - .try_send("Failed to start remote signer"); + .try_send(ShutdownReason::Failure("Failed to start remote signer")); } } _ => { @@ -337,12 +380,16 @@ fn run( }; // Block this thread until we get a ctrl-c or a task sends a shutdown signal. - environment.block_until_shutdown_requested()?; - info!(log, "Shutting down.."); + let shutdown_reason = environment.block_until_shutdown_requested()?; + info!(log, "Shutting down.."; "reason" => ?shutdown_reason); environment.fire_signal(); // Shutdown the environment once all tasks have completed. environment.shutdown_on_idle(); - Ok(()) + + match shutdown_reason { + ShutdownReason::Success(_) => Ok(()), + ShutdownReason::Failure(msg) => Err(msg.to_string()), + } } diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index b5b012998..dd4e8443e 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -1,5 +1,3 @@ -#![cfg(not(debug_assertions))] - use account_manager::{ validator::{ create::*, @@ -35,8 +33,8 @@ use validator_dir::ValidatorDir; /// Returns the `lighthouse account` command. fn account_cmd() -> Command { - let target_dir = env!("CARGO_BIN_EXE_lighthouse"); - let path = target_dir + let lighthouse_bin = env!("CARGO_BIN_EXE_lighthouse"); + let path = lighthouse_bin .parse::() .expect("should parse CARGO_TARGET_DIR"); diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs new file mode 100644 index 000000000..ef1edb774 --- /dev/null +++ b/lighthouse/tests/beacon_node.rs @@ -0,0 +1,841 @@ +use beacon_node::ClientConfig as Config; + +use eth2_libp2p::PeerId; +use serde_json::from_reader; +use std::fs::File; +use std::io::Write; +use std::net::{IpAddr, Ipv4Addr}; +use std::net::{TcpListener, UdpSocket}; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::str::{from_utf8, FromStr}; +use std::string::ToString; +use tempfile::TempDir; +use types::{Checkpoint, Epoch, Hash256}; + +const BEACON_CMD: &str = "beacon_node"; +const CONFIG_NAME: &str = "bn_dump.json"; +const DUMP_CONFIG_CMD: &str = "dump-config"; +const IMMEDIATE_SHUTDOWN_CMD: &str = "immediate-shutdown"; +const DEFAULT_ETH1_ENDPOINT: &str = "http://localhost:8545/"; + +/// Returns the `lighthouse beacon_node --immediate-shutdown` command. +fn base_cmd() -> Command { + let lighthouse_bin = env!("CARGO_BIN_EXE_lighthouse"); + let path = lighthouse_bin + .parse::() + .expect("should parse CARGO_TARGET_DIR"); + + let mut cmd = Command::new(path); + cmd.arg(BEACON_CMD) + .arg(format!("--{}", IMMEDIATE_SHUTDOWN_CMD)); + + cmd +} + +/// Executes a `Command`, returning a `Result` based upon the success exit code of the command. +fn output_result(cmd: &mut Command) -> Result { + let output = cmd.output().expect("should run command"); + + if output.status.success() { + Ok(output) + } else { + Err(from_utf8(&output.stderr) + .expect("stderr is not utf8") + .to_string()) + } +} + +// Wrapper around `Command` for easier Command Line Testing. +struct CommandLineTest { + cmd: Command, +} +impl CommandLineTest { + fn new() -> CommandLineTest { + let base_cmd = base_cmd(); + CommandLineTest { cmd: base_cmd } + } + + fn flag(mut self, flag: &str, value: Option<&str>) -> Self { + // Build the command by adding the flag and any values. + self.cmd.arg(format!("--{}", flag)); + if let Some(value) = value { + self.cmd.arg(value); + } + self + } + + fn run(&mut self) -> CompletedTest { + // Setup temp directories. + let tmp_dir = TempDir::new().expect("Unable to create temporary directory"); + let tmp_path: PathBuf = tmp_dir.path().join(CONFIG_NAME); + + // Add --datadir --dump-config -z to cmd. + self.cmd + .arg("--datadir") + .arg(tmp_dir.path().as_os_str()) + .arg(format!("--{}", DUMP_CONFIG_CMD)) + .arg(tmp_path.as_os_str()) + .arg("-z"); + + // Run the command. + let output = output_result(&mut self.cmd); + if let Err(e) = output { + panic!("{:?}", e); + } + + // Grab the config. + let config: Config = + from_reader(File::open(tmp_path).expect("Unable to open dumped config")) + .expect("Unable to deserialize to ClientConfig"); + CompletedTest { + config, + dir: tmp_dir, + } + } + + fn run_with_no_zero_port(&mut self) -> CompletedTest { + // Setup temp directories. + let tmp_dir = TempDir::new().expect("Unable to create temporary directory"); + let tmp_path: PathBuf = tmp_dir.path().join(CONFIG_NAME); + + // Add --datadir --dump-config to cmd. + self.cmd + .arg("--datadir") + .arg(tmp_dir.path().as_os_str()) + .arg(format!("--{}", DUMP_CONFIG_CMD)) + .arg(tmp_path.as_os_str()); + + // Run the command. + let output = output_result(&mut self.cmd); + if let Err(e) = output { + panic!("{:?}", e); + } + + // Grab the config. + let config: Config = + from_reader(File::open(tmp_path).expect("Unable to open dumped config")) + .expect("Unable to deserialize to ClientConfig"); + CompletedTest { + config, + dir: tmp_dir, + } + } +} +struct CompletedTest { + config: Config, + dir: TempDir, +} +impl CompletedTest { + fn with_config(self, func: F) { + func(&self.config); + } + fn with_config_and_dir(self, func: F) { + func(&self.config, &self.dir); + } +} + +#[test] +fn datadir_flag() { + CommandLineTest::new() + .run() + .with_config_and_dir(|config, dir| assert_eq!(config.data_dir, dir.path().join("beacon"))); +} + +#[test] +fn staking_flag() { + CommandLineTest::new() + .flag("staking", None) + .run() + .with_config(|config| { + assert!(config.http_api.enabled); + assert!(config.sync_eth1_chain); + assert_eq!(config.eth1.endpoints[0].to_string(), DEFAULT_ETH1_ENDPOINT); + }); +} + +#[test] +fn wss_checkpoint_flag() { + let state = Some(Checkpoint { + epoch: Epoch::new(1010), + root: Hash256::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .unwrap(), + }); + CommandLineTest::new() + .flag( + "wss-checkpoint", + Some("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef:1010"), + ) + .run() + .with_config(|config| assert_eq!(config.chain.weak_subjectivity_checkpoint, state)); +} +#[test] +fn max_skip_slots_flag() { + CommandLineTest::new() + .flag("max-skip-slots", Some("10")) + .run() + .with_config(|config| assert_eq!(config.chain.import_max_skip_slots, Some(10))); +} + +#[test] +fn freezer_dir_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("freezer-dir", dir.path().as_os_str().to_str()) + .run() + .with_config(|config| assert_eq!(config.freezer_db_path, Some(dir.path().to_path_buf()))); +} + +#[test] +fn graffiti_flag() { + CommandLineTest::new() + .flag("graffiti", Some("nice-graffiti")) + .run() + .with_config(|config| { + assert_eq!( + config.graffiti.to_string(), + "0x6e6963652d677261666669746900000000000000000000000000000000000000" + ); + }); +} + +#[test] +fn trusted_peers_flag() { + let peers = vec![PeerId::random(), PeerId::random()]; + CommandLineTest::new() + .flag( + "trusted-peers", + Some(format!("{},{}", peers[0].to_string(), peers[1].to_string()).as_str()), + ) + .run() + .with_config(|config| { + assert_eq!( + PeerId::from(config.network.trusted_peers[0].clone()).to_bytes(), + peers[0].to_bytes() + ); + assert_eq!( + PeerId::from(config.network.trusted_peers[1].clone()).to_bytes(), + peers[1].to_bytes() + ); + }); +} + +// Tests for Eth1 flags. +#[test] +fn dummy_eth1_flag() { + CommandLineTest::new() + .flag("dummy-eth1", None) + .run() + .with_config(|config| assert!(config.dummy_eth1_backend)); +} +#[test] +fn eth1_flag() { + CommandLineTest::new() + .flag("eth1", None) + .run() + .with_config(|config| assert!(config.sync_eth1_chain)); +} +#[test] +fn eth1_endpoints_flag() { + CommandLineTest::new() + .flag( + "eth1-endpoints", + Some("http://localhost:9545,https://infura.io/secret"), + ) + .run() + .with_config(|config| { + assert_eq!( + config.eth1.endpoints[0].full.to_string(), + "http://localhost:9545/" + ); + assert_eq!( + config.eth1.endpoints[0].to_string(), + "http://localhost:9545/" + ); + assert_eq!( + config.eth1.endpoints[1].full.to_string(), + "https://infura.io/secret" + ); + assert_eq!(config.eth1.endpoints[1].to_string(), "https://infura.io/"); + }); +} +#[test] +fn eth1_blocks_per_log_query_flag() { + CommandLineTest::new() + .flag("eth1-blocks-per-log-query", Some("500")) + .run() + .with_config(|config| assert_eq!(config.eth1.blocks_per_log_query, 500)); +} +#[test] +fn eth1_purge_cache_flag() { + CommandLineTest::new() + .flag("eth1-purge-cache", None) + .run() + .with_config(|config| assert!(config.eth1.purge_cache)); +} + +// Tests for Network flags. +#[test] +fn network_dir_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("network-dir", dir.path().as_os_str().to_str()) + .run() + .with_config(|config| assert_eq!(config.network.network_dir, dir.path())); +} +#[test] +fn network_target_peers_flag() { + CommandLineTest::new() + .flag("target-peers", Some("55")) + .run() + .with_config(|config| { + assert_eq!(config.network.target_peers, "55".parse::().unwrap()); + }); +} +#[test] +fn network_subscribe_all_subnets_flag() { + CommandLineTest::new() + .flag("subscribe-all-subnets", None) + .run() + .with_config(|config| assert!(config.network.subscribe_all_subnets)); +} +#[test] +fn network_import_all_attestations_flag() { + CommandLineTest::new() + .flag("import-all-attestations", None) + .run() + .with_config(|config| assert!(config.network.import_all_attestations)); +} +#[test] +fn network_listen_address_flag() { + let addr = "127.0.0.2".parse::().unwrap(); + CommandLineTest::new() + .flag("listen-address", Some("127.0.0.2")) + .run() + .with_config(|config| assert_eq!(config.network.listen_address, addr)); +} +#[test] +fn network_port_flag() { + let port = unused_port("tcp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("port", Some(port.to_string().as_str())) + .run_with_no_zero_port() + .with_config(|config| { + assert_eq!(config.network.libp2p_port, port); + assert_eq!(config.network.discovery_port, port); + }); +} +#[test] +fn network_port_and_discovery_port_flags() { + let port1 = unused_port("tcp").expect("Unable to find unused port."); + let port2 = unused_port("udp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("port", Some(port1.to_string().as_str())) + .flag("discovery-port", Some(port2.to_string().as_str())) + .run_with_no_zero_port() + .with_config(|config| { + assert_eq!(config.network.libp2p_port, port1); + assert_eq!(config.network.discovery_port, port2); + }); +} +#[test] +fn disable_discovery_flag() { + CommandLineTest::new() + .flag("disable-discovery", None) + .run() + .with_config(|config| assert!(config.network.disable_discovery)); +} +#[test] +fn disable_upnp_flag() { + CommandLineTest::new() + .flag("disable-upnp", None) + .run() + .with_config(|config| assert!(!config.network.upnp_enabled)); +} +#[test] +fn default_boot_nodes() { + let mainnet = vec![ + // Lighthouse Team (Sigma Prime) + "enr:-Jq4QFs9If3eUC8mHx6-BLVw0jRMbyEgXNn6sl7c77bBmji_afJ-0_X7Q4vttQ8SO8CYReudHsGVvgSybh1y96yyL-oChGV0aDKQtTA_KgAAAAD__________4JpZIJ2NIJpcIQ2_YtGiXNlY3AyNTZrMaECSHaY_36GdNjF8-CLfMSg-8lB0wce5VRZ96HkT9tSkVeDdWRwgiMo", + "enr:-Jq4QA4kNIdO1FkIHpl5iqEKjJEjCVfp77aFulytCEPvEQOdbTTf6ucNmWSuXjlwvgka86gkpnCTv-V7CfBn4AMBRvIChGV0aDKQtTA_KgAAAAD__________4JpZIJ2NIJpcIQ22Gh-iXNlY3AyNTZrMaEC0EiXxAB2QKZJuXnUwmf-KqbP9ZP7m9gsRxcYvoK9iTCDdWRwgiMo", + // EF Team + "enr:-Ku4QHqVeJ8PPICcWk1vSn_XcSkjOkNiTg6Fmii5j6vUQgvzMc9L1goFnLKgXqBJspJjIsB91LTOleFmyWWrFVATGngBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAMRHkWJc2VjcDI1NmsxoQKLVXFOhp2uX6jeT0DvvDpPcU8FWMjQdR4wMuORMhpX24N1ZHCCIyg", + "enr:-Ku4QG-2_Md3sZIAUebGYT6g0SMskIml77l6yR-M_JXc-UdNHCmHQeOiMLbylPejyJsdAPsTHJyjJB2sYGDLe0dn8uYBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhBLY-NyJc2VjcDI1NmsxoQORcM6e19T1T9gi7jxEZjk_sjVLGFscUNqAY9obgZaxbIN1ZHCCIyg", + "enr:-Ku4QPn5eVhcoF1opaFEvg1b6JNFD2rqVkHQ8HApOKK61OIcIXD127bKWgAtbwI7pnxx6cDyk_nI88TrZKQaGMZj0q0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDayLMaJc2VjcDI1NmsxoQK2sBOLGcUb4AwuYzFuAVCaNHA-dy24UuEKkeFNgCVCsIN1ZHCCIyg", + "enr:-Ku4QEWzdnVtXc2Q0ZVigfCGggOVB2Vc1ZCPEc6j21NIFLODSJbvNaef1g4PxhPwl_3kax86YPheFUSLXPRs98vvYsoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhDZBrP2Jc2VjcDI1NmsxoQM6jr8Rb1ktLEsVcKAPa08wCsKUmvoQ8khiOl_SLozf9IN1ZHCCIyg", + // Teku team (Consensys) + "enr:-KG4QOtcP9X1FbIMOe17QNMKqDxCpm14jcX5tiOE4_TyMrFqbmhPZHK_ZPG2Gxb1GE2xdtodOfx9-cgvNtxnRyHEmC0ghGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQDE8KdiXNlY3AyNTZrMaEDhpehBDbZjM_L9ek699Y7vhUJ-eAdMyQW_Fil522Y0fODdGNwgiMog3VkcIIjKA", + "enr:-KG4QDyytgmE4f7AnvW-ZaUOIi9i79qX4JwjRAiXBZCU65wOfBu-3Nb5I7b_Rmg3KCOcZM_C3y5pg7EBU5XGrcLTduQEhGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQ2_DUbiXNlY3AyNTZrMaEDKnz_-ps3UUOfHWVYaskI5kWYO_vtYMGYCQRAR3gHDouDdGNwgiMog3VkcIIjKA", + // Prysm team (Prysmatic Labs) + "enr:-Ku4QImhMc1z8yCiNJ1TyUxdcfNucje3BGwEHzodEZUan8PherEo4sF7pPHPSIB1NNuSg5fZy7qFsjmUKs2ea1Whi0EBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQOVphkDqal4QzPMksc5wnpuC3gvSC8AfbFOnZY_On34wIN1ZHCCIyg", + "enr:-Ku4QP2xDnEtUXIjzJ_DhlCRN9SN99RYQPJL92TMlSv7U5C1YnYLjwOQHgZIUXw6c-BvRg2Yc2QsZxxoS_pPRVe0yK8Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMeFF5GrS7UZpAH2Ly84aLK-TyvH-dRo0JM1i8yygH50YN1ZHCCJxA", + "enr:-Ku4QPp9z1W4tAO8Ber_NQierYaOStqhDqQdOPY3bB3jDgkjcbk6YrEnVYIiCBbTxuar3CzS528d2iE7TdJsrL-dEKoBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD1pf1CAAAAAP__________gmlkgnY0gmlwhBLf22SJc2VjcDI1NmsxoQMw5fqqkw2hHC4F5HZZDPsNmPdB1Gi8JPQK7pRc9XHh-oN1ZHCCKvg", + // Nimbus team + "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", + "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM" + ]; + + CommandLineTest::new().run().with_config(|config| { + // Lighthouse Team (Sigma Prime) + assert_eq!(config.network.boot_nodes_enr[0].to_base64(), mainnet[0]); + assert_eq!(config.network.boot_nodes_enr[1].to_base64(), mainnet[1]); + // EF Team + assert_eq!(config.network.boot_nodes_enr[2].to_base64(), mainnet[2]); + assert_eq!(config.network.boot_nodes_enr[3].to_base64(), mainnet[3]); + assert_eq!(config.network.boot_nodes_enr[4].to_base64(), mainnet[4]); + assert_eq!(config.network.boot_nodes_enr[5].to_base64(), mainnet[5]); + // Teku team (Consensys) + assert_eq!(config.network.boot_nodes_enr[6].to_base64(), mainnet[6]); + assert_eq!(config.network.boot_nodes_enr[7].to_base64(), mainnet[7]); + // Prysm team (Prysmatic Labs) + assert_eq!(config.network.boot_nodes_enr[8].to_base64(), mainnet[8]); + assert_eq!(config.network.boot_nodes_enr[9].to_base64(), mainnet[9]); + assert_eq!(config.network.boot_nodes_enr[10].to_base64(), mainnet[10]); + // Nimbus team + assert_eq!(config.network.boot_nodes_enr[11].to_base64(), mainnet[11]); + assert_eq!(config.network.boot_nodes_enr[12].to_base64(), mainnet[12]); + }); +} +#[test] +fn boot_nodes_flag() { + let nodes = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8,\ + enr:-LK4QFOFWca5ABQzxiCRcy37G7wy1K6zD4qMYBSN5ozzanwze_XVvXVhCk9JvF0cHXOBZrHK1E4vU7Gn-a0bHVczoDU6h2F0dG5ldHOIAAAAAAAAAACEZXRoMpA7CIeVAAAgCf__________gmlkgnY0gmlwhNIy-4iJc2VjcDI1NmsxoQJA3AXQJ6M3NpBWtJS3HPtbXG14t7qHjXuIaL6IOz89T4N0Y3CCIyiDdWRwgiMo"; + let enr: Vec<&str> = nodes.split(',').collect(); + CommandLineTest::new() + .flag("boot-nodes", Some(nodes)) + .run() + .with_config(|config| { + assert_eq!(config.network.boot_nodes_enr[0].to_base64(), enr[0]); + assert_eq!(config.network.boot_nodes_enr[1].to_base64(), enr[1]); + }); +} +#[test] +fn boot_nodes_multiaddr_flag() { + let nodes = "/ip4/0.0.0.0/tcp/9000/p2p/16Uiu2HAkynrfLjeoAP7R3WFySad2NfduShkTpx8f8ygpSSfP1yen,\ + /ip4/192.167.55.55/tcp/9000/p2p/16Uiu2HAkynrfLjeoBP7R3WFyDad2NfduVhkWpx8f8ygpSSfP1yen"; + let multiaddr: Vec<&str> = nodes.split(',').collect(); + CommandLineTest::new() + .flag("boot-nodes", Some(nodes)) + .run() + .with_config(|config| { + assert_eq!( + config.network.boot_nodes_multiaddr[0].to_string(), + multiaddr[0] + ); + assert_eq!( + config.network.boot_nodes_multiaddr[1].to_string(), + multiaddr[1] + ); + }); +} +#[test] +fn private_flag() { + CommandLineTest::new() + .flag("private", None) + .run() + .with_config(|config| assert!(config.network.private)); +} +#[test] +fn zero_ports_flag() { + CommandLineTest::new().run().with_config(|config| { + assert_eq!(config.network.enr_address, None); + assert_eq!(config.http_api.listen_port, 0); + assert_eq!(config.http_metrics.listen_port, 0); + }); +} + +// Tests for ENR flags. +#[test] +fn enr_udp_port_flags() { + let port = unused_port("udp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-udp-port", Some(port.to_string().as_str())) + .run() + .with_config(|config| assert_eq!(config.network.enr_udp_port, Some(port))); +} +#[test] +fn enr_tcp_port_flags() { + let port = unused_port("tcp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-tcp-port", Some(port.to_string().as_str())) + .run() + .with_config(|config| assert_eq!(config.network.enr_tcp_port, Some(port))); +} +#[test] +fn enr_match_flag() { + let addr = "127.0.0.2".parse::().unwrap(); + let port1 = unused_port("udp").expect("Unable to find unused port."); + let port2 = unused_port("udp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-match", None) + .flag("listen-address", Some("127.0.0.2")) + .flag("discovery-port", Some(port1.to_string().as_str())) + .flag("port", Some(port2.to_string().as_str())) + .run_with_no_zero_port() + .with_config(|config| { + assert_eq!(config.network.listen_address, addr); + assert_eq!(config.network.enr_address, Some(addr)); + assert_eq!(config.network.discovery_port, port1); + assert_eq!(config.network.enr_udp_port, Some(port1)); + }); +} +#[test] +fn enr_address_flag() { + let addr = "192.167.1.1".parse::().unwrap(); + let port = unused_port("udp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-address", Some("192.167.1.1")) + .flag("enr-udp-port", Some(port.to_string().as_str())) + .run() + .with_config(|config| { + assert_eq!(config.network.enr_address, Some(addr)); + assert_eq!(config.network.enr_udp_port, Some(port)); + }); +} +#[test] +fn enr_address_dns_flag() { + let addr = "127.0.0.1".parse::().unwrap(); + let ipv6addr = "::1".parse::().unwrap(); + let port = unused_port("udp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("enr-address", Some("localhost")) + .flag("enr-udp-port", Some(port.to_string().as_str())) + .run() + .with_config(|config| { + assert!( + config.network.enr_address == Some(addr) + || config.network.enr_address == Some(ipv6addr) + ); + assert_eq!(config.network.enr_udp_port, Some(port)); + }); +} +#[test] +fn disable_enr_auto_update_flag() { + CommandLineTest::new() + .flag("disable-enr-auto-update", None) + .run() + .with_config(|config| assert!(config.network.discv5_config.enr_update)); +} + +// Tests for HTTP flags. +#[test] +fn http_flag() { + CommandLineTest::new() + .flag("http", None) + .run() + .with_config(|config| assert!(config.http_api.enabled)); +} +#[test] +fn http_address_flag() { + let addr = "127.0.0.99".parse::().unwrap(); + CommandLineTest::new() + .flag("http-address", Some("127.0.0.99")) + .run() + .with_config(|config| assert_eq!(config.http_api.listen_addr, addr)); +} +#[test] +fn http_port_flag() { + let port1 = unused_port("tcp").expect("Unable to find unused port."); + let port2 = unused_port("tcp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("http-port", Some(port1.to_string().as_str())) + .flag("port", Some(port2.to_string().as_str())) + .run_with_no_zero_port() + .with_config(|config| assert_eq!(config.http_api.listen_port, port1)); +} +#[test] +fn http_allow_origin_flag() { + CommandLineTest::new() + .flag("http-allow-origin", Some("127.0.0.99")) + .run() + .with_config(|config| { + assert_eq!(config.http_api.allow_origin, Some("127.0.0.99".to_string())); + }); +} +#[test] +fn http_allow_origin_all_flag() { + CommandLineTest::new() + .flag("http-allow-origin", Some("*")) + .run() + .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); +} + +// Tests for Metrics flags. +#[test] +fn metrics_flag() { + CommandLineTest::new() + .flag("metrics", None) + .run() + .with_config(|config| assert!(config.http_metrics.enabled)); +} +#[test] +fn metrics_address_flag() { + let addr = "127.0.0.99".parse::().unwrap(); + CommandLineTest::new() + .flag("metrics", None) + .flag("metrics-address", Some("127.0.0.99")) + .run() + .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); +} +#[test] +fn metrics_port_flag() { + let port1 = unused_port("tcp").expect("Unable to find unused port."); + let port2 = unused_port("tcp").expect("Unable to find unused port."); + CommandLineTest::new() + .flag("metrics", None) + .flag("metrics-port", Some(port1.to_string().as_str())) + .flag("port", Some(port2.to_string().as_str())) + .run_with_no_zero_port() + .with_config(|config| assert_eq!(config.http_metrics.listen_port, port1)); +} +#[test] +fn metrics_allow_origin_flag() { + CommandLineTest::new() + .flag("metrics", None) + .flag("metrics-allow-origin", Some("http://localhost:5059")) + .run() + .with_config(|config| { + assert_eq!( + config.http_metrics.allow_origin, + Some("http://localhost:5059".to_string()) + ) + }); +} +#[test] +fn metrics_allow_origin_all_flag() { + CommandLineTest::new() + .flag("metrics", None) + .flag("metrics-allow-origin", Some("*")) + .run() + .with_config(|config| assert_eq!(config.http_metrics.allow_origin, Some("*".to_string()))); +} + +// Tests for Validator Monitor flags. +#[test] +fn validator_monitor_auto_flag() { + CommandLineTest::new() + .flag("validator-monitor-auto", None) + .run() + .with_config(|config| assert!(config.validator_monitor_auto)); +} +#[test] +fn validator_monitor_pubkeys_flag() { + CommandLineTest::new() + .flag("validator-monitor-pubkeys", Some("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef,\ + 0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) + .run() + .with_config(|config| { + assert_eq!(config.validator_monitor_pubkeys[0].to_string(), "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + assert_eq!(config.validator_monitor_pubkeys[1].to_string(), "0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + }); +} +#[test] +fn validator_monitor_file_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + let mut file = File::create(dir.path().join("pubkeys.txt")).expect("Unable to create file"); + file.write_all(b"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef,\ + 0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .expect("Unable to write to file"); + CommandLineTest::new() + .flag("validator-monitor-file", dir.path().join("pubkeys.txt").as_os_str().to_str()) + .run() + .with_config(|config| { + assert_eq!(config.validator_monitor_pubkeys[0].to_string(), "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + assert_eq!(config.validator_monitor_pubkeys[1].to_string(), "0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + }); +} + +// Tests for Store flags. +#[test] +fn slots_per_restore_point_flag() { + CommandLineTest::new() + .flag("slots-per-restore-point", Some("64")) + .run() + .with_config(|config| assert_eq!(config.store.slots_per_restore_point, 64)); +} +#[test] +fn block_cache_size_flag() { + CommandLineTest::new() + .flag("block-cache-size", Some("4")) + .run() + .with_config(|config| assert_eq!(config.store.block_cache_size, 4_usize)); +} +#[test] +fn auto_compact_db_flag() { + CommandLineTest::new() + .flag("auto-compact-db", Some("false")) + .run() + .with_config(|config| assert!(!config.store.compact_on_prune)); +} +#[test] +fn compact_db_flag() { + CommandLineTest::new() + .flag("auto-compact-db", Some("false")) + .flag("compact-db", None) + .run() + .with_config(|config| assert!(config.store.compact_on_init)); +} + +// Tests for Slasher flags. +#[test] +fn slasher_flag() { + CommandLineTest::new() + .flag("slasher", None) + .run() + .with_config_and_dir(|config, dir| { + if let Some(slasher_config) = &config.slasher { + assert_eq!( + slasher_config.database_path, + dir.path().join("beacon").join("slasher_db") + ) + } else { + panic!("Slasher config was parsed incorrectly"); + } + }); +} +#[test] +fn slasher_dir_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-dir", dir.path().as_os_str().to_str()) + .run() + .with_config(|config| { + if let Some(slasher_config) = &config.slasher { + assert_eq!(slasher_config.database_path, dir.path()); + } else { + panic!("Slasher config was parsed incorrectly"); + } + }); +} +#[test] +fn slasher_update_period_flag() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-update-period", Some("100")) + .run() + .with_config(|config| { + if let Some(slasher_config) = &config.slasher { + assert_eq!(slasher_config.update_period, 100); + } else { + panic!("Slasher config was parsed incorrectly"); + } + }); +} +#[test] +fn slasher_history_length_flag() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-history-length", Some("2048")) + .run() + .with_config(|config| { + if let Some(slasher_config) = &config.slasher { + assert_eq!(slasher_config.history_length, 2048); + } else { + panic!("Slasher config was parsed incorrectly"); + } + }); +} +#[test] +fn slasher_max_db_size_flag() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-max-db-size", Some("10")) + .run() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert_eq!(slasher_config.max_db_size_mbs, 10240); + }); +} +#[test] +fn slasher_chunk_size_flag() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-chunk-size", Some("32")) + .run() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert_eq!(slasher_config.chunk_size, 32); + }); +} +#[test] +fn slasher_validator_chunk_size_flag() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-validator-chunk-size", Some("512")) + .run() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert_eq!(slasher_config.validator_chunk_size, 512); + }); +} +#[test] +fn slasher_broadcast_flag() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-broadcast", None) + .run() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert!(slasher_config.broadcast); + }); +} +#[test] +#[should_panic] +fn ensure_panic_on_failed_launch() { + CommandLineTest::new() + .flag("slasher", None) + .flag("slasher-chunk-size", Some("10")) + .run() + .with_config(|config| { + let slasher_config = config + .slasher + .as_ref() + .expect("Unable to parse Slasher config"); + assert_eq!(slasher_config.chunk_size, 10); + }); +} + +/// A bit of hack to find an unused port. +/// +/// Does not guarantee that the given port is unused after the function exits, just that it was +/// unused before the function started (i.e., it does not reserve a port). +pub fn unused_port(transport: &str) -> Result { + let local_addr = match transport { + "tcp" => { + let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| { + format!("Failed to create TCP listener to find unused port: {:?}", e) + })?; + listener.local_addr().map_err(|e| { + format!( + "Failed to read TCP listener local_addr to find unused port: {:?}", + e + ) + })? + } + "udp" => { + let socket = UdpSocket::bind("127.0.0.1:0") + .map_err(|e| format!("Failed to create UDP socket to find unused port: {:?}", e))?; + socket.local_addr().map_err(|e| { + format!( + "Failed to read UDP socket local_addr to find unused port: {:?}", + e + ) + })? + } + _ => return Err("Invalid transport to find unused port".into()), + }; + Ok(local_addr.port()) +} diff --git a/lighthouse/tests/main.rs b/lighthouse/tests/main.rs new file mode 100644 index 000000000..c800e855d --- /dev/null +++ b/lighthouse/tests/main.rs @@ -0,0 +1,5 @@ +#![cfg(not(debug_assertions))] + +mod account_manager; +mod beacon_node; +mod validator_client; diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs new file mode 100644 index 000000000..435c45dd1 --- /dev/null +++ b/lighthouse/tests/validator_client.rs @@ -0,0 +1,346 @@ +use validator_client::Config; + +use bls::{Keypair, PublicKeyBytes}; +use serde_json::from_reader; +use std::fs::File; +use std::io::Write; +use std::net::Ipv4Addr; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::str::from_utf8; +use std::string::ToString; +use tempfile::TempDir; + +const VALIDATOR_CMD: &str = "validator_client"; +const CONFIG_NAME: &str = "vc_dump.json"; +const DUMP_CONFIG_CMD: &str = "dump-config"; +const IMMEDIATE_SHUTDOWN_CMD: &str = "immediate-shutdown"; + +/// Returns the `lighthouse validator_client --immediate-shutdown` command. +fn base_cmd() -> Command { + let lighthouse_bin = env!("CARGO_BIN_EXE_lighthouse"); + let path = lighthouse_bin + .parse::() + .expect("should parse CARGO_TARGET_DIR"); + + let mut cmd = Command::new(path); + cmd.arg(VALIDATOR_CMD) + .arg(format!("--{}", IMMEDIATE_SHUTDOWN_CMD)); + + cmd +} + +/// Executes a `Command`, returning a `Result` based upon the success exit code of the command. +fn output_result(cmd: &mut Command) -> Result { + let output = cmd.output().expect("should run command"); + + if output.status.success() { + Ok(output) + } else { + Err(from_utf8(&output.stderr) + .expect("stderr is not utf8") + .to_string()) + } +} + +// Wrapper around `Command` for easier Command Line Testing. +struct CommandLineTest { + cmd: Command, +} +impl CommandLineTest { + fn new() -> CommandLineTest { + let base_cmd = base_cmd(); + CommandLineTest { cmd: base_cmd } + } + + fn flag(mut self, flag: &str, value: Option<&str>) -> Self { + // Build the command by adding the flag and any values. + self.cmd.arg(format!("--{}", flag)); + if let Some(value) = value { + self.cmd.arg(value); + } + self + } + + fn run(&mut self) -> CompletedTest { + // Setup temp directories. + let tmp_dir = TempDir::new().expect("Unable to create temporary directory"); + let tmp_path: PathBuf = tmp_dir.path().join(CONFIG_NAME); + + // Add --datadir --dump-config to cmd. + self.cmd + .arg("--datadir") + .arg(tmp_dir.path().as_os_str()) + .arg(format!("--{}", DUMP_CONFIG_CMD)) + .arg(tmp_path.as_os_str()); + + // Run the command. + let _output = output_result(&mut self.cmd).expect("Unable to run command"); + + // Grab the config. + let config: Config = + from_reader(File::open(tmp_path).expect("Unable to open dumped config")) + .expect("Unable to deserialize to ClientConfig"); + CompletedTest { + config, + dir: tmp_dir, + } + } + + // In order to test custom validator and secrets directory flags, + // datadir cannot be defined. + fn run_with_no_datadir(&mut self) -> CompletedTest { + // Setup temp directories + let tmp_dir = TempDir::new().expect("Unable to create temporary directory"); + let tmp_path: PathBuf = tmp_dir.path().join(CONFIG_NAME); + + // Add --dump-config to cmd. + self.cmd + .arg(format!("--{}", DUMP_CONFIG_CMD)) + .arg(tmp_path.as_os_str()); + + // Run the command. + let _output = output_result(&mut self.cmd).expect("Unable to run command"); + + // Grab the config. + let config: Config = + from_reader(File::open(tmp_path).expect("Unable to open dumped config")) + .expect("Unable to deserialize to ClientConfig"); + CompletedTest { + config, + dir: tmp_dir, + } + } +} +struct CompletedTest { + config: Config, + dir: TempDir, +} +impl CompletedTest { + fn with_config(self, func: F) { + func(&self.config); + } + fn with_config_and_dir(self, func: F) { + func(&self.config, &self.dir); + } +} + +#[test] +fn datadir_flag() { + CommandLineTest::new() + .run() + .with_config_and_dir(|config, dir| { + assert_eq!(config.validator_dir, dir.path().join("validators")); + assert_eq!(config.secrets_dir, dir.path().join("secrets")); + }); +} + +#[test] +fn validators_and_secrets_dir_flags() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + CommandLineTest::new() + .flag("validators-dir", dir.path().join("validators").to_str()) + .flag("secrets-dir", dir.path().join("secrets").to_str()) + .run_with_no_datadir() + .with_config(|config| { + assert_eq!(config.validator_dir, dir.path().join("validators")); + assert_eq!(config.secrets_dir, dir.path().join("secrets")); + }); +} + +#[test] +fn beacon_nodes_flag() { + CommandLineTest::new() + .flag( + "beacon-nodes", + Some("http://localhost:1001,https://project:secret@infura.io/"), + ) + .run() + .with_config(|config| { + assert_eq!( + config.beacon_nodes[0].full.to_string(), + "http://localhost:1001/" + ); + assert_eq!(config.beacon_nodes[0].to_string(), "http://localhost:1001/"); + assert_eq!( + config.beacon_nodes[1].full.to_string(), + "https://project:secret@infura.io/" + ); + assert_eq!(config.beacon_nodes[1].to_string(), "https://infura.io/"); + }); +} + +#[test] +fn allow_unsynced_flag() { + CommandLineTest::new() + .flag("allow-unsynced", None) + .run() + .with_config(|config| assert!(config.allow_unsynced_beacon_node)); +} + +#[test] +fn disable_auto_discover_flag() { + CommandLineTest::new() + .flag("disable-auto-discover", None) + .run() + .with_config(|config| assert!(config.disable_auto_discover)); +} + +#[test] +fn init_slashing_protections_flag() { + CommandLineTest::new() + .flag("init-slashing-protection", None) + .run() + .with_config(|config| assert!(config.init_slashing_protection)); +} + +// Tests for Graffiti flags. +#[test] +fn graffiti_flag() { + CommandLineTest::new() + .flag("graffiti", Some("nice-graffiti")) + .run() + .with_config(|config| { + assert_eq!( + config.graffiti.unwrap().to_string(), + "0x6e6963652d677261666669746900000000000000000000000000000000000000" + ) + }); +} +#[test] +fn graffiti_file_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + let mut file = File::create(dir.path().join("graffiti.txt")).expect("Unable to create file"); + let new_key = Keypair::random(); + let pubkeybytes = PublicKeyBytes::from(new_key.pk); + let contents = "default:nice-graffiti"; + file.write_all(contents.as_bytes()) + .expect("Unable to write to file"); + CommandLineTest::new() + .flag( + "graffiti-file", + dir.path().join("graffiti.txt").as_os_str().to_str(), + ) + .run() + .with_config(|config| { + // Public key not present so load default. + assert_eq!( + config + .graffiti_file + .clone() + .unwrap() + .load_graffiti(&pubkeybytes) + .unwrap() + .unwrap() + .to_string(), + "0x6e6963652d677261666669746900000000000000000000000000000000000000" + ) + }); +} +#[test] +fn graffiti_file_with_pk_flag() { + let dir = TempDir::new().expect("Unable to create temporary directory"); + let mut file = File::create(dir.path().join("graffiti.txt")).expect("Unable to create file"); + let new_key = Keypair::random(); + let pubkeybytes = PublicKeyBytes::from(new_key.pk); + let contents = format!("{}:nice-graffiti", pubkeybytes.to_string()); + file.write_all(contents.as_bytes()) + .expect("Unable to write to file"); + CommandLineTest::new() + .flag( + "graffiti-file", + dir.path().join("graffiti.txt").as_os_str().to_str(), + ) + .run() + .with_config(|config| { + assert_eq!( + config + .graffiti_file + .clone() + .unwrap() + .load_graffiti(&pubkeybytes) + .unwrap() + .unwrap() + .to_string(), + "0x6e6963652d677261666669746900000000000000000000000000000000000000" + ) + }); +} + +// Tests for HTTP flags. +#[test] +fn http_flag() { + CommandLineTest::new() + .flag("http", None) + .run() + .with_config(|config| assert!(config.http_api.enabled)); +} +#[test] +fn http_port_flag() { + CommandLineTest::new() + .flag("http-port", Some("9090")) + .run() + .with_config(|config| assert_eq!(config.http_api.listen_port, 9090)); +} +#[test] +fn http_allow_origin_flag() { + CommandLineTest::new() + .flag("http-allow-origin", Some("http://localhost:9009")) + .run() + .with_config(|config| { + assert_eq!( + config.http_api.allow_origin, + Some("http://localhost:9009".to_string()) + ); + }); +} +#[test] +fn http_allow_origin_all_flag() { + CommandLineTest::new() + .flag("http-allow-origin", Some("*")) + .run() + .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); +} + +// Tests for Metrics flags. +#[test] +fn metrics_flag() { + CommandLineTest::new() + .flag("metrics", None) + .run() + .with_config(|config| assert!(config.http_metrics.enabled)); +} +#[test] +fn metrics_address_flag() { + let addr = "127.0.0.99".parse::().unwrap(); + CommandLineTest::new() + .flag("metrics-address", Some("127.0.0.99")) + .run() + .with_config(|config| assert_eq!(config.http_metrics.listen_addr, addr)); +} +#[test] +fn metrics_port_flag() { + CommandLineTest::new() + .flag("metrics-port", Some("9090")) + .run() + .with_config(|config| assert_eq!(config.http_metrics.listen_port, 9090)); +} +#[test] +fn metrics_allow_origin_flag() { + CommandLineTest::new() + .flag("metrics-allow-origin", Some("http://localhost:9009")) + .run() + .with_config(|config| { + assert_eq!( + config.http_metrics.allow_origin, + Some("http://localhost:9009".to_string()) + ); + }); +} +#[test] +fn metrics_allow_origin_all_flag() { + CommandLineTest::new() + .flag("metrics-allow-origin", Some("*")) + .run() + .with_config(|config| assert_eq!(config.http_metrics.allow_origin, Some("*".to_string()))); +}