diff --git a/account_manager/src/cli.rs b/account_manager/src/cli.rs index 9eded83cc5..ad962a14f8 100644 --- a/account_manager/src/cli.rs +++ b/account_manager/src/cli.rs @@ -1,10 +1,8 @@ use clap::{App, Arg, SubCommand}; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { - App::new("Account Manager") - .visible_aliases(&["am", "accounts", "accounts_manager"]) - .version("0.0.1") - .author("Sigma Prime ") + App::new("account_manager") + .visible_aliases(&["am", "account", "account_manager"]) .about("Eth 2.0 Accounts Manager") .arg( Arg::with_name("logfile") @@ -22,33 +20,45 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true), ) .subcommand( - SubCommand::with_name("generate") - .about("Generates a new validator private key") - .version("0.0.1") - .author("Sigma Prime "), - ) - .subcommand( - SubCommand::with_name("generate_deterministic") - .about("Generates a deterministic validator private key FOR TESTING") + SubCommand::with_name("validator") + .about("Eth2 validator managment commands.") .version("0.0.1") .author("Sigma Prime ") - .arg( - Arg::with_name("validator index") - .long("index") - .short("i") - .value_name("index") - .help("The index of the validator, for which the test key is generated") - .takes_value(true) - .required(true), + .subcommand( + SubCommand::with_name("new") + .about("Create a new validator.") + .subcommand( + SubCommand::with_name("insecure") + .about("Uses the insecure deterministic keypairs. Do not store value in these.") + .arg( + Arg::with_name("first") + .index(1) + .value_name("INDEX") + .help("Index of the first validator") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("last") + .index(2) + .value_name("INDEX") + .help("Index of the first validator") + .takes_value(true) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("random") + .about("Uses the Rust rand crate ThreadRandom to generate keys.") + .arg( + Arg::with_name("validator_count") + .index(1) + .value_name("INTEGER") + .help("The number of new validators to generate.") + .takes_value(true) + .default_value("1"), + ), + ) ) - .arg( - Arg::with_name("validator count") - .long("validator_count") - .short("n") - .value_name("validator_count") - .help("If supplied along with `index`, generates keys `i..i + n`.") - .takes_value(true) - .default_value("1"), - ), ) } diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 8687e3d09c..58d2427f2d 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -1,20 +1,17 @@ mod cli; pub mod validator; -use bls::Keypair; use clap::ArgMatches; use environment::RuntimeContext; -use slog::{crit, debug, info}; +use slog::{crit, info}; use std::fs; use std::path::PathBuf; -use types::{test_utils::generate_deterministic_keypair, EthSpec}; -use validator_client::Config as ValidatorClientConfig; +use types::{ChainSpec, EthSpec}; +use validator::{ValidatorDirectory, ValidatorDirectoryBuilder}; pub use cli::cli_app; -pub const DEFAULT_DATA_DIR: &str = ".lighthouse-validator"; -pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml"; - +/// Run the account manager, logging an error if the operation did not succeed. pub fn run(matches: &ArgMatches, context: RuntimeContext) { let log = context.log.clone(); match run_account_manager(matches, context) { @@ -23,13 +20,14 @@ pub fn run(matches: &ArgMatches, context: RuntimeContext) { } } +/// Run the account manager, returning an error if the operation did not succeed. fn run_account_manager( matches: &ArgMatches, context: RuntimeContext, ) -> Result<(), String> { let log = context.log.clone(); - let data_dir = matches + let datadir = matches .value_of("datadir") .map(PathBuf::from) .unwrap_or_else(|| { @@ -39,128 +37,114 @@ fn run_account_manager( panic!("Failed to find a home directory"); } }; - default_dir.push(DEFAULT_DATA_DIR); + default_dir.push(".lighthouse"); + default_dir.push("validator"); default_dir }); - fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to initialize data dir: {}", e))?; + fs::create_dir_all(&datadir).map_err(|e| format!("Failed to initialize datadir: {}", e))?; - let mut client_config = ValidatorClientConfig::default(); - client_config.data_dir = data_dir.clone(); - client_config - .apply_cli_args(&matches, &log) - .map_err(|e| format!("Failed to parse ClientConfig CLI arguments: {:?}", e))?; - - info!(log, "Located data directory"; - "path" => &client_config.data_dir.to_str()); - - panic!() - // -} - -pub fn run_old(matches: &ArgMatches, context: RuntimeContext) { - let mut log = context.log; - - let data_dir = match matches - .value_of("datadir") - .and_then(|v| Some(PathBuf::from(v))) - { - Some(v) => v, - None => { - // use the default - let mut default_dir = match dirs::home_dir() { - Some(v) => v, - None => { - crit!(log, "Failed to find a home directory"); - return; - } - }; - default_dir.push(DEFAULT_DATA_DIR); - default_dir - } - }; - - // create the directory if needed - match fs::create_dir_all(&data_dir) { - Ok(_) => {} - Err(e) => { - crit!(log, "Failed to initialize data dir"; "error" => format!("{}", e)); - return; - } - } - - let mut client_config = ValidatorClientConfig::default(); - - // Ensure the `data_dir` in the config matches that supplied to the CLI. - client_config.data_dir = data_dir.clone(); - - if let Err(e) = client_config.apply_cli_args(&matches, &mut log) { - crit!(log, "Failed to parse ClientConfig CLI arguments"; "error" => format!("{:?}", e)); - return; - }; - - // Log configuration - info!(log, ""; - "data_dir" => &client_config.data_dir.to_str()); + info!( + log, + "Located data directory"; + "path" => format!("{:?}", datadir) + ); match matches.subcommand() { - ("generate", Some(_)) => generate_random(&client_config, &log), - ("generate_deterministic", Some(m)) => { - if let Some(string) = m.value_of("validator index") { - let i: usize = string.parse().expect("Invalid validator index"); - if let Some(string) = m.value_of("validator count") { - let n: usize = string.parse().expect("Invalid end validator count"); - - let indices: Vec = (i..i + n).collect(); - generate_deterministic_multiple(&indices, &client_config, &log) - } else { - generate_deterministic(i, &client_config, &log) - } + ("validator", Some(matches)) => match matches.subcommand() { + ("new", Some(matches)) => new_validator_subcommand(matches, datadir, context)?, + _ => { + return Err("Invalid 'validator new' command. See --help.".to_string()); } + }, + _ => { + return Err("Invalid 'validator' command. See --help.".to_string()); + } + } + + Ok(()) +} + +/// Describes the crypto key generation methods for a validator. +enum KeygenMethod { + /// Produce an insecure "deterministic" keypair. Used only for interop and testing. + Insecure(usize), + /// Generate a new key from the `rand` thread random RNG. + ThreadRandom, +} + +/// Process the subcommand for creating new validators. +fn new_validator_subcommand( + matches: &ArgMatches, + datadir: PathBuf, + context: RuntimeContext, +) -> Result<(), String> { + let log = context.log.clone(); + + let methods: Vec = match matches.subcommand() { + ("insecure", Some(matches)) => { + let first = matches + .value_of("first") + .ok_or_else(|| "No first index".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse first index: {}", e))?; + let last = matches + .value_of("last") + .ok_or_else(|| "No last index".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse first index: {}", e))?; + + (first..last).map(KeygenMethod::Insecure).collect() + } + ("random", Some(matches)) => { + let count = matches + .value_of("validator_count") + .ok_or_else(|| "No validator count".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse validator count: {}", e))?; + + (0..count).map(|_| KeygenMethod::ThreadRandom).collect() } _ => { - crit!( - log, - "The account manager must be run with a subcommand. See help for more information." - ); + return Err("Invalid 'validator' command. See --help.".to_string()); } - } -} + }; -fn generate_random(config: &ValidatorClientConfig, log: &slog::Logger) { - save_key(&Keypair::random(), config, log) -} + let validators = make_validators(datadir.clone(), &methods, context.eth2_config.spec)?; -fn generate_deterministic_multiple( - validator_indices: &[usize], - config: &ValidatorClientConfig, - log: &slog::Logger, -) { - for validator_index in validator_indices { - generate_deterministic(*validator_index, config, log) - } -} - -fn generate_deterministic( - validator_index: usize, - config: &ValidatorClientConfig, - log: &slog::Logger, -) { - save_key( - &generate_deterministic_keypair(validator_index), - config, + info!( log, - ) -} - -fn save_key(keypair: &Keypair, config: &ValidatorClientConfig, log: &slog::Logger) { - let key_path: PathBuf = config - .save_key(&keypair) - .expect("Unable to save newly generated private key."); - debug!( - log, - "Keypair generated {:?}, saved to: {:?}", - keypair.identifier(), - key_path.to_string_lossy() + "Generated validator directories"; + "base_path" => format!("{:?}", datadir), + "count" => validators.len(), ); + + Ok(()) +} + +/// Produces a validator directory for each of the key generation methods provided in `methods`. +fn make_validators( + datadir: PathBuf, + methods: &[KeygenMethod], + spec: ChainSpec, +) -> Result, String> { + methods + .iter() + .map(|method| { + let mut builder = ValidatorDirectoryBuilder::default() + .spec(spec.clone()) + .full_deposit_amount()?; + + builder = match method { + KeygenMethod::Insecure(index) => builder.insecure_keypairs(*index), + KeygenMethod::ThreadRandom => builder.thread_random_keypairs(), + }; + + builder + .create_directory(datadir.clone())? + .write_keypair_files()? + .write_eth1_data_file()? + .build() + }) + .collect() } diff --git a/account_manager/src/validator.rs b/account_manager/src/validator.rs index 6a68a98725..6a67a0c443 100644 --- a/account_manager/src/validator.rs +++ b/account_manager/src/validator.rs @@ -14,12 +14,16 @@ use types::{ const VOTING_KEY_PREFIX: &str = "voting"; const WITHDRAWAL_KEY_PREFIX: &str = "withdrawal"; -const ETH1_DEPOSIT_DATA_FILE: &str = "eth1_deposit_data_{}.rlp"; +const ETH1_DEPOSIT_DATA_FILE: &str = "eth1_deposit_data.rlp"; +/// Returns the filename of a keypair file. fn keypair_file(prefix: &str) -> String { format!("{}_keypair", prefix) } +/// Represents the files/objects for each dedicated lighthouse validator directory. +/// +/// Generally lives in `~/.lighthouse/validators/`. #[derive(Debug, Clone, PartialEq)] pub struct ValidatorDirectory { pub directory: PathBuf, @@ -51,6 +55,7 @@ impl ValidatorDirectory { } } +/// Load a `Keypair` from a file. fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result { let path = base_path.join(keypair_file(file_prefix)); @@ -70,6 +75,7 @@ fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result Result, String> { let path = base_path.join(ETH1_DEPOSIT_DATA_FILE); @@ -87,6 +93,7 @@ fn load_eth1_deposit_data(base_path: PathBuf) -> Result, String> { Ok(bytes) } +/// A helper struct to allow SSZ enc/dec for a `Keypair`. #[derive(Encode, Decode)] struct SszEncodableKeypair { pk: PublicKey, @@ -111,6 +118,7 @@ impl From for SszEncodableKeypair { } } +/// Builds a `ValidatorDirectory`, both in-memory and on-disk. #[derive(Default)] pub struct ValidatorDirectoryBuilder { directory: Option, @@ -141,13 +149,13 @@ impl ValidatorDirectoryBuilder { self } - pub fn random_keypairs(mut self) -> Self { + pub fn thread_random_keypairs(mut self) -> Self { self.voting_keypair = Some(Keypair::random()); self.withdrawal_keypair = Some(Keypair::random()); self } - pub fn deterministic_keypairs(mut self, index: usize) -> Self { + pub fn insecure_keypairs(mut self, index: usize) -> Self { let keypair = generate_deterministic_keypair(index); self.voting_keypair = Some(keypair.clone()); self.withdrawal_keypair = Some(keypair); @@ -292,7 +300,7 @@ mod tests { type E = MinimalEthSpec; #[test] - fn round_trip() { + fn random_keypairs_round_trip() { let spec = E::default_spec(); let temp_dir = TempDir::new("acc_manager").expect("should create test dir"); @@ -300,7 +308,7 @@ mod tests { .spec(spec) .full_deposit_amount() .expect("should set full deposit amount") - .deterministic_keypairs(42) + .thread_random_keypairs() .create_directory(temp_dir.path().into()) .expect("should create directory") .write_keypair_files() @@ -318,4 +326,67 @@ mod tests { "the directory created should match the one loaded" ); } + + #[test] + fn deterministic_keypairs_round_trip() { + let spec = E::default_spec(); + let temp_dir = TempDir::new("acc_manager").expect("should create test dir"); + let index = 42; + + let created_dir = ValidatorDirectoryBuilder::default() + .spec(spec) + .full_deposit_amount() + .expect("should set full deposit amount") + .insecure_keypairs(index) + .create_directory(temp_dir.path().into()) + .expect("should create directory") + .write_keypair_files() + .expect("should write keypair files") + .write_eth1_data_file() + .expect("should write eth1 data file") + .build() + .expect("should build dir"); + + assert!( + created_dir.directory.exists(), + "should have created directory" + ); + + let mut parent = created_dir.directory.clone(); + parent.pop(); + assert_eq!( + parent, + PathBuf::from(temp_dir.path()), + "should have created directory ontop of base dir" + ); + + let expected_keypair = generate_deterministic_keypair(index); + assert_eq!( + created_dir.voting_keypair, + Some(expected_keypair.clone()), + "voting keypair should be as expected" + ); + assert_eq!( + created_dir.withdrawal_keypair, + Some(expected_keypair), + "withdrawal keypair should be as expected" + ); + assert!( + created_dir + .deposit_data + .clone() + .expect("should have data") + .len() + > 0, + "should have some deposit data" + ); + + let loaded_dir = ValidatorDirectory::load_for_signing(created_dir.directory.clone()) + .expect("should load directory"); + + assert_eq!( + created_dir, loaded_dir, + "the directory created should match the one loaded" + ); + } } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index a55345c858..74c261840c 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -31,7 +31,6 @@ fn main() { .value_name("TITLE") .help("Specifies the default eth2 spec type. Only effective when creating a new datadir.") .takes_value(true) - .required(true) .possible_values(&["mainnet", "minimal", "interop"]) .global(true) .default_value("minimal") @@ -124,7 +123,7 @@ fn run( // // Creating a command which can run both might be useful future works. - if let Some(sub_matches) = matches.subcommand_matches("Account Manager") { + if let Some(sub_matches) = matches.subcommand_matches("account_manager") { let runtime_context = environment.core_context(); account_manager::run(sub_matches, runtime_context); diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 623d1b349b..6b6956f786 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -22,16 +22,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("File path where output will be written.") .takes_value(true), ) - .arg( - Arg::with_name("spec") - .long("spec") - .value_name("TITLE") - .help("Specifies the default eth2 spec type.") - .takes_value(true) - .possible_values(&["mainnet", "minimal", "interop"]) - .conflicts_with("eth2-config") - .global(true) - ) .arg( Arg::with_name("eth2-config") .long("eth2-config") @@ -66,16 +56,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value(DEFAULT_SERVER_HTTP_PORT) .takes_value(true), ) - .arg( - Arg::with_name("debug-level") - .long("debug-level") - .value_name("LEVEL") - .short("s") - .help("The title of the spec constants for chain config.") - .takes_value(true) - .possible_values(&["info", "debug", "trace", "warn", "error", "crit"]) - .default_value("trace"), - ) /* * The "testnet" sub-command. *