Add EF launchpad import (#1381)

## Issue Addressed

NA

## Proposed Changes

Adds an integration for keys generated via https://github.com/ethereum/eth2.0-deposit (In reality keys are *actually* generated here: https://github.com/ethereum/eth2.0-deposit-cli).

## Additional Info

NA

## TODO

- [x] Docs
- [x] Tests

Co-authored-by: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Paul Hauner 2020-07-29 04:32:50 +00:00
parent ba0f3daf9d
commit eaa9f9744f
17 changed files with 620 additions and 65 deletions

17
Cargo.lock generated
View File

@ -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"

View File

@ -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<T: EthSpec>(
/// 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<P: AsRef<Path>>(validator_dir: P) -> Result<usize, String> {
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))
}

View File

@ -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<PathBuf> = clap_utils::parse_optional(matches, KEYSTORE_FLAG)?;
let keystores_dir: Option<PathBuf> = 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(())
}

View File

@ -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<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<
match matches.subcommand() {
(create::CMD, Some(matches)) => create::cli_run::<T>(matches, env, base_wallet_dir),
(deposit::CMD, Some(matches)) => deposit::cli_run::<T>(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",

View File

@ -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)

View File

@ -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.

View File

@ -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"

View File

@ -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<u8>) -> Vec<u8> {
bytes
}
/// Reads a password from TTY or stdin if `use_stdin == true`.
pub fn read_password_from_user(use_stdin: bool) -> Result<ZeroizeString, String> {
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);

View File

@ -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<P: AsRef<Path>>(
voting_keystore_path: P,
voting_keystore_password: Option<ZeroizeString>,
) -> Result<Self, Error> {
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<P: AsRef<Path>>(
&& 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"
));
}
}

View File

@ -27,6 +27,7 @@ pub struct JsonKeystore {
pub path: String,
pub pubkey: String,
pub version: Version,
pub description: Option<String>,
}
/// Version for `JsonKeystore`.

View File

@ -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<PublicKey> {
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<R: Read>(reader: R) -> Result<Self, Error> {
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<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
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`.

View File

@ -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
}

View File

@ -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" }

View File

@ -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"
);
}

View File

@ -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)
}
}

View File

@ -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<Self, Error> {
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<Keypair, Error> {
@ -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) => {

View File

@ -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.