From e26da35cbfabff1a51f8ca8eb8356fcdef20b4d1 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 22 Jul 2020 09:34:55 +0000 Subject: [PATCH] Introduce validator definition file for VC (#1357) ## Issue Addressed NA ## Proposed Changes - Introduces the `valdiator_definitions.yml` file which serves as an explicit list of validators that should be run by the validator client. - Removes `--strict` flag, split into `--strict-lockfiles` and `--disable-auto-discover` - Adds a "Validator Management" page to the book. - Adds the `common/account_utils` crate which contains some logic that was starting to duplicate across the codebase. The new docs for this feature are the best description of it (apart from the code, I guess): https://github.com/sigp/lighthouse/blob/9cb87e93ce239bbe06a9d01e92126f978356dd13/book/src/validator-management.md ## API Changes This change should be transparent for *most* existing users. If the `valdiator_definitions.yml` doesn't exist then it will be automatically generated using a method that will detect all the validators in their `validators_dir`. Users will have issues if they are: 1. Using `--strict`. 1. Have keystores in their `~/.lighthouse/validators` directory that weren't being detected by the current keystore discovery method. For users with (1), the VC will refuse to start because the `--strict` flag has been removed. They will be forced to review `--help` and choose an equivalent flag. For users with (2), this seems fairly unlikely and since we're only in testnets there's no *real* value on the line here. I'm happy to take the risk, it would be a different case for mainnet. ## Additional Info This PR adds functionality we will need for #1347. ## TODO - [x] Reconsider flags - [x] Move doc into a more reasonable chapter. - [x] Check for compile warnings. --- Cargo.lock | 16 + Cargo.toml | 1 + account_manager/Cargo.toml | 1 + account_manager/src/common.rs | 71 --- account_manager/src/validator/create.rs | 6 +- account_manager/src/wallet/create.rs | 6 +- book/src/SUMMARY.md | 1 + book/src/validator-management.md | 187 ++++++++ common/account_utils/Cargo.toml | 15 + common/account_utils/src/lib.rs | 151 +++++++ testing/simulator/src/eth1_sim.rs | 2 +- testing/simulator/src/no_eth1_sim.rs | 2 +- validator_client/Cargo.toml | 3 + validator_client/src/cli.rs | 16 +- validator_client/src/config.rs | 21 +- .../src/initialized_validators.rs | 425 ++++++++++++++++++ validator_client/src/lib.rs | 54 +-- validator_client/src/validator_definitions.rs | 244 ++++++++++ validator_client/src/validator_store.rs | 48 +- 19 files changed, 1117 insertions(+), 153 deletions(-) create mode 100644 book/src/validator-management.md create mode 100644 common/account_utils/Cargo.toml create mode 100644 common/account_utils/src/lib.rs create mode 100644 validator_client/src/initialized_validators.rs create mode 100644 validator_client/src/validator_definitions.rs diff --git a/Cargo.lock b/Cargo.lock index 5233ceb4b..f61f5f18a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,6 +4,7 @@ name = "account_manager" version = "0.0.1" dependencies = [ + "account_utils", "bls", "clap", "clap_utils", @@ -30,6 +31,18 @@ dependencies = [ "web3", ] +[[package]] +name = "account_utils" +version = "0.1.0" +dependencies = [ + "eth2_keystore", + "eth2_wallet", + "rand 0.7.3", + "serde", + "serde_derive", + "zeroize", +] + [[package]] name = "addr2line" version = "0.13.0" @@ -5980,6 +5993,7 @@ dependencies = [ name = "validator_client" version = "0.1.2" dependencies = [ + "account_utils", "bls", "clap", "clap_utils", @@ -5988,6 +6002,7 @@ dependencies = [ "environment", "eth2_config", "eth2_interop_keypairs", + "eth2_keystore", "eth2_ssz", "eth2_ssz_derive", "exit-future", @@ -6002,6 +6017,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serde_yaml", "slashing_protection", "slog", "slog-async", diff --git a/Cargo.toml b/Cargo.toml index b86aa723b..df882251f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "boot_node", + "common/account_utils", "common/clap_utils", "common/compare_fields", "common/compare_fields_derive", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 877a80dd4..f890ff4db 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -29,3 +29,4 @@ rand = "0.7.2" validator_dir = { path = "../common/validator_dir", features = ["unencrypted_keys"] } tokio = { version = "0.2.21", features = ["full"] } eth2_keystore = { path = "../crypto/eth2_keystore" } +account_utils = { path = "../common/account_utils" } diff --git a/account_manager/src/common.rs b/account_manager/src/common.rs index 195f8c5d1..d2339c2d6 100644 --- a/account_manager/src/common.rs +++ b/account_manager/src/common.rs @@ -1,25 +1,7 @@ use clap::ArgMatches; -use eth2_wallet::PlainText; -use rand::{distributions::Alphanumeric, Rng}; use std::fs::create_dir_all; use std::path::{Path, PathBuf}; -/// The `Alphanumeric` crate only generates a-z, A-Z, 0-9, therefore it has a range of 62 -/// characters. -/// -/// 62**48 is greater than 255**32, therefore this password has more bits of entropy than a byte -/// array of length 32. -const DEFAULT_PASSWORD_LEN: usize = 48; - -pub fn random_password() -> PlainText { - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(DEFAULT_PASSWORD_LEN) - .collect::() - .into_bytes() - .into() -} - pub fn ensure_dir_exists>(path: P) -> Result<(), String> { let path = path.as_ref(); @@ -37,56 +19,3 @@ pub fn base_wallet_dir(matches: &ArgMatches, arg: &'static str) -> Result) -> Vec { - let mut strip_off = 0; - for (i, byte) in bytes.iter().rev().enumerate() { - if *byte == b'\n' || *byte == b'\r' { - strip_off = i + 1; - } else { - break; - } - } - bytes.truncate(bytes.len() - strip_off); - bytes -} - -#[cfg(test)] -mod test { - use super::strip_off_newlines; - - #[test] - fn test_strip_off() { - let expected = "hello world".as_bytes().to_vec(); - - assert_eq!( - strip_off_newlines("hello world\n".as_bytes().to_vec()), - expected - ); - assert_eq!( - strip_off_newlines("hello world\n\n\n\n".as_bytes().to_vec()), - expected - ); - assert_eq!( - strip_off_newlines("hello world\r".as_bytes().to_vec()), - expected - ); - assert_eq!( - strip_off_newlines("hello world\r\r\r\r\r".as_bytes().to_vec()), - expected - ); - assert_eq!( - strip_off_newlines("hello world\r\n".as_bytes().to_vec()), - expected - ); - assert_eq!( - strip_off_newlines("hello world\r\n\r\n".as_bytes().to_vec()), - expected - ); - assert_eq!( - strip_off_newlines("hello world".as_bytes().to_vec()), - expected - ); - } -} diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index d3379e583..dfb9f371b 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,7 +1,5 @@ -use crate::{ - common::{ensure_dir_exists, random_password, strip_off_newlines}, - SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG, -}; +use crate::{common::ensure_dir_exists, SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG}; +use account_utils::{random_password, strip_off_newlines}; use clap::{App, Arg, ArgMatches}; use environment::Environment; use eth2_wallet::PlainText; diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index 53bfc5693..904f651dd 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -1,7 +1,5 @@ -use crate::{ - common::{random_password, strip_off_newlines}, - BASE_DIR_FLAG, -}; +use crate::BASE_DIR_FLAG; +use account_utils::{random_password, strip_off_newlines}; use clap::{App, Arg, ArgMatches}; use eth2_wallet::{ bip39::{Language, Mnemonic, MnemonicType}, diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 90e8eef41..1fc483e11 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -10,6 +10,7 @@ * [Key Management](./key-managment.md) * [Create a wallet](./wallet-create.md) * [Create a validator](./validator-create.md) +* [Validator Management](./validator-management.md) * [Local Testnets](./local-testnets.md) * [API](./api.md) * [HTTP (RESTful JSON)](./http.md) diff --git a/book/src/validator-management.md b/book/src/validator-management.md new file mode 100644 index 000000000..fbb76c9b4 --- /dev/null +++ b/book/src/validator-management.md @@ -0,0 +1,187 @@ +# Validator Management + +The `lighthouse vc` command starts a *validator client* instance which connects +to a beacon node performs the duties of a staked validator. + +This document provides information on how the validator client discovers the +validators it will act for and how it should obtain their cryptographic +signatures. + +Users that create validators using the `lighthouse account` tool in the +standard directories and do not start their `lighthouse vc` with the +`--disable-auto-discover` flag should not need to understand the contents of +this document. However, users with more complex needs may find this document +useful. + +## Introducing the `validator_definitions.yml` file + +The `validator_definitions.yml` file is located in the `validator-dir`, which +defaults to `~/.lighthouse/validators`. It is a +[YAML](https://en.wikipedia.org/wiki/YAML) encoded file defining exactly which +validators the validator client will (and won't) act for. + +### Example + +Here's an example file with two validators: + +```yaml +--- +- enabled: true + voting_public_key: "0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007/voting-keystore.json + voting_keystore_password_path: /home/paul/.lighthouse/secrets/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007 +- enabled: false + voting_public_key: "0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477/voting-keystore.json + voting_keystore_password: myStrongpa55word123&$ +``` +In this example we can see two validators: + +- A validator identified by the `0x87a5...` public key which is enabled. +- Another validator identified by the `0x0xa556...` public key which is **not** enabled. + +### Fields + +Each permitted field of the file is listed below for reference: + +- `enabled`: A `true`/`false` indicating if the validator client should consider this + validator "enabled". +- `voting_public_key`: A validator public key. +- `type`: How the validator signs messages (currently restricted to `local_keystore`). +- `voting_keystore_path`: The path to a EIP-2335 keystore. +- `voting_keystore_password_path`: The path to the password for the EIP-2335 keystore. +- `voting_keystore_password`: The password to the EIP-2335 keystore. + +> **Note**: Either `voting_keystore_password_path` or `voting_keystore_password` *must* be +> supplied. If both are supplied, `voting_keystore_password_path` is ignored. + +## Populating the `validator_definitions.yml` file + +When validator client starts and the `validator_definitions.yml` file doesn't +exist, a new file will be created. If the `--disable-auto-discover` flag is +provided, the new file will be empty and the validator client will not start +any validators. If the `--disable-auto-discover` flag is **not** provided, an +*automatic validator discovery* routine will start (more on that later). To +recap: + +- `lighthouse vc`: validators are automatically discovered. +- `lighthouse vc --disable-auto-discover`: validators are **not** automatically discovered. + +### Automatic validator discovery + +When the `--disable-auto-discover` flag is **not** provided, the validator will search the +`validator-dir` for validators and add any *new* validators to the +`validator_definitions.yml` with `enabled: true`. + +The routine for this search begins in the `validator-dir`, where it obtains a +list of all files in that directory and all sub-directories (i.e., recursive +directory-tree search). For each file named `voting-keystore.json` it creates a +new validator definition by the following process: + +1. Set `enabled` to `true`. +1. Set `voting_public_key` to the `pubkey` value from the `voting-keystore.json`. +1. Set `type` to `local_keystore`. +1. Set `voting_keystore_path` to the full path of the discovered keystore. +1. Set `voting_keystore_password_path` to be a file in the `secrets-dir` with a +name identical to the `voting_public_key` value. + +#### Discovery Example + +Lets assume the following directory structure: + +``` +~/.lighthouse/validators +├── john +│   └── voting-keystore.json +├── sally +│   ├── one +│   │   └── voting-keystore.json +│   ├── three +│   │   └── my-voting-keystore.json +│   └── two +│   └── voting-keystore.json +└── slashing_protection.sqlite +``` + +There is no `validator_definitions.yml` file present, so we can run `lighthouse +vc` (**without** `--disable-auto-discover`) and it will create the following `validator_definitions.yml`: + +```yaml +--- +- enabled: true + voting_public_key: "0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/sally/one/voting-keystore.json + voting_keystore_password_path: /home/paul/.lighthouse/secrets/0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477 +- enabled: true + voting_public_key: "0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/sally/two/voting-keystore.json + voting_keystore_password_path: /home/paul/.lighthouse/secrets/0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337 +- enabled: true + voting_public_key: "0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007" + type: local_keystore + voting_keystore_path: /home/paul/.lighthouse/validators/john/voting-keystore.json + voting_keystore_password_path: /home/paul/.lighthouse/secrets/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007 +``` + +All `voting-keystore.json` files have been detected and added to the file. +Notably, the `sally/three/my-voting-keystore.json` file was *not* added to the +file, since the file name is not exactly `voting-keystore.json`. + +In order for the validator client to decrypt the validators, they will need to +ensure their `secrets-dir` is organised as below: + +``` +~/.lighthouse/secrets +├── 0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477 +├── 0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337 +└── 0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007 +``` + + +### Manual configuration + +The automatic validator discovery process works out-of-the-box with validators +that are created using the `lighthouse account validator new` command. The +details of this process are only interesting to those who are using keystores +generated with another tool or have a non-standard requirements. + +If you are one of these users, manually edit the `validator_definitions.yml` +file to suit your requirements. If the file is poorly formatted or any one of +the validators is unable to be initialized, the validator client will refuse to +start. + +## How the `validator_definitions.yml` file is processed + +If a validator client were to start using the [first example +`validator_definitions.yml` file](#example) it would print the following log, +acknowledging there there are two validators and one is disabled: + +``` +INFO Initialized validators enabled: 1, disabled: 1 +``` + +The validator client will simply ignore the disabled validator. However, for +the active validator, the validator client will: + +1. Load an EIP-2335 keystore from the `voting_keystore_path`. +1. If the `voting_keystore_password` field is present, use it as the keystore + password. Otherwise, attempt to read the file at + `voting_keystore_password_path` and use the contents as the keystore + password. +1. Use the keystore password to decrypt the keystore and obtain a BLS keypair. +1. Verify that the decrypted BLS keypair matches the `voting_public_key`. +1. Create a `voting-keystore.json.lock` file adjacent to the +`voting_keystore_path`, indicating that the voting keystore is in-use and +should not be opened by another process. +1. Proceed to act for that validator, creating blocks and attestations if/when required. + +If there is an error during any of these steps (e.g., a file is missing or +corrupt) the validator client will log an error and continue to attempt to +process other validators. + +When the validator client exits (or the validator is deactivated) it will +remove the `voting-keystore.json.lock` to indicate that the keystore is free for use again. diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml new file mode 100644 index 000000000..4a9fa0fb4 --- /dev/null +++ b/common/account_utils/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "account_utils" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.7.2" +eth2_wallet = { path = "../../crypto/eth2_wallet" } +eth2_keystore = { path = "../../crypto/eth2_keystore" } +zeroize = { version = "1.0.0", features = ["zeroize_derive"] } +serde = "1.0.110" +serde_derive = "1.0.110" diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs new file mode 100644 index 000000000..351db3305 --- /dev/null +++ b/common/account_utils/src/lib.rs @@ -0,0 +1,151 @@ +//! Provides functions that are used for key/account management across multiple crates in the +//! Lighthouse project. + +use eth2_keystore::Keystore; +use eth2_wallet::Wallet; +use rand::{distributions::Alphanumeric, Rng}; +use serde_derive::{Deserialize, Serialize}; +use std::fs::{self, File}; +use std::io; +use std::io::prelude::*; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use zeroize::Zeroize; + +pub use eth2_wallet::PlainText; + +/// The `Alphanumeric` crate only generates a-z, A-Z, 0-9, therefore it has a range of 62 +/// characters. +/// +/// 62**48 is greater than 255**32, therefore this password has more bits of entropy than a byte +/// array of length 32. +const DEFAULT_PASSWORD_LEN: usize = 48; + +/// Returns the "default" path where a wallet should store its password file. +pub fn default_wallet_password_path>(wallet_name: &str, secrets_dir: P) -> PathBuf { + secrets_dir.as_ref().join(format!("{}.pass", wallet_name)) +} + +/// Returns a password for a wallet, where that password is loaded from the "default" path. +pub fn default_wallet_password>( + wallet: &Wallet, + secrets_dir: P, +) -> Result { + let path = default_wallet_password_path(wallet.name(), secrets_dir); + fs::read(path).map(|bytes| PlainText::from(strip_off_newlines(bytes))) +} + +/// Returns the "default" path where a keystore should store its password file. +pub fn default_keystore_password_path>( + keystore: &Keystore, + secrets_dir: P, +) -> PathBuf { + secrets_dir + .as_ref() + .join(format!("0x{}", keystore.pubkey())) +} + +/// Reads a password file into a Zeroize-ing `PlainText` struct, with new-lines removed. +pub fn read_password>(path: P) -> Result { + fs::read(path).map(strip_off_newlines).map(Into::into) +} + +/// Creates a file with `600 (-rw-------)` permissions. +pub fn create_with_600_perms>(path: P, bytes: &[u8]) -> Result<(), io::Error> { + let path = path.as_ref(); + + let mut file = File::create(&path)?; + + let mut perm = file.metadata()?.permissions(); + + perm.set_mode(0o600); + + file.set_permissions(perm)?; + + file.write_all(bytes)?; + + Ok(()) +} + +/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN`. +pub fn random_password() -> PlainText { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(DEFAULT_PASSWORD_LEN) + .collect::() + .into_bytes() + .into() +} + +/// Remove any number of newline or carriage returns from the end of a vector of bytes. +pub fn strip_off_newlines(mut bytes: Vec) -> Vec { + let mut strip_off = 0; + for (i, byte) in bytes.iter().rev().enumerate() { + if *byte == b'\n' || *byte == b'\r' { + strip_off = i + 1; + } else { + break; + } + } + bytes.truncate(bytes.len() - strip_off); + bytes +} + +/// 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)] +#[zeroize(drop)] +#[serde(transparent)] +pub struct ZeroizeString(String); + +impl From for ZeroizeString { + fn from(s: String) -> Self { + Self(s) + } +} + +impl AsRef<[u8]> for ZeroizeString { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[cfg(test)] +mod test { + use super::strip_off_newlines; + + #[test] + fn test_strip_off() { + let expected = "hello world".as_bytes().to_vec(); + + assert_eq!( + strip_off_newlines("hello world\n".as_bytes().to_vec()), + expected + ); + assert_eq!( + strip_off_newlines("hello world\n\n\n\n".as_bytes().to_vec()), + expected + ); + assert_eq!( + strip_off_newlines("hello world\r".as_bytes().to_vec()), + expected + ); + assert_eq!( + strip_off_newlines("hello world\r\r\r\r\r".as_bytes().to_vec()), + expected + ); + assert_eq!( + strip_off_newlines("hello world\r\n".as_bytes().to_vec()), + expected + ); + assert_eq!( + strip_off_newlines("hello world\r\n\r\n".as_bytes().to_vec()), + expected + ); + assert_eq!( + strip_off_newlines("hello world".as_bytes().to_vec()), + expected + ); + } +} diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/eth1_sim.rs index c8f9ee314..18b846a11 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/eth1_sim.rs @@ -130,7 +130,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { network .add_validator_client( ValidatorConfig { - auto_register: true, + disable_auto_discover: false, ..ValidatorConfig::default() }, i, diff --git a/testing/simulator/src/no_eth1_sim.rs b/testing/simulator/src/no_eth1_sim.rs index e89fac7c4..910c20267 100644 --- a/testing/simulator/src/no_eth1_sim.rs +++ b/testing/simulator/src/no_eth1_sim.rs @@ -104,7 +104,7 @@ pub fn run_no_eth1_sim(matches: &ArgMatches) -> Result<(), String> { network .add_validator_client( ValidatorConfig { - auto_register: true, + disable_auto_discover: false, ..ValidatorConfig::default() }, i, diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 83f775208..2bf00aec4 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -24,6 +24,7 @@ types = { path = "../consensus/types" } serde = "1.0.110" serde_derive = "1.0.110" serde_json = "1.0.52" +serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } slog-async = "2.5.0" slog-term = "2.5.0" @@ -44,3 +45,5 @@ tempdir = "0.3.7" rayon = "1.3.0" validator_dir = { path = "../common/validator_dir" } clap_utils = { path = "../common/clap_utils" } +eth2_keystore = { path = "../crypto/eth2_keystore" } +account_utils = { path = "../common/account_utils" } diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 7684e9616..6b20d9780 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -37,11 +37,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { nodes using the same key. Automatically enabled unless `--strict` is specified", )) .arg( - Arg::with_name("strict") - .long("strict") + Arg::with_name("strict-lockfiles") + .long("strict-lockfiles") .help( - "If present, require that validator keypairs are unlocked and that auto-register \ - is explicit before new validators are allowed to be used." + "If present, do not load validators that have are guarded by a lockfile. Note: for \ + Eth2 mainnet, this flag will likely be removed and its behaviour will become default." + ) + ) + .arg( + Arg::with_name("disable-auto-discover") + .long("disable-auto-discover") + .help( + "If present, do not attempt to discover new validators in the validators-dir. Validators \ + will need to be manually added to the validator_definitions.yml file." ) ) .arg( diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 75ec02a7f..55bbc8b4a 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -23,10 +23,10 @@ pub struct Config { /// If true, the validator client will still poll for duties and produce blocks even if the /// beacon node is not synced at startup. pub allow_unsynced_beacon_node: bool, - /// If true, we will be strict about concurrency and validator registration. - pub strict: bool, - /// If true, register new validator keys with the slashing protection database. - pub auto_register: bool, + /// If true, refuse to unlock a keypair that is guarded by a lockfile. + pub strict_lockfiles: bool, + /// If true, don't scan the validators dir for new keystores. + pub disable_auto_discover: bool, } impl Default for Config { @@ -43,8 +43,8 @@ impl Default for Config { secrets_dir, http_server: DEFAULT_HTTP_SERVER.to_string(), allow_unsynced_beacon_node: false, - auto_register: false, - strict: false, + strict_lockfiles: false, + disable_auto_discover: false, } } } @@ -73,13 +73,8 @@ impl Config { } config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced"); - config.auto_register = cli_args.is_present("auto-register"); - config.strict = cli_args.is_present("strict"); - - if !config.strict { - // Do not require an explicit `--auto-register` if `--strict` is disabled. - config.auto_register = true - } + config.strict_lockfiles = cli_args.is_present("strict-lockfiles"); + config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); if let Some(secrets_dir) = parse_optional(cli_args, "secrets-dir")? { config.secrets_dir = secrets_dir; diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs new file mode 100644 index 000000000..28f1ecbc3 --- /dev/null +++ b/validator_client/src/initialized_validators.rs @@ -0,0 +1,425 @@ +//! Provides management of "initialized" validators. +//! +//! A validator is "initialized" if it is ready for signing blocks, attestations, etc in this +//! validator client. +//! +//! 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, 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::path::PathBuf; +use types::{Keypair, PublicKey}; + +#[derive(Debug)] +pub enum Error { + /// Refused to open a validator with an existing lockfile since that validator may be in-use by + /// another process. + LockfileExists(PathBuf), + /// There was a filesystem error when creating the lockfile. + UnableToCreateLockfile(io::Error), + /// The voting public key in the definition did not match the one in the keystore. + VotingPublicKeyMismatch { + definition: Box, + keystore: Box, + }, + /// There was a filesystem error when opening the keystore. + UnableToOpenVotingKeystore(io::Error), + /// The keystore path is not as expected. It should be a file, not `..` or something obscure + /// like that. + BadVotingKeystorePath(PathBuf), + /// The keystore could not be parsed, it is likely bad JSON. + UnableToParseVotingKeystore(eth2_keystore::Error), + /// The keystore could not be decrypted. The password might be wrong. + UnableToDecryptKeystore(eth2_keystore::Error), + /// There was a filesystem error when reading the keystore password from disk. + UnableToReadVotingKeystorePassword(io::Error), + /// There was an error updating the on-disk validator definitions file. + UnableToSaveDefinitions(validator_definitions::Error), + /// It is not legal to try and initialize a disabled validator definition. + 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), +} + +/// A method used by a validator to sign messages. +/// +/// Presently there is only a single variant, however we expect more variants to arise (e.g., +/// remote signing). +pub enum SigningMethod { + /// A validator that is defined by an EIP-2335 keystore on the local filesystem. + LocalKeystore { + voting_keystore_path: PathBuf, + voting_keystore_lockfile_path: PathBuf, + voting_keystore: Keystore, + voting_keypair: Keypair, + }, +} + +/// A validator that is ready to sign messages. +pub struct InitializedValidator { + signing_method: SigningMethod, +} + +impl InitializedValidator { + /// Instantiate `self` from a `ValidatorDefinition`. + /// + /// If `stdin.is_some()` any missing passwords will result in a prompt requesting input on + /// stdin (prompts published to stderr). + /// + /// ## Errors + /// + /// If the validator is unable to be initialized for whatever reason. + pub fn from_definition( + def: ValidatorDefinition, + strict_lockfiles: bool, + stdin: Option<&Stdin>, + log: &Logger, + ) -> Result { + if !def.enabled { + return Err(Error::UnableToInitializeDisabledValidator); + } + + match def.signing_definition { + // Load the keystore, password, decrypt the keypair and create a lockfile for a + // EIP-2335 keystore on the local filesystem. + SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password_path, + voting_keystore_password, + } => { + let keystore_file = + File::open(&voting_keystore_path).map_err(Error::UnableToOpenVotingKeystore)?; + let voting_keystore = Keystore::from_json_reader(keystore_file) + .map_err(Error::UnableToParseVotingKeystore)?; + + let voting_keypair = match (voting_keystore_password_path, voting_keystore_password) + { + // If the password is supplied, use it and ignore the path (if supplied). + (_, Some(password)) => voting_keystore + .decrypt_keypair(password.as_ref()) + .map_err(Error::UnableToDecryptKeystore)?, + // If only the path is supplied, use the path. + (Some(path), None) => { + let password = read_password(path) + .map_err(Error::UnableToReadVotingKeystorePassword)?; + + voting_keystore + .decrypt_keypair(password.as_bytes()) + .map_err(Error::UnableToDecryptKeystore)? + } + // 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)); + } + } + }; + + if voting_keypair.pk != def.voting_public_key { + return Err(Error::VotingPublicKeyMismatch { + definition: Box::new(def.voting_public_key), + keystore: Box::new(voting_keypair.pk), + }); + } + + // Append a `.lock` suffix to the voting keystore. + let voting_keystore_lockfile_path = voting_keystore_path + .file_name() + .ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone())) + .and_then(|os_str| { + os_str.to_str().ok_or_else(|| { + Error::BadVotingKeystorePath(voting_keystore_path.clone()) + }) + }) + .map(|filename| { + voting_keystore_path + .clone() + .with_file_name(format!("{}.lock", filename)) + })?; + + if voting_keystore_lockfile_path.exists() { + if strict_lockfiles { + return Err(Error::LockfileExists(voting_keystore_lockfile_path)); + } else { + // If **not** respecting lockfiles, just raise a warning if the voting + // keypair cannot be unlocked. + warn!( + log, + "Ignoring validator lockfile"; + "file" => format!("{:?}", voting_keystore_lockfile_path) + ); + } + } else { + // Create a new lockfile. + OpenOptions::new() + .write(true) + .create_new(true) + .open(&voting_keystore_lockfile_path) + .map_err(Error::UnableToCreateLockfile)?; + } + + Ok(Self { + signing_method: SigningMethod::LocalKeystore { + voting_keystore_path, + voting_keystore_lockfile_path, + voting_keystore, + voting_keypair, + }, + }) + } + } + } + + /// Returns the voting public key for this validator. + pub fn voting_public_key(&self) -> &PublicKey { + match &self.signing_method { + SigningMethod::LocalKeystore { voting_keypair, .. } => &voting_keypair.pk, + } + } + + /// Returns the voting keypair for this validator. + pub fn voting_keypair(&self) -> &Keypair { + match &self.signing_method { + SigningMethod::LocalKeystore { voting_keypair, .. } => voting_keypair, + } + } +} + +/// Custom drop implementation to allow for `LocalKeystore` to remove lockfiles. +impl Drop for InitializedValidator { + fn drop(&mut self) { + match &self.signing_method { + SigningMethod::LocalKeystore { + voting_keystore_lockfile_path, + .. + } => { + if voting_keystore_lockfile_path.exists() { + if let Err(e) = fs::remove_file(&voting_keystore_lockfile_path) { + eprintln!( + "Failed to remove {:?}: {:?}", + voting_keystore_lockfile_path, e + ) + } + } else { + eprintln!("Lockfile missing: {:?}", voting_keystore_lockfile_path) + } + } + } + } +} + +/// 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 { + eprintln!(""); + eprintln!( + "The {} file does not contain either of the following fields for {:?}:", + CONFIG_FILENAME, keystore_path + ); + eprintln!(""); + eprintln!(" - voting_keystore_password"); + eprintln!(" - voting_keystore_password_path"); + eprintln!(""); + eprintln!( + "You may exit and update {} or enter a password. \ + If you choose to enter a password now then this prompt \ + will be raised next time the validator is started.", + CONFIG_FILENAME + ); + eprintln!(""); + 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)?; + + eprintln!(""); + + match keystore.decrypt_keypair(password.as_ref()) { + Ok(keystore) => break Ok(keystore), + Err(eth2_keystore::Error::InvalidPassword) => { + eprintln!("Invalid password, try again (or press Ctrl+c to exit):"); + } + Err(e) => return Err(Error::UnableToDecryptKeystore(e)), + } + } +} + +/// A set of `InitializedValidator` objects which is initialized from a list of +/// `ValidatorDefinition`. The `ValidatorDefinition` file is maintained as `self` is modified. +/// +/// Forms the fundamental list of validators that are managed by this validator client instance. +pub struct InitializedValidators { + /// If `true`, no validator will be opened if a lockfile exists. If `false`, a warning will be + /// raised for an existing lockfile, but it will ultimately be ignored. + strict_lockfiles: bool, + /// A list of validator definitions which can be stored on-disk. + definitions: ValidatorDefinitions, + /// The directory that the `self.definitions` will be saved into. + validators_dir: PathBuf, + /// The canonical set of validators. + validators: HashMap, + /// For logging via `slog`. + log: Logger, +} + +impl InitializedValidators { + /// Instantiates `Self`, initializing all validators in `definitions`. + pub fn from_definitions( + definitions: ValidatorDefinitions, + validators_dir: PathBuf, + strict_lockfiles: bool, + log: Logger, + ) -> Result { + let mut this = Self { + strict_lockfiles, + validators_dir, + definitions, + validators: HashMap::default(), + log, + }; + this.update_validators()?; + Ok(this) + } + + /// The count of enabled validators contained in `self`. + pub fn num_enabled(&self) -> usize { + self.validators.len() + } + + /// The total count of enabled and disabled validators contained in `self`. + pub fn num_total(&self) -> usize { + self.definitions.as_slice().len() + } + + /// Iterate through all **enabled** voting public keys in `self`. + pub fn iter_voting_pubkeys(&self) -> impl Iterator { + self.validators.iter().map(|(pubkey, _)| pubkey) + } + + /// Returns the voting `Keypair` for a given voting `PublicKey`, if that validator is known to + /// `self` **and** the validator is enabled. + pub fn voting_keypair(&self, voting_public_key: &PublicKey) -> Option<&Keypair> { + self.validators + .get(voting_public_key) + .map(|v| v.voting_keypair()) + } + + /// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values. + /// + /// ## Notes + /// + /// Enabling or disabling a validator will cause `self.definitions` to be updated and saved to + /// disk. A newly enabled validator will be added to `self.validators`, whilst a newly disabled + /// validator will be removed from `self.validators`. + /// + /// Saves the `ValidatorDefinitions` to file, even if no definitions were changed. + pub fn set_validator_status( + &mut self, + voting_public_key: &PublicKey, + enabled: bool, + ) -> Result<(), Error> { + self.definitions + .as_mut_slice() + .iter_mut() + .find(|def| def.voting_public_key == *voting_public_key) + .map(|def| def.enabled = enabled); + + self.update_validators()?; + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(()) + } + + /// Scans `self.definitions` and attempts to initialize and validators which are not already + /// initialized. + /// + /// The function exits early with an error if any enabled validator is unable to be + /// initialized. + /// + /// ## Notes + /// + /// A validator is considered "already known" and skipped if the public key is already known. + /// 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 { + SigningDefinition::LocalKeystore { .. } => { + if self.validators.contains_key(&def.voting_public_key) { + continue; + } + + match InitializedValidator::from_definition( + def.clone(), + self.strict_lockfiles, + Some(&stdin), + &self.log, + ) { + Ok(init) => { + self.validators + .insert(init.voting_public_key().clone(), init); + info!( + self.log, + "Enabled validator"; + "voting_pubkey" => format!("{:?}", def.voting_public_key) + ); + } + Err(e) => { + error!( + self.log, + "Failed to initialize validator"; + "error" => format!("{:?}", e), + "validator" => format!("{:?}", def.voting_public_key) + ); + + // Exit on an invalid validator. + return Err(e); + } + } + } + } + } else { + self.validators.remove(&def.voting_public_key); + info!( + self.log, + "Disabled validator"; + "voting_pubkey" => format!("{:?}", def.voting_public_key) + ); + } + } + Ok(()) + } +} diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 629732cee..0efeba769 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -4,8 +4,10 @@ mod cli; mod config; mod duties_service; mod fork_service; +mod initialized_validators; mod is_synced; mod notifier; +mod validator_definitions; mod validator_store; pub use cli::cli_app; @@ -14,20 +16,20 @@ pub use config::Config; use attestation_service::{AttestationService, AttestationServiceBuilder}; use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; -use config::SLASHING_PROTECTION_FILENAME; use duties_service::{DutiesService, DutiesServiceBuilder}; use environment::RuntimeContext; use fork_service::{ForkService, ForkServiceBuilder}; use futures::channel::mpsc; +use initialized_validators::InitializedValidators; use notifier::spawn_notifier; use remote_beacon_node::RemoteBeaconNode; -use slog::{error, info, warn, Logger}; +use slog::{error, info, Logger}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{delay_for, Duration}; use types::EthSpec; -use validator_dir::Manager as ValidatorManager; +use validator_definitions::ValidatorDefinitions; use validator_store::ValidatorStore; /// The interval between attempts to contact the beacon node during startup. @@ -69,30 +71,36 @@ impl ProductionValidatorClient { "datadir" => format!("{:?}", config.data_dir), ); - if !config.data_dir.join(SLASHING_PROTECTION_FILENAME).exists() && !config.auto_register { - warn!( + let mut validator_defs = ValidatorDefinitions::open_or_create(&config.data_dir) + .map_err(|e| format!("Unable to open or create validator definitions: {:?}", e))?; + + if !config.disable_auto_discover { + let new_validators = validator_defs + .discover_local_keystores(&config.data_dir, &config.secrets_dir, &log) + .map_err(|e| format!("Unable to discover local validator keystores: {:?}", e))?; + validator_defs + .save(&config.data_dir) + .map_err(|e| format!("Unable to update validator definitions: {:?}", e))?; + info!( log, - "Will not register any validators"; - "msg" => "strongly consider using --auto-register on the first use", + "Completed validator discovery"; + "new_validators" => new_validators, ); } - let validator_manager = ValidatorManager::open(&config.data_dir) - .map_err(|e| format!("unable to read data_dir: {:?}", e))?; - - let validators_result = if config.strict { - validator_manager.decrypt_all_validators(config.secrets_dir.clone(), Some(&log)) - } else { - validator_manager.force_decrypt_all_validators(config.secrets_dir.clone(), Some(&log)) - }; - - let validators = validators_result - .map_err(|e| format!("unable to decrypt all validator directories: {:?}", e))?; + let validators = InitializedValidators::from_definitions( + validator_defs, + config.data_dir.clone(), + config.strict_lockfiles, + log.clone(), + ) + .map_err(|e| format!("Unable to initialize validators: {:?}", e))?; info!( log, - "Decrypted validator keystores"; - "count" => validators.len(), + "Initialized validators"; + "disabled" => validators.num_total().saturating_sub(validators.num_enabled()), + "enabled" => validators.num_enabled(), ); let beacon_node = @@ -194,11 +202,7 @@ impl ProductionValidatorClient { "voting_validators" => validator_store.num_voting_validators() ); - if config.auto_register { - info!(log, "Registering all validators for slashing protection"); - validator_store.register_all_validators_for_slashing_protection()?; - info!(log, "Validator auto-registration complete"); - } + validator_store.register_all_validators_for_slashing_protection()?; let duties_service = DutiesServiceBuilder::new() .slot_clock(slot_clock.clone()) diff --git a/validator_client/src/validator_definitions.rs b/validator_client/src/validator_definitions.rs new file mode 100644 index 000000000..6b23b7d18 --- /dev/null +++ b/validator_client/src/validator_definitions.rs @@ -0,0 +1,244 @@ +//! 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. + +use account_utils::{create_with_600_perms, default_keystore_password_path, ZeroizeString}; +use eth2_keystore::Keystore; +use serde_derive::{Deserialize, Serialize}; +use serde_yaml; +use slog::{error, Logger}; +use std::collections::HashSet; +use std::fs::{self, OpenOptions}; +use std::io; +use std::iter::FromIterator; +use std::path::{Path, PathBuf}; +use types::PublicKey; +use validator_dir::VOTING_KEYSTORE_FILE; + +/// The file name for the serialized `ValidatorDefinitions` struct. +pub const CONFIG_FILENAME: &str = "validator_definitions.yml"; + +#[derive(Debug)] +pub enum Error { + /// The config file could not be opened. + UnableToOpenFile(io::Error), + /// The config file could not be parsed as YAML. + UnableToParseFile(serde_yaml::Error), + /// There was an error whilst performing the recursive keystore search function. + UnableToSearchForKeystores(io::Error), + /// The config file could not be serialized as YAML. + UnableToEncodeFile(serde_yaml::Error), + /// The config file could not be written to the filesystem. + UnableToWriteFile(io::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)] +#[serde(tag = "type")] +pub enum SigningDefinition { + /// A validator that is defined by an EIP-2335 keystore on the local filesystem. + #[serde(rename = "local_keystore")] + LocalKeystore { + voting_keystore_path: PathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + voting_keystore_password_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + voting_keystore_password: Option, + }, +} + +/// A validator that may be initialized by this validator client. +/// +/// Presently there is only a single variant, however we expect more variants to arise (e.g., +/// remote signing). +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorDefinition { + pub enabled: bool, + pub voting_public_key: PublicKey, + #[serde(flatten)] + pub signing_definition: SigningDefinition, +} + +/// 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)] +pub struct ValidatorDefinitions(Vec); + +impl ValidatorDefinitions { + /// Open an existing file or create a new, empty one if it does not exist. + pub fn open_or_create>(validators_dir: P) -> Result { + let config_path = validators_dir.as_ref().join(CONFIG_FILENAME); + if !config_path.exists() { + let this = Self::default(); + this.save(&validators_dir)?; + } + Self::open(validators_dir) + } + + /// Open an existing file, returning an error if the file does not exist. + pub fn open>(validators_dir: P) -> Result { + let config_path = validators_dir.as_ref().join(CONFIG_FILENAME); + let file = OpenOptions::new() + .write(true) + .read(true) + .create_new(false) + .open(&config_path) + .map_err(Error::UnableToOpenFile)?; + serde_yaml::from_reader(file).map_err(Error::UnableToParseFile) + } + + /// Perform a recursive, exhaustive search through `validators_dir` and add any keystores + /// matching the `validator_dir::VOTING_KEYSTORE_FILE` file name. + /// + /// Returns the count of *new* keystores that were added to `self` during this search. + /// + /// ## Notes + /// + /// Determines the path for the password file based upon the scheme defined by + /// `account_utils::default_keystore_password_path`. + /// + /// If a keystore cannot be parsed the function does not exit early. Instead it logs an `error` + /// and continues searching. + pub fn discover_local_keystores>( + &mut self, + validators_dir: P, + secrets_dir: P, + log: &Logger, + ) -> Result { + let mut keystore_paths = vec![]; + recursively_find_voting_keystores(validators_dir, &mut keystore_paths) + .map_err(Error::UnableToSearchForKeystores)?; + + let known_paths: HashSet<&PathBuf> = + HashSet::from_iter(self.0.iter().map(|def| match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => voting_keystore_path, + })); + + let mut new_defs = keystore_paths + .into_iter() + .filter_map(|voting_keystore_path| { + if known_paths.contains(&voting_keystore_path) { + return None; + } + + let keystore_result = OpenOptions::new() + .read(true) + .create(false) + .open(&voting_keystore_path) + .map_err(|e| format!("{:?}", e)) + .and_then(|file| { + Keystore::from_json_reader(file).map_err(|e| format!("{:?}", e)) + }); + + let keystore = match keystore_result { + Ok(keystore) => keystore, + Err(e) => { + error!( + log, + "Unable to read validator keystore"; + "error" => e, + "keystore" => format!("{:?}", voting_keystore_path) + ); + return None; + } + }; + + let voting_keystore_password_path = Some(default_keystore_password_path( + &keystore, + secrets_dir.as_ref(), + )) + .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; + } + }; + + Some(ValidatorDefinition { + enabled: true, + voting_public_key, + signing_definition: SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password_path, + voting_keystore_password: None, + }, + }) + }) + .collect::>(); + + let new_defs_count = new_defs.len(); + + self.0.append(&mut new_defs); + + Ok(new_defs_count) + } + + /// Encodes `self` as a YAML string it writes it to the `CONFIG_FILENAME` file in the + /// `validators_dir` directory. + /// + /// Will create a new file if it does not exist or over-write any existing file. + pub fn save>(&self, validators_dir: P) -> Result<(), Error> { + let config_path = validators_dir.as_ref().join(CONFIG_FILENAME); + let bytes = serde_yaml::to_vec(self).map_err(Error::UnableToEncodeFile)?; + + if config_path.exists() { + fs::write(config_path, &bytes).map_err(Error::UnableToWriteFile) + } else { + create_with_600_perms(&config_path, &bytes).map_err(Error::UnableToWriteFile) + } + } + + /// Returns a slice of all `ValidatorDefinition` in `self`. + pub fn as_slice(&self) -> &[ValidatorDefinition] { + self.0.as_slice() + } + + /// Returns a mutable slice of all `ValidatorDefinition` in `self`. + pub fn as_mut_slice(&mut self) -> &mut [ValidatorDefinition] { + self.0.as_mut_slice() + } +} + +/// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to +/// `matches`. +/// +/// ## Errors +/// +/// Returns with an error immediately if any filesystem error is raised. +pub fn recursively_find_voting_keystores>( + dir: P, + matches: &mut Vec, +) -> Result<(), io::Error> { + fs::read_dir(dir)?.try_for_each(|dir_entry| { + let dir_entry = dir_entry?; + let file_type = dir_entry.file_type()?; + if file_type.is_dir() { + recursively_find_voting_keystores(dir_entry.path(), matches)? + } else if file_type.is_file() { + if dir_entry + .file_name() + .to_str() + .map_or(false, |filename| filename == VOTING_KEYSTORE_FILE) + { + matches.push(dir_entry.path()) + } + } + Ok(()) + }) +} diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 8e93ef584..6b11f9a13 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -1,11 +1,12 @@ -use crate::config::SLASHING_PROTECTION_FILENAME; -use crate::{config::Config, fork_service::ForkService}; +use crate::{ + config::{Config, SLASHING_PROTECTION_FILENAME}, + fork_service::ForkService, + initialized_validators::InitializedValidators, +}; use parking_lot::RwLock; use slashing_protection::{NotSafe, Safe, SlashingDatabase}; use slog::{crit, error, warn, Logger}; use slot_clock::SlotClock; -use std::collections::HashMap; -use std::iter::FromIterator; use std::marker::PhantomData; use std::sync::Arc; use tempdir::TempDir; @@ -42,7 +43,7 @@ impl PartialEq for LocalValidator { #[derive(Clone)] pub struct ValidatorStore { - validators: Arc>>, + validators: Arc>, slashing_protection: SlashingDatabase, genesis_validators_root: Hash256, spec: Arc, @@ -54,7 +55,7 @@ pub struct ValidatorStore { impl ValidatorStore { pub fn new( - validators: Vec<(Keypair, ValidatorDir)>, + validators: InitializedValidators, config: &Config, genesis_validators_root: Hash256, spec: ChainSpec, @@ -70,18 +71,8 @@ impl ValidatorStore { ) })?; - let validator_key_values = validators.into_iter().map(|(kp, dir)| { - ( - kp.pk.clone(), - LocalValidator { - validator_dir: dir, - voting_keypair: kp, - }, - ) - }); - Ok(Self { - validators: Arc::new(RwLock::new(HashMap::from_iter(validator_key_values))), + validators: Arc::new(RwLock::new(validators)), slashing_protection, genesis_validators_root, spec: Arc::new(spec), @@ -98,20 +89,20 @@ impl ValidatorStore { /// such as when relocating validator keys to a new machine. pub fn register_all_validators_for_slashing_protection(&self) -> Result<(), String> { self.slashing_protection - .register_validators(self.validators.read().keys()) + .register_validators(self.validators.read().iter_voting_pubkeys()) .map_err(|e| format!("Error while registering validators: {:?}", e)) } pub fn voting_pubkeys(&self) -> Vec { self.validators .read() - .iter() - .map(|(pubkey, _dir)| pubkey.clone()) + .iter_voting_pubkeys() + .cloned() .collect() } pub fn num_voting_validators(&self) -> usize { - self.validators.read().len() + self.validators.read().num_enabled() } fn fork(&self) -> Option { @@ -128,9 +119,8 @@ impl ValidatorStore { // TODO: check this against the slot clock to make sure it's not an early reveal? self.validators .read() - .get(validator_pubkey) - .and_then(|local_validator| { - let voting_keypair = &local_validator.voting_keypair; + .voting_keypair(validator_pubkey) + .and_then(|voting_keypair| { let domain = self.spec.get_domain( epoch, Domain::Randao, @@ -179,8 +169,7 @@ impl ValidatorStore { // We can safely sign this block. Ok(Safe::Valid) => { let validators = self.validators.read(); - let validator = validators.get(validator_pubkey)?; - let voting_keypair = &validator.voting_keypair; + let voting_keypair = validators.voting_keypair(validator_pubkey)?; Some(block.sign( &voting_keypair.sk, @@ -247,8 +236,7 @@ impl ValidatorStore { // We can safely sign this attestation. Ok(Safe::Valid) => { let validators = self.validators.read(); - let validator = validators.get(validator_pubkey)?; - let voting_keypair = &validator.voting_keypair; + let voting_keypair = validators.voting_keypair(validator_pubkey)?; attestation .sign( @@ -309,7 +297,7 @@ impl ValidatorStore { selection_proof: SelectionProof, ) -> Option> { let validators = self.validators.read(); - let voting_keypair = &validators.get(validator_pubkey)?.voting_keypair; + let voting_keypair = &validators.voting_keypair(validator_pubkey)?; Some(SignedAggregateAndProof::from_aggregate( validator_index, @@ -330,7 +318,7 @@ impl ValidatorStore { slot: Slot, ) -> Option { let validators = self.validators.read(); - let voting_keypair = &validators.get(validator_pubkey)?.voting_keypair; + let voting_keypair = &validators.voting_keypair(validator_pubkey)?; Some(SelectionProof::new::( slot,