diff --git a/Cargo.lock b/Cargo.lock index 2d38665be..e9544d71d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,8 +38,14 @@ dependencies = [ "eth2_keystore", "eth2_wallet", "rand 0.7.3", + "regex", + "rpassword", "serde", "serde_derive", + "serde_yaml", + "slog", + "types", + "validator_dir", "zeroize", ] @@ -2955,6 +2961,7 @@ name = "lighthouse" version = "0.1.2" dependencies = [ "account_manager", + "account_utils", "beacon_node", "boot_node", "clap", @@ -4337,6 +4344,16 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rpassword" +version = "4.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99371657d3c8e4d816fb6221db98fa408242b0b53bac08f8676a41f8554fe99f" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "rusqlite" version = "0.23.1" diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index dfb9f371b..0781e911a 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,9 +1,10 @@ use crate::{common::ensure_dir_exists, SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG}; -use account_utils::{random_password, strip_off_newlines}; +use account_utils::{random_password, strip_off_newlines, validator_definitions}; use clap::{App, Arg, ArgMatches}; use environment::Environment; use eth2_wallet::PlainText; use eth2_wallet_manager::WalletManager; +use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; use types::EthSpec; @@ -192,10 +193,15 @@ pub fn cli_run( /// Returns the number of validators that exist in the given `validator_dir`. /// -/// This function just assumes any file is a validator directory, making it likely to return a -/// higher number than accurate but never a lower one. +/// This function just assumes all files and directories, excluding the validator definitions YAML, +/// are validator directories, making it likely to return a higher number than accurate +/// but never a lower one. fn existing_validator_count>(validator_dir: P) -> Result { fs::read_dir(validator_dir.as_ref()) - .map(|iter| iter.count()) + .map(|iter| { + iter.filter_map(|e| e.ok()) + .filter(|e| e.file_name() != OsStr::new(validator_definitions::CONFIG_FILENAME)) + .count() + }) .map_err(|e| format!("Unable to read {:?}: {}", validator_dir.as_ref(), e)) } diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs new file mode 100644 index 000000000..adc08dce2 --- /dev/null +++ b/account_manager/src/validator/import.rs @@ -0,0 +1,214 @@ +use crate::{common::ensure_dir_exists, VALIDATOR_DIR_FLAG}; +use account_utils::{ + eth2_keystore::Keystore, + read_password_from_user, + validator_definitions::{ + recursively_find_voting_keystores, ValidatorDefinition, ValidatorDefinitions, + CONFIG_FILENAME, + }, +}; +use clap::{App, Arg, ArgMatches}; +use std::fs; +use std::path::PathBuf; +use std::thread::sleep; +use std::time::Duration; + +pub const CMD: &str = "import"; +pub const KEYSTORE_FLAG: &str = "keystore"; +pub const DIR_FLAG: &str = "directory"; +pub const STDIN_PASSWORD_FLAG: &str = "stdin-passwords"; + +pub const PASSWORD_PROMPT: &str = "Enter the keystore password, or press enter to omit it:"; +pub const KEYSTORE_REUSE_WARNING: &str = "DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH \ + ANOTHER CLIENT, OR YOU WILL GET SLASHED."; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about( + "Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \ + requesting passwords interactively. The directory flag provides a convenient \ + method for importing a directory of keys generated by the eth2-deposit-cli \ + Python utility.", + ) + .arg( + Arg::with_name(KEYSTORE_FLAG) + .long(KEYSTORE_FLAG) + .value_name("KEYSTORE_PATH") + .help("Path to a single keystore to be imported.") + .conflicts_with(DIR_FLAG) + .required_unless(DIR_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(DIR_FLAG) + .long(DIR_FLAG) + .value_name("KEYSTORES_DIRECTORY") + .help( + "Path to a directory which contains zero or more keystores \ + for import. This directory and all sub-directories will be \ + searched and any file name which contains 'keystore' and \ + has the '.json' extension will be attempted to be imported.", + ) + .conflicts_with(KEYSTORE_FLAG) + .required_unless(KEYSTORE_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(VALIDATOR_DIR_FLAG) + .long(VALIDATOR_DIR_FLAG) + .value_name("VALIDATOR_DIRECTORY") + .help( + "The path where the validator directories will be created. \ + Defaults to ~/.lighthouse/validators", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(STDIN_PASSWORD_FLAG) + .long(STDIN_PASSWORD_FLAG) + .help("If present, read passwords from stdin instead of tty."), + ) +} + +pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { + let keystore: Option = clap_utils::parse_optional(matches, KEYSTORE_FLAG)?; + let keystores_dir: Option = clap_utils::parse_optional(matches, DIR_FLAG)?; + let validator_dir = clap_utils::parse_path_with_default_in_home_dir( + matches, + VALIDATOR_DIR_FLAG, + PathBuf::new().join(".lighthouse").join("validators"), + )?; + let stdin_password = matches.is_present(STDIN_PASSWORD_FLAG); + + ensure_dir_exists(&validator_dir)?; + + let mut defs = ValidatorDefinitions::open_or_create(&validator_dir) + .map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?; + + // Collect the paths for the keystores that should be imported. + let keystore_paths = match (keystore, keystores_dir) { + (Some(keystore), None) => vec![keystore], + (None, Some(keystores_dir)) => { + let mut keystores = vec![]; + + recursively_find_voting_keystores(&keystores_dir, &mut keystores) + .map_err(|e| format!("Unable to search {:?}: {:?}", keystores_dir, e))?; + + if keystores.is_empty() { + eprintln!("No keystores found in {:?}", keystores_dir); + return Ok(()); + } + + keystores + } + _ => { + return Err(format!( + "Must supply either --{} or --{}", + KEYSTORE_FLAG, DIR_FLAG + )) + } + }; + + eprintln!("WARNING: {}", KEYSTORE_REUSE_WARNING); + + // For each keystore: + // + // - Obtain the keystore password, if the user desires. + // - Copy the keystore into the `validator_dir`. + // - Add the keystore to the validator definitions file. + // + // Skip keystores that already exist, but exit early if any operation fails. + let mut num_imported_keystores = 0; + for src_keystore in &keystore_paths { + let keystore = Keystore::from_json_file(src_keystore) + .map_err(|e| format!("Unable to read keystore JSON {:?}: {:?}", src_keystore, e))?; + + eprintln!(""); + eprintln!("Keystore found at {:?}:", src_keystore); + eprintln!(""); + eprintln!(" - Public key: 0x{}", keystore.pubkey()); + eprintln!(" - UUID: {}", keystore.uuid()); + eprintln!(""); + eprintln!( + "If you enter the password it will be stored as plain-text in {} so that it is not \ + required each time the validator client starts.", + CONFIG_FILENAME + ); + + let password_opt = loop { + eprintln!(""); + eprintln!("{}", PASSWORD_PROMPT); + + let password = read_password_from_user(stdin_password)?; + + if password.as_ref().is_empty() { + eprintln!("Continuing without password."); + sleep(Duration::from_secs(1)); // Provides nicer UX. + break None; + } + + match keystore.decrypt_keypair(password.as_ref()) { + Ok(_) => { + eprintln!("Password is correct."); + eprintln!(""); + sleep(Duration::from_secs(1)); // Provides nicer UX. + break Some(password); + } + Err(eth2_keystore::Error::InvalidPassword) => { + eprintln!("Invalid password"); + } + Err(e) => return Err(format!("Error whilst decrypting keypair: {:?}", e)), + } + }; + + // The keystore is placed in a directory that matches the name of the public key. This + // provides some loose protection against adding the same keystore twice. + let dest_dir = validator_dir.join(format!("0x{}", keystore.pubkey())); + if dest_dir.exists() { + eprintln!( + "Skipping import of keystore for existing public key: {:?}", + src_keystore + ); + continue; + } + + fs::create_dir_all(&dest_dir) + .map_err(|e| format!("Unable to create import directory: {:?}", e))?; + + // Retain the keystore file name, but place it in the new directory. + let dest_keystore = src_keystore + .file_name() + .and_then(|file_name| file_name.to_str()) + .map(|file_name_str| dest_dir.join(file_name_str)) + .ok_or_else(|| format!("Badly formatted file name: {:?}", src_keystore))?; + + // Copy the keystore to the new location. + fs::copy(&src_keystore, &dest_keystore) + .map_err(|e| format!("Unable to copy keystore: {:?}", e))?; + + eprintln!("Successfully imported keystore."); + num_imported_keystores += 1; + + let validator_def = + ValidatorDefinition::new_keystore_with_password(&dest_keystore, password_opt) + .map_err(|e| format!("Unable to create new validator definition: {:?}", e))?; + + defs.push(validator_def); + + defs.save(&validator_dir) + .map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?; + + eprintln!("Successfully updated {}.", CONFIG_FILENAME); + } + + eprintln!(""); + eprintln!( + "Successfully imported {} validators ({} skipped).", + num_imported_keystores, + keystore_paths.len() - num_imported_keystores + ); + eprintln!(""); + eprintln!("WARNING: {}", KEYSTORE_REUSE_WARNING); + + Ok(()) +} diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index e4862df06..0df22d7fb 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -1,5 +1,6 @@ pub mod create; pub mod deposit; +pub mod import; pub mod list; use crate::common::base_wallet_dir; @@ -21,6 +22,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .subcommand(create::cli_app()) .subcommand(deposit::cli_app()) + .subcommand(import::cli_app()) .subcommand(list::cli_app()) } @@ -30,6 +32,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< match matches.subcommand() { (create::CMD, Some(matches)) => create::cli_run::(matches, env, base_wallet_dir), (deposit::CMD, Some(matches)) => deposit::cli_run::(matches, env), + (import::CMD, Some(matches)) => import::cli_run(matches), (list::CMD, Some(matches)) => list::cli_run(matches), (unknown, _) => Err(format!( "{} does not have a {} command. See --help", diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1fc483e11..b4faece0c 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -11,6 +11,7 @@ * [Create a wallet](./wallet-create.md) * [Create a validator](./validator-create.md) * [Validator Management](./validator-management.md) + * [Importing from the Eth2 Launchpad](./validator-import-launchpad.md) * [Local Testnets](./local-testnets.md) * [API](./api.md) * [HTTP (RESTful JSON)](./http.md) diff --git a/book/src/validator-import-launchpad.md b/book/src/validator-import-launchpad.md new file mode 100644 index 000000000..3688b139c --- /dev/null +++ b/book/src/validator-import-launchpad.md @@ -0,0 +1,87 @@ +# Importing from the Ethereum 2.0 Launchpad + +The [Eth2 Lauchpad](https://github.com/ethereum/eth2.0-deposit) is a website +from the Ethereum Foundation which guides users how to use the +[`eth2.0-deposit-cli`](https://github.com/ethereum/eth2.0-deposit-cli) +command-line program to generate Eth2 validator keys. + +The keys that are generated from `eth2.0-deposit-cli` can be easily loaded into +a Lighthouse validator client (`lighthouse vc`). In fact, both of these +programs are designed to work with each other. + +This guide will show the user how to import their keys into Lighthouse so they +can perform their duties as a validator. The guide assumes the user has already +[installed Lighthouse](./installation.md). + +## Instructions + +Whilst following the steps on the website, users are instructed to download the +[`eth2.0-deposit-cli`](https://github.com/ethereum/eth2.0-deposit-cli) +repository. This `eth2-deposit-cli` script will generate the validator BLS keys +into a `validator_keys` directory. We assume that the user's +present-working-directory is the `eth2-deposit-cli` repository (this is where +you will be if you just ran the `./deposit.sh` script from the Eth2 Launchpad +website). If this is not the case, simply change the `--directory` to point to +the `validator_keys` directory. + +Now, assuming that the user is in the `eth2-deposit-cli` directory and they're +using the standard `validators` directory (specify a different one using +`--validator-dir` flag), they can follow these steps: + +### 1. Run the `lighthouse account validator import` command. + + +```bash +lighthouse account validator import --directory validator_keys +``` + + +After which they will be prompted for a password for each keystore discovered: + +``` +Keystore found at "validator_keys/keystore-m_12381_3600_0_0_0-1595406747.json": + + - Public key: 0xa5e8702533f6d66422e042a0bf3471ab9b302ce115633fa6fdc5643f804b6b4f1c33baf95f125ec21969a3b1e0dd9e56 + - UUID: 8ea4cf99-8719-43c5-9eda-e97b8a4e074f + +If you enter a password it will be stored in validator_definitions.yml so that it is not required each time the validator client starts. + +Enter a password, or press enter to omit a password: +``` + +The user can choose whether or not they'd like to store the validator password +in the [`validator_definitions.yml`](./validator-management.md) file. If the +password is *not* stored here, the validator client (`lighthouse vc`) +application will ask for the password each time it starts. This might be nice +for some users from a security perspective (i.e., if it is a shared computer), +however it means that if the validator client restarts, the user will be liable +to off-line penalties until they can enter the password. If the user trusts the +computer that is running the validator client and they are seeking maximum +validator rewards, we recommend entering a password at this point. + +Once the process is done the user will see: + +``` +Successfully imported keystore. +Successfully updated validator_definitions.yml. + +Successfully imported 1 validators (0 skipped). + +WARNING: DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH ANOTHER CLIENT, OR YOU WILL GET SLASHED.. +``` + +The import process is complete! + +### 2. Run the `lighthouse vc` command. + +Now the keys are imported the user can start performing their validator duties +by running `lighthouse vc` and checking that their validator public key appears +as a `voting_pubkey` in one of the following logs: + +``` +INFO Enabled validator voting_pubkey: 0xa5e8702533f6d66422e042a0bf3471ab9b302ce115633fa6fdc5643f804b6b4f1c33baf95f125ec21969a3b1e0dd9e56 +``` + +Once this log appears (and there are no errors) the `lighthouse vc` application +will ensure that the validator starts performing its duties and being rewarded +by the protocol. There is no more input required from the user. diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index 4a9fa0fb4..feb8f6467 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -13,3 +13,9 @@ eth2_keystore = { path = "../../crypto/eth2_keystore" } zeroize = { version = "1.0.0", features = ["zeroize_derive"] } serde = "1.0.110" serde_derive = "1.0.110" +serde_yaml = "0.8.13" +slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } +types = { path = "../../consensus/types" } +validator_dir = { path = "../validator_dir" } +regex = "1.3.9" +rpassword = "4.0.5" diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 351db3305..48cf09134 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -12,6 +12,9 @@ use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use zeroize::Zeroize; +pub mod validator_definitions; + +pub use eth2_keystore; pub use eth2_wallet::PlainText; /// The `Alphanumeric` crate only generates a-z, A-Z, 0-9, therefore it has a range of 62 @@ -91,10 +94,23 @@ pub fn strip_off_newlines(mut bytes: Vec) -> Vec { bytes } +/// Reads a password from TTY or stdin if `use_stdin == true`. +pub fn read_password_from_user(use_stdin: bool) -> Result { + let result = if use_stdin { + rpassword::prompt_password_stderr("") + .map_err(|e| format!("Error reading from stdin: {}", e)) + } else { + rpassword::read_password_from_tty(None) + .map_err(|e| format!("Error reading from tty: {}", e)) + }; + + result.map(ZeroizeString::from) +} + /// Provides a new-type wrapper around `String` that is zeroized on `Drop`. /// /// Useful for ensuring that password memory is zeroed-out on drop. -#[derive(Clone, Serialize, Deserialize, Zeroize)] +#[derive(Clone, PartialEq, Serialize, Deserialize, Zeroize)] #[zeroize(drop)] #[serde(transparent)] pub struct ZeroizeString(String); diff --git a/validator_client/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs similarity index 66% rename from validator_client/src/validator_definitions.rs rename to common/account_utils/src/validator_definitions.rs index f7dbfabe8..c1f8e0ad5 100644 --- a/validator_client/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -1,10 +1,11 @@ //! Provides a file format for defining validators that should be initialized by this validator. //! //! Serves as the source-of-truth of which validators this validator client should attempt (or not -//! attempt) to load //! into the `crate::intialized_validators::InitializedValidators` struct. +//! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. -use account_utils::{create_with_600_perms, default_keystore_password_path, ZeroizeString}; +use crate::{create_with_600_perms, default_keystore_password_path, ZeroizeString}; use eth2_keystore::Keystore; +use regex::Regex; use serde_derive::{Deserialize, Serialize}; use slog::{error, Logger}; use std::collections::HashSet; @@ -30,13 +31,17 @@ pub enum Error { UnableToEncodeFile(serde_yaml::Error), /// The config file could not be written to the filesystem. UnableToWriteFile(io::Error), + /// The public key from the keystore is invalid. + InvalidKeystorePubkey, + /// The keystore was unable to be opened. + UnableToOpenKeystore(eth2_keystore::Error), } /// Defines how the validator client should attempt to sign messages for this validator. /// /// Presently there is only a single variant, however we expect more variants to arise (e.g., /// remote signing). -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type")] pub enum SigningDefinition { /// A validator that is defined by an EIP-2335 keystore on the local filesystem. @@ -54,7 +59,7 @@ pub enum SigningDefinition { /// /// Presently there is only a single variant, however we expect more variants to arise (e.g., /// remote signing). -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct ValidatorDefinition { pub enabled: bool, pub voting_public_key: PublicKey, @@ -62,6 +67,36 @@ pub struct ValidatorDefinition { pub signing_definition: SigningDefinition, } +impl ValidatorDefinition { + /// Create a new definition for a voting keystore at the given `voting_keystore_path` that can + /// be unlocked with `voting_keystore_password`. + /// + /// ## Notes + /// + /// This function does not check the password against the keystore. + pub fn new_keystore_with_password>( + voting_keystore_path: P, + voting_keystore_password: Option, + ) -> Result { + let voting_keystore_path = voting_keystore_path.as_ref().into(); + let keystore = + Keystore::from_json_file(&voting_keystore_path).map_err(Error::UnableToOpenKeystore)?; + let voting_public_key = keystore + .public_key() + .ok_or_else(|| Error::InvalidKeystorePubkey)?; + + Ok(ValidatorDefinition { + enabled: true, + voting_public_key, + signing_definition: SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password_path: None, + voting_keystore_password, + }, + }) + } +} + /// A list of `ValidatorDefinition` that serves as a serde-able configuration file which defines a /// list of validators to be initialized by this validator client. #[derive(Default, Serialize, Deserialize)] @@ -155,19 +190,17 @@ impl ValidatorDefinitions { )) .filter(|path| path.exists()); - let voting_public_key = - match serde_yaml::from_str(&format!("0x{}", keystore.pubkey())) { - Ok(pubkey) => pubkey, - Err(e) => { - error!( - log, - "Invalid keystore public key"; - "error" => format!("{:?}", e), - "keystore" => format!("{:?}", voting_keystore_path) - ); - return None; - } - }; + let voting_public_key = match keystore.public_key() { + Some(pubkey) => pubkey, + None => { + error!( + log, + "Invalid keystore public key"; + "keystore" => format!("{:?}", voting_keystore_path) + ); + return None; + } + }; Some(ValidatorDefinition { enabled: true, @@ -203,6 +236,11 @@ impl ValidatorDefinitions { } } + /// Adds a new `ValidatorDefinition` to `self`. + pub fn push(&mut self, def: ValidatorDefinition) { + self.0.push(def) + } + /// Returns a slice of all `ValidatorDefinition` in `self`. pub fn as_slice(&self) -> &[ValidatorDefinition] { self.0.as_slice() @@ -233,10 +271,70 @@ pub fn recursively_find_voting_keystores>( && dir_entry .file_name() .to_str() - .map_or(false, |filename| filename == VOTING_KEYSTORE_FILE) + .map_or(false, is_voting_keystore) { matches.push(dir_entry.path()) } Ok(()) }) } + +/// Returns `true` if we should consider the `file_name` to represent a voting keystore. +fn is_voting_keystore(file_name: &str) -> bool { + // All formats end with `.json`. + if !file_name.ends_with(".json") { + return false; + } + + // The format used by Lighthouse. + if file_name == VOTING_KEYSTORE_FILE { + return true; + } + + // The format exported by the `eth2.0-deposit-cli` library. + // + // Reference to function that generates keystores: + // + // https://github.com/ethereum/eth2.0-deposit-cli/blob/7cebff15eac299b3b1b090c896dd3410c8463450/eth2deposit/credentials.py#L58-L62 + // + // Since we include the key derivation path of `m/12381/3600/x/0/0` this should only ever match + // with a voting keystore and never a withdrawal keystore. + // + // Key derivation path reference: + // + // https://eips.ethereum.org/EIPS/eip-2334 + if Regex::new("keystore-m_12381_3600_[0-9]+_0_0-[0-9]+.json") + .expect("regex is valid") + .is_match(file_name) + { + return true; + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn voting_keystore_filename() { + assert!(is_voting_keystore(VOTING_KEYSTORE_FILE)); + assert!(!is_voting_keystore("cats")); + assert!(!is_voting_keystore(&format!("a{}", VOTING_KEYSTORE_FILE))); + assert!(!is_voting_keystore(&format!("{}b", VOTING_KEYSTORE_FILE))); + assert!(is_voting_keystore( + "keystore-m_12381_3600_0_0_0-1593476250.json" + )); + assert!(is_voting_keystore( + "keystore-m_12381_3600_1_0_0-1593476250.json" + )); + assert!(is_voting_keystore("keystore-m_12381_3600_1_0_0-1593.json")); + assert!(!is_voting_keystore( + "keystore-m_12381_3600_0_0-1593476250.json" + )); + assert!(!is_voting_keystore( + "keystore-m_12381_3600_1_0-1593476250.json" + )); + } +} diff --git a/crypto/eth2_keystore/src/json_keystore/mod.rs b/crypto/eth2_keystore/src/json_keystore/mod.rs index 3706082ea..a30855d21 100644 --- a/crypto/eth2_keystore/src/json_keystore/mod.rs +++ b/crypto/eth2_keystore/src/json_keystore/mod.rs @@ -27,6 +27,7 @@ pub struct JsonKeystore { pub path: String, pub pubkey: String, pub version: Version, + pub description: Option, } /// Version for `JsonKeystore`. diff --git a/crypto/eth2_keystore/src/keystore.rs b/crypto/eth2_keystore/src/keystore.rs index 536a7ae2e..ede03275f 100644 --- a/crypto/eth2_keystore/src/keystore.rs +++ b/crypto/eth2_keystore/src/keystore.rs @@ -10,7 +10,7 @@ use crate::Uuid; use aes_ctr::stream_cipher::generic_array::GenericArray; use aes_ctr::stream_cipher::{NewStreamCipher, SyncStreamCipher}; use aes_ctr::Aes128Ctr as AesCtr; -use bls::{Keypair, SecretKey, ZeroizeHash}; +use bls::{Keypair, PublicKey, SecretKey, ZeroizeHash}; use eth2_key_derivation::PlainText; use hmac::Hmac; use pbkdf2::pbkdf2; @@ -21,7 +21,9 @@ use scrypt::{ }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +use std::fs::OpenOptions; use std::io::{Read, Write}; +use std::path::Path; /// The byte-length of a BLS secret key. const SECRET_KEY_LEN: usize = 32; @@ -173,6 +175,7 @@ impl Keystore { path, pubkey: keypair.pk.to_hex_string()[2..].to_string(), version: Version::four(), + description: None, }, }) } @@ -224,6 +227,11 @@ impl Keystore { &self.json.pubkey } + /// Returns the pubkey for the keystore, parsed as a `PublicKey` if it parses. + pub fn public_key(&self) -> Option { + serde_json::from_str(&format!("\"0x{}\"", &self.json.pubkey)).ok() + } + /// Returns the key derivation function for the keystore. pub fn kdf(&self) -> &Kdf { &self.json.crypto.kdf.params @@ -248,6 +256,17 @@ impl Keystore { pub fn from_json_reader(reader: R) -> Result { serde_json::from_reader(reader).map_err(|e| Error::ReadError(format!("{}", e))) } + + /// Instantiates `self` by reading a JSON file at `path`. + pub fn from_json_file>(path: P) -> Result { + OpenOptions::new() + .read(true) + .write(false) + .create(false) + .open(path) + .map_err(|e| Error::ReadError(format!("{}", e))) + .and_then(Self::from_json_reader) + } } /// Instantiates a BLS keypair from the given `secret`. diff --git a/crypto/eth2_keystore/tests/eip2335_vectors.rs b/crypto/eth2_keystore/tests/eip2335_vectors.rs index b0049ed00..59d017c69 100644 --- a/crypto/eth2_keystore/tests/eip2335_vectors.rs +++ b/crypto/eth2_keystore/tests/eip2335_vectors.rs @@ -14,6 +14,10 @@ pub fn decode_and_check_sk(json: &str) -> Keystore { let keystore = Keystore::from_json_str(json).expect("should decode keystore json"); let expected_sk = hex::decode(EXPECTED_SECRET).unwrap(); let keypair = keystore.decrypt_keypair(PASSWORD.as_bytes()).unwrap(); + assert_eq!( + format!("0x{}", keystore.pubkey()), + format!("{:?}", keystore.public_key().unwrap()) + ); assert_eq!(keypair.sk.serialize().as_ref(), &expected_sk[..]); keystore } diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 5e27ac40a..59eac118f 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -30,3 +30,4 @@ git-version = "0.3.4" [dev-dependencies] tempfile = "3.1.0" validator_dir = { path = "../common/validator_dir" } +account_utils = { path = "../common/account_utils" } diff --git a/lighthouse/tests/account_manager.rs b/lighthouse/tests/account_manager.rs index 079f78cba..133bbd721 100644 --- a/lighthouse/tests/account_manager.rs +++ b/lighthouse/tests/account_manager.rs @@ -1,7 +1,11 @@ #![cfg(not(debug_assertions))] use account_manager::{ - validator::{create::*, CMD as VALIDATOR_CMD}, + validator::{ + create::*, + import::{self, CMD as IMPORT_CMD}, + CMD as VALIDATOR_CMD, + }, wallet::{ create::{CMD as CREATE_CMD, *}, list::CMD as LIST_CMD, @@ -9,12 +13,19 @@ use account_manager::{ }, BASE_DIR_FLAG, CMD as ACCOUNT_CMD, *, }; +use account_utils::{ + eth2_keystore::KeystoreBuilder, + validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions}, + ZeroizeString, +}; use std::env; -use std::fs; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::{Command, Output, Stdio}; use std::str::from_utf8; use tempfile::{tempdir, TempDir}; +use types::Keypair; use validator_dir::ValidatorDir; // TODO: create tests for the `lighthouse account validator deposit` command. This involves getting @@ -191,7 +202,7 @@ fn wallet_create_and_list() { assert_eq!(list_wallets(wally.base_dir()).len(), 2); } -/// Returns the `lighthouse account wallet` command. +/// Returns the `lighthouse account validator` command. fn validator_cmd() -> Command { let mut cmd = account_cmd(); cmd.arg(VALIDATOR_CMD); @@ -363,3 +374,97 @@ fn validator_create() { assert_eq!(dir_child_count(validator_dir.path()), 6); } + +/// Returns the `lighthouse account validator import` command. +fn validator_import_cmd() -> Command { + let mut cmd = validator_cmd(); + cmd.arg(IMPORT_CMD); + cmd +} + +#[test] +fn validator_import_launchpad() { + const PASSWORD: &str = "cats"; + const KEYSTORE_NAME: &str = "keystore-m_12381_3600_0_0_0-1595406747.json"; + const NOT_KEYSTORE_NAME: &str = "keystore-m_12381_3600_0_0-1595406747.json"; + + let src_dir = tempdir().unwrap(); + let dst_dir = tempdir().unwrap(); + + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, PASSWORD.as_bytes(), "".into()) + .unwrap() + .build() + .unwrap(); + + let dst_keystore_dir = dst_dir.path().join(format!("0x{}", keystore.pubkey())); + + // Create a keystore in the src dir. + File::create(src_dir.path().join(KEYSTORE_NAME)) + .map(|mut file| keystore.to_json_writer(&mut file).unwrap()) + .unwrap(); + + // Create a not-keystore file in the src dir. + File::create(src_dir.path().join(NOT_KEYSTORE_NAME)).unwrap(); + + let mut child = validator_import_cmd() + .arg(format!("--{}", import::STDIN_PASSWORD_FLAG)) // Using tty does not work well with tests. + .arg(format!("--{}", import::DIR_FLAG)) + .arg(src_dir.path().as_os_str()) + .arg(format!("--{}", VALIDATOR_DIR_FLAG)) + .arg(dst_dir.path().as_os_str()) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + + let mut stderr = child.stderr.as_mut().map(BufReader::new).unwrap().lines(); + let stdin = child.stdin.as_mut().unwrap(); + + loop { + if stderr.next().unwrap().unwrap() == import::PASSWORD_PROMPT { + break; + } + } + + stdin.write(format!("{}\n", PASSWORD).as_bytes()).unwrap(); + + child.wait().unwrap(); + + assert!( + src_dir.path().join(KEYSTORE_NAME).exists(), + "keystore should not be removed from src dir" + ); + assert!( + src_dir.path().join(NOT_KEYSTORE_NAME).exists(), + "not-keystore should not be removed from src dir." + ); + + let voting_keystore_path = dst_keystore_dir.join(KEYSTORE_NAME); + + assert!( + voting_keystore_path.exists(), + "keystore should be present in dst dir" + ); + assert!( + !dst_dir.path().join(NOT_KEYSTORE_NAME).exists(), + "not-keystore should not be present in dst dir" + ); + + let defs = ValidatorDefinitions::open(&dst_dir).unwrap(); + + let expected_def = ValidatorDefinition { + enabled: true, + voting_public_key: keystore.public_key().unwrap(), + signing_definition: SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password_path: None, + voting_keystore_password: Some(ZeroizeString::from(PASSWORD.to_string())), + }, + }; + + assert!( + defs.as_slice() == &[expected_def], + "validator defs file should be accurate" + ); +} diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 55bbc8b4a..4f1b2079e 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -80,13 +80,6 @@ impl Config { config.secrets_dir = secrets_dir; } - if !config.secrets_dir.exists() { - return Err(format!( - "The directory for validator passwords (--secrets-dir) does not exist: {:?}", - config.secrets_dir - )); - } - Ok(config) } } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 14e034590..436dcb4ba 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -6,18 +6,23 @@ //! The `InitializedValidators` struct in this file serves as the source-of-truth of which //! validators are managed by this validator client. -use crate::validator_definitions::{ - self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME, +use account_utils::{ + read_password, read_password_from_user, + validator_definitions::{ + self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME, + }, }; -use account_utils::{read_password, ZeroizeString}; use eth2_keystore::Keystore; use slog::{error, info, warn, Logger}; use std::collections::HashMap; use std::fs::{self, File, OpenOptions}; -use std::io::{self, BufRead, Stdin}; +use std::io; use std::path::PathBuf; use types::{Keypair, PublicKey}; +// Use TTY instead of stdin to capture passwords from users. +const USE_STDIN: bool = false; + #[derive(Debug)] pub enum Error { /// Refused to open a validator with an existing lockfile since that validator may be in-use by @@ -47,10 +52,8 @@ pub enum Error { UnableToInitializeDisabledValidator, /// It is not legal to try and initialize a disabled validator definition. PasswordUnknown(PathBuf), - /// There was no line when reading from stdin. - NoStdinLine, /// There was an error reading from stdin. - UnableToReadFromStdin(io::Error), + UnableToReadPasswordFromUser(String), } /// A method used by a validator to sign messages. @@ -84,7 +87,6 @@ impl InitializedValidator { pub fn from_definition( def: ValidatorDefinition, strict_lockfiles: bool, - stdin: Option<&Stdin>, log: &Logger, ) -> Result { if !def.enabled { @@ -121,15 +123,7 @@ impl InitializedValidator { } // If there is no password available, maybe prompt for a password. (None, None) => { - if let Some(stdin) = stdin { - unlock_keystore_via_stdin_password( - stdin, - &voting_keystore, - &voting_keystore_path, - )? - } else { - return Err(Error::PasswordUnknown(voting_keystore_path)); - } + unlock_keystore_via_stdin_password(&voting_keystore, &voting_keystore_path)? } }; @@ -228,7 +222,6 @@ impl Drop for InitializedValidator { /// Try to unlock `keystore` at `keystore_path` by prompting the user via `stdin`. fn unlock_keystore_via_stdin_password( - stdin: &Stdin, keystore: &Keystore, keystore_path: &PathBuf, ) -> Result { @@ -251,13 +244,8 @@ fn unlock_keystore_via_stdin_password( eprintln!("Enter password (or press Ctrl+c to exit):"); loop { - let password = stdin - .lock() - .lines() - .next() - .ok_or_else(|| Error::NoStdinLine)? - .map_err(Error::UnableToReadFromStdin) - .map(ZeroizeString::from)?; + let password = + read_password_from_user(USE_STDIN).map_err(Error::UnableToReadPasswordFromUser)?; eprintln!(""); @@ -375,8 +363,6 @@ impl InitializedValidators { /// I.e., if there are two different definitions with the same public key then the second will /// be ignored. fn update_validators(&mut self) -> Result<(), Error> { - let stdin = io::stdin(); - for def in self.definitions.as_slice() { if def.enabled { match &def.signing_definition { @@ -388,7 +374,6 @@ impl InitializedValidators { match InitializedValidator::from_definition( def.clone(), self.strict_lockfiles, - Some(&stdin), &self.log, ) { Ok(init) => { diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 0efeba769..e5d8428ce 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -7,12 +7,12 @@ mod fork_service; mod initialized_validators; mod is_synced; mod notifier; -mod validator_definitions; mod validator_store; pub use cli::cli_app; pub use config::Config; +use account_utils::validator_definitions::ValidatorDefinitions; use attestation_service::{AttestationService, AttestationServiceBuilder}; use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; @@ -29,7 +29,6 @@ use slot_clock::SystemTimeSlotClock; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{delay_for, Duration}; use types::EthSpec; -use validator_definitions::ValidatorDefinitions; use validator_store::ValidatorStore; /// The interval between attempts to contact the beacon node during startup.