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:
parent
ba0f3daf9d
commit
eaa9f9744f
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -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"
|
||||
|
@ -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))
|
||||
}
|
||||
|
214
account_manager/src/validator/import.rs
Normal file
214
account_manager/src/validator/import.rs
Normal 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(())
|
||||
}
|
@ -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",
|
||||
|
@ -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)
|
||||
|
87
book/src/validator-import-launchpad.md
Normal file
87
book/src/validator-import-launchpad.md
Normal 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.
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
));
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ pub struct JsonKeystore {
|
||||
pub path: String,
|
||||
pub pubkey: String,
|
||||
pub version: Version,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Version for `JsonKeystore`.
|
||||
|
@ -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`.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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" }
|
||||
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user