mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-14 10:22:38 +00:00
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:
@@ -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" }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
425
validator_client/src/initialized_validators.rs
Normal file
425
validator_client/src/initialized_validators.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
244
validator_client/src/validator_definitions.rs
Normal file
244
validator_client/src/validator_definitions.rs
Normal 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(())
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user