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

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");
}
validator_store.register_all_validators_for_slashing_protection()?;
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,