diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 6dcaa42e88..43ce4923d9 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -8,9 +8,6 @@ edition = "2018" name = "validator_client" path = "src/lib.rs" -[dev-dependencies] -tempdir = "0.3" - [dependencies] eth2_ssz = "0.1.2" eth2_config = { path = "../eth2/utils/eth2_config" } @@ -43,3 +40,5 @@ hex = "0.4" deposit_contract = { path = "../eth2/utils/deposit_contract" } bls = { path = "../eth2/utils/bls" } remote_beacon_node = { path = "../eth2/utils/remote_beacon_node" } +tempdir = "0.3" +rayon = "1.2.0" diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 1eb71e68d2..948b0734e3 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -93,10 +93,10 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("VALIDATOR_INDEX") .required(true) .help("The first validator public key to be generated for this client.")) - .arg(Arg::with_name("validator_count") - .value_name("COUNT") + .arg(Arg::with_name("last_validator") + .value_name("VALIDATOR_INDEX") .required(true) - .help("The number of validators.")) + .help("The end of the range of keys to generate. This index is not generated.")) ) .subcommand(SubCommand::with_name("interop-yaml") .about("Loads plain-text secret keys from YAML files. Expects the interop format defined diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 336585788f..ede389eba9 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,16 +1,8 @@ -use crate::validator_directory::ValidatorDirectory; -use bincode; use clap::ArgMatches; use serde_derive::{Deserialize, Serialize}; -use slog::{error, warn}; -use std::fs::{self, File}; -use std::io::{Error, ErrorKind}; use std::ops::Range; use std::path::PathBuf; -use types::{ - test_utils::{generate_deterministic_keypair, load_keypairs_from_yaml}, - EthSpec, Keypair, MainnetEthSpec, -}; +use types::{EthSpec, MainnetEthSpec}; #[derive(Clone)] pub enum KeySource { @@ -18,8 +10,6 @@ pub enum KeySource { Disk, /// Generate the keypairs (insecure, generates predictable keys). TestingKeypairRange(Range), - /// Load testing keypairs from YAML - YamlKeypairs(PathBuf), } impl Default for KeySource { @@ -48,8 +38,6 @@ pub struct Config { pub slots_per_epoch: u64, } -const DEFAULT_PRIVATE_KEY_FILENAME: &str = "private.key"; - impl Default for Config { /// Build a new configuration from defaults. fn default() -> Self { @@ -66,137 +54,74 @@ impl Default for Config { } impl Config { - /// Returns the full path for the client data directory (not just the name of the directory). - pub fn full_data_dir(&self) -> Option { - dirs::home_dir().map(|path| path.join(&self.data_dir)) - } + /// Parses the CLI arguments and attempts to load the client configuration. + pub fn from_cli(cli_args: &ArgMatches) -> Result { + let mut client_config = Config::default(); - /// Creates the data directory (and any non-existing parent directories). - pub fn create_data_dir(&self) -> Option { - let path = dirs::home_dir()?.join(&self.data_dir); - fs::create_dir_all(&path).ok()?; - Some(path) - } - - /// Apply the following arguments to `self`, replacing values if they are specified in `args`. - /// - /// Returns an error if arguments are obviously invalid. May succeed even if some values are - /// invalid. - pub fn apply_cli_args( - &mut self, - args: &ArgMatches, - _log: &slog::Logger, - ) -> Result<(), &'static str> { - if let Some(datadir) = args.value_of("datadir") { - self.data_dir = PathBuf::from(datadir); + if let Some(datadir) = cli_args.value_of("datadir") { + client_config.data_dir = PathBuf::from(datadir); }; - if let Some(srv) = args.value_of("server") { - self.server = srv.to_string(); - }; - - Ok(()) - } - - /// Loads the validator keys from disk. - /// - /// ## Errors - /// - /// Returns an error if the base directory does not exist, however it does not return for any - /// invalid directories/files. Instead, it just filters out failures and logs errors. This - /// behaviour is intended to avoid the scenario where a single invalid file can stop all - /// validators. - pub fn fetch_keys_from_disk(&self, log: &slog::Logger) -> Result, String> { - let base_dir = self - .full_data_dir() - .ok_or_else(|| format!("Base directory does not exist: {:?}", self.full_data_dir()))?; - - let keypairs = fs::read_dir(&base_dir) - .map_err(|e| format!("Failed to read base directory: {:?}", e))? - .filter_map(|validator_dir| { - let path = validator_dir.ok()?.path(); - - if path.is_dir() { - match ValidatorDirectory::load_for_signing(path.clone()) { - Ok(validator_directory) => validator_directory.voting_keypair, - Err(e) => { - error!( - log, - "Failed to load a validator directory"; - "error" => e, - "path" => path.to_str(), - ); - None - } - } - } else { - None - } - }) - .collect(); - - Ok(keypairs) - } - - pub fn fetch_testing_keypairs( - &self, - range: std::ops::Range, - ) -> Result, String> { - Ok(range.map(generate_deterministic_keypair).collect()) - } - - /// Loads the keypairs according to `self.key_source`. Will return one or more keypairs, or an - /// error. - #[allow(dead_code)] - pub fn fetch_keys(&self, log: &slog::Logger) -> Result, String> { - let keypairs = match &self.key_source { - KeySource::Disk => self.fetch_keys_from_disk(log)?, - KeySource::TestingKeypairRange(range) => { - warn!( - log, - "Using insecure interop private keys"; - "range" => format!("{:?}", range) - ); - self.fetch_testing_keypairs(range.clone())? - } - KeySource::YamlKeypairs(path) => { - warn!( - log, - "Private keys are stored insecurely (plain text). Testing use only." - ); - - load_keypairs_from_yaml(path.to_path_buf())? - } - }; - - // Check if it's an empty vector, and return none. - if keypairs.is_empty() { - Err( - "No validator keypairs were found, unable to proceed. To generate \ - testing keypairs, see 'testnet range --help'." - .into(), - ) - } else { - Ok(keypairs) + if let Some(server) = cli_args.value_of("server") { + client_config.server = server.to_string(); } - } - /// Saves a keypair to a file inside the appropriate validator directory. Returns the saved path filename. - #[allow(dead_code)] - pub fn save_key(&self, key: &Keypair) -> Result { - use std::os::unix::fs::PermissionsExt; - let validator_config_path = self.data_dir.join(key.identifier()); - let key_path = validator_config_path.join(DEFAULT_PRIVATE_KEY_FILENAME); + if let Some(port) = cli_args.value_of("server-http-port") { + client_config.server_http_port = port + .parse::() + .map_err(|e| format!("Unable to parse HTTP port: {:?}", e))?; + } - fs::create_dir_all(&validator_config_path)?; + if let Some(port) = cli_args.value_of("server-grpc-port") { + client_config.server_grpc_port = port + .parse::() + .map_err(|e| format!("Unable to parse gRPC port: {:?}", e))?; + } - let mut key_file = File::create(&key_path)?; - let mut perm = key_file.metadata()?.permissions(); - perm.set_mode((libc::S_IWUSR | libc::S_IRUSR) as u32); - key_file.set_permissions(perm)?; + let client_config = match cli_args.subcommand() { + ("testnet", Some(sub_cli_args)) => { + if cli_args.is_present("eth2-config") && sub_cli_args.is_present("bootstrap") { + return Err( + "Cannot specify --eth2-config and --bootstrap as it may result \ + in ambiguity." + .into(), + ); + } + process_testnet_subcommand(sub_cli_args, client_config) + } + _ => return Err("You must use the testnet command. See '--help'.".into()), + }?; - bincode::serialize_into(&mut key_file, &key) - .map_err(|e| Error::new(ErrorKind::InvalidData, e))?; - Ok(key_path) + Ok(client_config) } } + +/// Parses the `testnet` CLI subcommand. +fn process_testnet_subcommand( + cli_args: &ArgMatches, + mut client_config: Config, +) -> Result { + client_config.key_source = match cli_args.subcommand() { + ("insecure", Some(sub_cli_args)) => { + let first = sub_cli_args + .value_of("first_validator") + .ok_or_else(|| "No first validator supplied")? + .parse::() + .map_err(|e| format!("Unable to parse first validator: {:?}", e))?; + let last = sub_cli_args + .value_of("last_validator") + .ok_or_else(|| "No last validator supplied")? + .parse::() + .map_err(|e| format!("Unable to parse last validator: {:?}", e))?; + + if last < first { + return Err("Cannot supply a last validator less than the first".to_string()); + } + + KeySource::TestingKeypairRange(first..last) + } + _ => KeySource::Disk, + }; + + Ok(client_config) +} diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 88b1afc3d7..035197cb08 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -17,22 +17,25 @@ use clap::ArgMatches; use config::{Config as ClientConfig, KeySource}; use duties_service::{DutiesService, DutiesServiceBuilder}; use environment::RuntimeContext; -use eth2_config::Eth2Config; use exit_future::Signal; use fork_service::{ForkService, ForkServiceBuilder}; -use futures::{Future, IntoFuture}; -use lighthouse_bootstrap::Bootstrapper; +use futures::{ + future::{self, loop_fn, Loop}, + Future, IntoFuture, +}; use parking_lot::RwLock; use remote_beacon_node::RemoteBeaconNode; -use slog::{info, Logger}; +use slog::{error, info, Logger}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; -use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; +use tokio::timer::Delay; use types::EthSpec; use validator_store::ValidatorStore; +const RETRY_DELAY: Duration = Duration::from_secs(2); + #[derive(Clone)] pub struct ProductionValidatorClient { context: RuntimeContext, @@ -47,22 +50,13 @@ impl ProductionValidatorClient { /// Instantiates the validator client, _without_ starting the timers to trigger block /// and attestation production. pub fn new_from_cli( - mut context: RuntimeContext, - matches: &ArgMatches, + context: RuntimeContext, + cli_args: &ArgMatches, ) -> impl Future { - let mut log = context.log.clone(); - - get_configs(&matches, &mut log) + ClientConfig::from_cli(&cli_args) .into_future() .map_err(|e| format!("Unable to initialize config: {}", e)) - .and_then(|(client_config, eth2_config)| { - // TODO: the eth2 config in the env is being completely ignored. - // - // See https://github.com/sigp/lighthouse/issues/602 - context.eth2_config = eth2_config; - - Self::new(context, client_config) - }) + .and_then(|client_config| Self::new(context, client_config)) } /// Instantiates the validator client, _without_ starting the timers to trigger block @@ -71,12 +65,14 @@ impl ProductionValidatorClient { mut context: RuntimeContext, client_config: ClientConfig, ) -> impl Future { - let log = context.log.clone(); + let log_1 = context.log.clone(); + let log_2 = context.log.clone(); + let log_3 = context.log.clone(); info!( - log, + log_1, "Starting validator client"; - "datadir" => client_config.full_data_dir().expect("Unable to find datadir").to_str(), + "datadir" => format!("{:?}", client_config.data_dir), ); format!( @@ -86,12 +82,18 @@ impl ProductionValidatorClient { .parse() .map_err(|e| format!("Unable to parse server address: {:?}", e)) .into_future() - .and_then(|http_server_addr| { + .and_then(move |http_server_addr| { + info!( + log_1, + "Beacon node connection info"; + "http_server" => format!("{}", http_server_addr), + ); + RemoteBeaconNode::new(http_server_addr) .map_err(|e| format!("Unable to init beacon node http client: {}", e)) }) + .and_then(move |beacon_node| wait_for_node(beacon_node, log_2)) .and_then(|beacon_node| { - // TODO: add loop function to retry if node not online. beacon_node .http .spec() @@ -131,17 +133,29 @@ impl ProductionValidatorClient { Duration::from_millis(context.eth2_config.spec.milliseconds_per_slot), ); - dbg!(context.eth2_config.spec.milliseconds_per_slot); - - // TODO: fix expect. - let validator_store = ValidatorStore::load_from_disk( - client_config.full_data_dir().expect("Get rid of this."), - context.eth2_config.spec.clone(), - log.clone(), - )?; + let validator_store: ValidatorStore = match &client_config.key_source { + // Load pre-existing validators from the data dir. + // + // Use the `account_manager` to generate these files. + KeySource::Disk => ValidatorStore::load_from_disk( + client_config.data_dir.clone(), + context.eth2_config.spec.clone(), + log_3.clone(), + )?, + // Generate ephemeral insecure keypairs for testing purposes. + // + // Do not use in production. + KeySource::TestingKeypairRange(range) => { + ValidatorStore::insecure_ephemeral_validators( + range.clone(), + context.eth2_config.spec.clone(), + log_3.clone(), + )? + } + }; info!( - log, + log_3, "Loaded validator keypair store"; "voting_validators" => validator_store.num_voting_validators() ); @@ -221,134 +235,50 @@ impl ProductionValidatorClient { } } -/// Parses the CLI arguments and attempts to load the client and eth2 configuration. -/// -/// This is not a pure function, it reads from disk and may contact network servers. -fn get_configs( - cli_args: &ArgMatches, - mut log: &mut Logger, -) -> Result<(ClientConfig, Eth2Config), String> { - let mut client_config = ClientConfig::default(); +/// Request the version from the node, looping back and trying again on failure. Exit once the node +/// has been contacted. +fn wait_for_node( + beacon_node: RemoteBeaconNode, + log: Logger, +) -> impl Future, Error = String> { + // Try to get the version string from the node, looping until success is returned. + loop_fn(beacon_node.clone(), move |beacon_node| { + let log = log.clone(); + beacon_node + .clone() + .http + .node() + .get_version() + .map_err(|e| format!("{:?}", e)) + .then(move |result| { + let future: Box, Error = String> + Send> = match result + { + Ok(version) => { + info!( + log, + "Connected to beacon node"; + "version" => version, + ); - client_config.apply_cli_args(&cli_args, &mut log)?; + Box::new(future::ok(Loop::Break(beacon_node))) + } + Err(e) => { + error!( + log, + "Unable to connect to beacon node"; + "error" => format!("{:?}", e), + ); - if let Some(server) = cli_args.value_of("server") { - client_config.server = server.to_string(); - } + Box::new( + Delay::new(Instant::now() + RETRY_DELAY) + .map_err(|e| format!("Failed to trigger delay: {:?}", e)) + .and_then(|_| future::ok(Loop::Continue(beacon_node))), + ) + } + }; - if let Some(port) = cli_args.value_of("server-http-port") { - client_config.server_http_port = port - .parse::() - .map_err(|e| format!("Unable to parse HTTP port: {:?}", e))?; - } - - if let Some(port) = cli_args.value_of("server-grpc-port") { - client_config.server_grpc_port = port - .parse::() - .map_err(|e| format!("Unable to parse gRPC port: {:?}", e))?; - } - - info!( - *log, - "Beacon node connection info"; - "grpc_port" => client_config.server_grpc_port, - "http_port" => client_config.server_http_port, - "server" => &client_config.server, - ); - - let (client_config, eth2_config) = match cli_args.subcommand() { - ("testnet", Some(sub_cli_args)) => { - if cli_args.is_present("eth2-config") && sub_cli_args.is_present("bootstrap") { - return Err( - "Cannot specify --eth2-config and --bootstrap as it may result \ - in ambiguity." - .into(), - ); - } - process_testnet_subcommand(sub_cli_args, client_config, log) - } - _ => return Err("You must use the testnet command. See '--help'.".into()), - }?; - - Ok((client_config, eth2_config)) -} - -/// Parses the `testnet` CLI subcommand. -/// -/// This is not a pure function, it reads from disk and may contact network servers. -fn process_testnet_subcommand( - cli_args: &ArgMatches, - mut client_config: ClientConfig, - log: &Logger, -) -> Result<(ClientConfig, Eth2Config), String> { - let eth2_config = if cli_args.is_present("bootstrap") { - info!(log, "Connecting to bootstrap server"); - let bootstrapper = Bootstrapper::connect( - format!( - "http://{}:{}", - client_config.server, client_config.server_http_port - ), - &log, - )?; - - let eth2_config = bootstrapper.eth2_config()?; - - info!( - log, - "Bootstrapped eth2 config via HTTP"; - "slot_time_millis" => eth2_config.spec.milliseconds_per_slot, - "spec" => ð2_config.spec_constants, - ); - - eth2_config - } else { - match cli_args.value_of("spec") { - Some("mainnet") => Eth2Config::mainnet(), - Some("minimal") => Eth2Config::minimal(), - Some("interop") => Eth2Config::interop(), - _ => return Err("No --spec flag provided. See '--help'.".into()), - } - }; - - client_config.key_source = match cli_args.subcommand() { - ("insecure", Some(sub_cli_args)) => { - let first = sub_cli_args - .value_of("first_validator") - .ok_or_else(|| "No first validator supplied")? - .parse::() - .map_err(|e| format!("Unable to parse first validator: {:?}", e))?; - let count = sub_cli_args - .value_of("validator_count") - .ok_or_else(|| "No validator count supplied")? - .parse::() - .map_err(|e| format!("Unable to parse validator count: {:?}", e))?; - - info!( - log, - "Generating unsafe testing keys"; - "first_validator" => first, - "count" => count - ); - - KeySource::TestingKeypairRange(first..first + count) - } - ("interop-yaml", Some(sub_cli_args)) => { - let path = sub_cli_args - .value_of("path") - .ok_or_else(|| "No yaml path supplied")? - .parse::() - .map_err(|e| format!("Unable to parse yaml path: {:?}", e))?; - - info!( - log, - "Loading keypairs from interop YAML format"; - "path" => format!("{:?}", path), - ); - - KeySource::YamlKeypairs(path) - } - _ => KeySource::Disk, - }; - - Ok((client_config, eth2_config)) + future + }) + }) + .map(|_| beacon_node) } diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index ca647a7c45..2e089b2f71 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -1,12 +1,15 @@ -use crate::validator_directory::ValidatorDirectory; +use crate::validator_directory::{ValidatorDirectory, ValidatorDirectoryBuilder}; use parking_lot::RwLock; +use rayon::prelude::*; use slog::{error, Logger}; use std::collections::HashMap; use std::fs::read_dir; use std::iter::FromIterator; use std::marker::PhantomData; +use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; +use tempdir::TempDir; use tree_hash::{SignedRoot, TreeHash}; use types::{ Attestation, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork, PublicKey, Signature, @@ -17,6 +20,7 @@ pub struct ValidatorStore { validators: Arc>>, spec: Arc, log: Logger, + temp_dir: Option>, _phantom: PhantomData, } @@ -55,6 +59,47 @@ impl ValidatorStore { validators: Arc::new(RwLock::new(HashMap::from_iter(validator_iter))), spec: Arc::new(spec), log, + temp_dir: None, + _phantom: PhantomData, + }) + } + + pub fn insecure_ephemeral_validators( + range: Range, + spec: ChainSpec, + log: Logger, + ) -> Result { + let temp_dir = TempDir::new("insecure_validator") + .map_err(|e| format!("Unable to create temp dir: {:?}", e))?; + let data_dir = PathBuf::from(temp_dir.path()); + + let validators = range + .collect::>() + .par_iter() + .map(|index| { + ValidatorDirectoryBuilder::default() + .spec(spec.clone()) + .full_deposit_amount()? + .insecure_keypairs(*index) + .create_directory(data_dir.clone())? + .write_keypair_files()? + .write_eth1_data_file()? + .build() + }) + .collect::, _>>()? + .into_iter() + .filter_map(|validator_directory| { + validator_directory + .voting_keypair + .clone() + .map(|voting_keypair| (voting_keypair.pk, validator_directory)) + }); + + Ok(Self { + validators: Arc::new(RwLock::new(HashMap::from_iter(validators))), + spec: Arc::new(spec), + log, + temp_dir: Some(Arc::new(temp_dir)), _phantom: PhantomData, }) }