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): 9cb87e93ce/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.
This commit is contained in:
Paul Hauner 2020-07-22 09:34:55 +00:00
parent 393782f632
commit e26da35cbf
19 changed files with 1117 additions and 153 deletions

16
Cargo.lock generated
View File

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

View File

@ -16,6 +16,7 @@ members = [
"boot_node",
"common/account_utils",
"common/clap_utils",
"common/compare_fields",
"common/compare_fields_derive",

View File

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

View File

@ -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::<String>()
.into_bytes()
.into()
}
pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> Result<(), String> {
let path = path.as_ref();
@ -37,56 +19,3 @@ pub fn base_wallet_dir(matches: &ArgMatches, arg: &'static str) -> Result<PathBu
PathBuf::new().join(".lighthouse").join("wallets"),
)
}
/// Remove any number of newline or carriage returns from the end of a vector of bytes.
pub fn strip_off_newlines(mut bytes: Vec<u8>) -> Vec<u8> {
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
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
[package]
name = "account_utils"
version = "0.1.0"
authors = ["Paul Hauner <paul@paulhauner.com>"]
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"

View File

@ -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<P: AsRef<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<P: AsRef<Path>>(
wallet: &Wallet,
secrets_dir: P,
) -> Result<PlainText, io::Error> {
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<P: AsRef<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<P: AsRef<Path>>(path: P) -> Result<PlainText, io::Error> {
fs::read(path).map(strip_off_newlines).map(Into::into)
}
/// Creates a file with `600 (-rw-------)` permissions.
pub fn create_with_600_perms<P: AsRef<Path>>(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::<String>()
.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<u8>) -> Vec<u8> {
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<String> 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
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PublicKey>,
keystore: Box<PublicKey>,
},
/// 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<Self, Error> {
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<Keypair, Error> {
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<PublicKey, InitializedValidator>,
/// 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<Self, Error> {
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<Item = &PublicKey> {
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(())
}
}

View File

@ -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<T: EthSpec> ProductionValidatorClient<T> {
"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<T: EthSpec> ProductionValidatorClient<T> {
"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");
}
let duties_service = DutiesServiceBuilder::new()
.slot_clock(slot_clock.clone())

View File

@ -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<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
voting_keystore_password: Option<ZeroizeString>,
},
}
/// 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<ValidatorDefinition>);
impl ValidatorDefinitions {
/// Open an existing file or create a new, empty one if it does not exist.
pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
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<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
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<P: AsRef<Path>>(
&mut self,
validators_dir: P,
secrets_dir: P,
log: &Logger,
) -> Result<usize, Error> {
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::<Vec<_>>();
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(
dir: P,
matches: &mut Vec<PathBuf>,
) -> 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(())
})
}

View File

@ -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<T, E: EthSpec> {
validators: Arc<RwLock<HashMap<PublicKey, LocalValidator>>>,
validators: Arc<RwLock<InitializedValidators>>,
slashing_protection: SlashingDatabase,
genesis_validators_root: Hash256,
spec: Arc<ChainSpec>,
@ -54,7 +55,7 @@ pub struct ValidatorStore<T, E: EthSpec> {
impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
pub fn new(
validators: Vec<(Keypair, ValidatorDir)>,
validators: InitializedValidators,
config: &Config,
genesis_validators_root: Hash256,
spec: ChainSpec,
@ -70,18 +71,8 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
)
})?;
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<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
/// 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<PublicKey> {
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<Fork> {
@ -128,9 +119,8 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
// 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<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
// 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<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
// 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<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
selection_proof: SelectionProof,
) -> Option<SignedAggregateAndProof<E>> {
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<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
slot: Slot,
) -> Option<SelectionProof> {
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::<E>(
slot,