Implement key cache to reduce keystore loading times for validator_client (#1695)
## Issue Addressed #1618 ## Proposed Changes Adds an encrypted key cache that is loaded on validator_client startup. It stores the keypairs for all enabled keystores and uses as password the concatenation the passwords of all enabled keystores. This reduces the number of time intensive key derivitions for `N` validators from `N` to `1`. On changes the cache gets updated asynchronously to avoid blocking the main thread. ## Additional Info If the cache contains the keypair of a keystore that is not in the validator_definitions.yml file during loading the cache cannot get decrypted. In this case all the keystores get decrypted and then the cache gets overwritten. To avoid that one can disable keystores in validator_definitions.yml and restart the client which will remove them from the cache, after that one can entirely remove the keystore (from the validator_definitions.yml and from the disk). Other solutions to the above "problem" might be: * Add a CLI and/or API function for removing keystores which will update the cache (asynchronously). * Add a CLI and/or API function that just updates the cache (asynchronously) after a modification of the `validator_definitions.yml` file. Note that the cache file has a lock file which gets removed immediatly after the cache was used or updated.
This commit is contained in:
parent
da44821e39
commit
59adc5ba00
44
Cargo.lock
generated
44
Cargo.lock
generated
@ -474,6 +474,16 @@ dependencies = [
|
||||
"types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "0.9.1"
|
||||
@ -1632,7 +1642,7 @@ dependencies = [
|
||||
"hmac 0.9.0",
|
||||
"pbkdf2 0.5.0",
|
||||
"rand 0.7.3",
|
||||
"scrypt",
|
||||
"scrypt 0.4.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@ -2347,6 +2357,16 @@ dependencies = [
|
||||
"digest 0.8.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840"
|
||||
dependencies = [
|
||||
"crypto-mac 0.8.0",
|
||||
"digest 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.9.0"
|
||||
@ -3933,6 +3953,15 @@ dependencies = [
|
||||
"crypto-mac 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd"
|
||||
dependencies = [
|
||||
"crypto-mac 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.5.0"
|
||||
@ -4761,6 +4790,17 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10e7e75e27e8cd47e4be027d4b9fdc0b696116f981c22de21ca7bad63a9cb33a"
|
||||
dependencies = [
|
||||
"hmac 0.8.1",
|
||||
"pbkdf2 0.4.0",
|
||||
"sha2 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.4.1"
|
||||
@ -6356,6 +6396,7 @@ name = "validator_client"
|
||||
version = "0.2.13"
|
||||
dependencies = [
|
||||
"account_utils",
|
||||
"bincode",
|
||||
"bls",
|
||||
"clap",
|
||||
"clap_utils",
|
||||
@ -6381,6 +6422,7 @@ dependencies = [
|
||||
"rand 0.7.3",
|
||||
"rayon",
|
||||
"ring",
|
||||
"scrypt 0.3.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
@ -1,9 +1,11 @@
|
||||
use super::SECRET_KEY_BYTES_LEN;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Provides a wrapper around a `[u8; SECRET_KEY_BYTES_LEN]` that implements `Zeroize` on `Drop`.
|
||||
#[derive(Zeroize)]
|
||||
#[derive(Zeroize, Serialize, Deserialize)]
|
||||
#[zeroize(drop)]
|
||||
#[serde(transparent)]
|
||||
pub struct ZeroizeHash([u8; SECRET_KEY_BYTES_LEN]);
|
||||
|
||||
impl ZeroizeHash {
|
||||
|
@ -24,6 +24,7 @@ slot_clock = { path = "../common/slot_clock" }
|
||||
types = { path = "../consensus/types" }
|
||||
serde = "1.0.116"
|
||||
serde_derive = "1.0.116"
|
||||
bincode = "1.3.1"
|
||||
serde_json = "1.0.58"
|
||||
serde_yaml = "0.8.13"
|
||||
slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] }
|
||||
@ -57,3 +58,4 @@ serde_utils = { path = "../consensus/serde_utils" }
|
||||
libsecp256k1 = "0.3.5"
|
||||
ring = "0.16.12"
|
||||
rand = "0.7.3"
|
||||
scrypt = { version = "0.3.0", default-features = false }
|
||||
|
@ -11,15 +11,19 @@ use account_utils::{
|
||||
validator_definitions::{
|
||||
self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME,
|
||||
},
|
||||
ZeroizeString,
|
||||
};
|
||||
use eth2_keystore::Keystore;
|
||||
use slog::{error, info, warn, Logger};
|
||||
use std::collections::HashMap;
|
||||
use slog::{debug, error, info, warn, Logger};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use types::{Keypair, PublicKey};
|
||||
|
||||
use crate::key_cache;
|
||||
use crate::key_cache::KeyCache;
|
||||
|
||||
// Use TTY instead of stdin to capture passwords from users.
|
||||
const USE_STDIN: bool = false;
|
||||
|
||||
@ -37,9 +41,11 @@ pub enum Error {
|
||||
},
|
||||
/// There was a filesystem error when opening the keystore.
|
||||
UnableToOpenVotingKeystore(io::Error),
|
||||
UnableToOpenKeyCache(key_cache::Error),
|
||||
/// The keystore path is not as expected. It should be a file, not `..` or something obscure
|
||||
/// like that.
|
||||
BadVotingKeystorePath(PathBuf),
|
||||
BadKeyCachePath(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.
|
||||
@ -79,6 +85,59 @@ pub struct InitializedValidator {
|
||||
signing_method: SigningMethod,
|
||||
}
|
||||
|
||||
fn open_keystore(path: &PathBuf) -> Result<Keystore, Error> {
|
||||
let keystore_file = File::open(path).map_err(Error::UnableToOpenVotingKeystore)?;
|
||||
Keystore::from_json_reader(keystore_file).map_err(Error::UnableToParseVotingKeystore)
|
||||
}
|
||||
|
||||
fn get_lockfile_path(file_path: &PathBuf) -> Option<PathBuf> {
|
||||
file_path
|
||||
.file_name()
|
||||
.and_then(|os_str| os_str.to_str())
|
||||
.map(|filename| {
|
||||
file_path
|
||||
.clone()
|
||||
.with_file_name(format!("{}.lock", filename))
|
||||
})
|
||||
}
|
||||
|
||||
fn create_lock_file(
|
||||
file_path: &PathBuf,
|
||||
delete_lockfiles: bool,
|
||||
log: &Logger,
|
||||
) -> Result<(), Error> {
|
||||
if file_path.exists() {
|
||||
if delete_lockfiles {
|
||||
warn!(
|
||||
log,
|
||||
"Deleting validator lockfile";
|
||||
"file" => format!("{:?}", file_path)
|
||||
);
|
||||
|
||||
fs::remove_file(file_path).map_err(Error::UnableToDeleteLockfile)?;
|
||||
} else {
|
||||
return Err(Error::LockfileExists(file_path.clone()));
|
||||
}
|
||||
}
|
||||
// Create a new lockfile.
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(file_path)
|
||||
.map_err(Error::UnableToCreateLockfile)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_lock(lock_path: &PathBuf) {
|
||||
if lock_path.exists() {
|
||||
if let Err(e) = fs::remove_file(&lock_path) {
|
||||
eprintln!("Failed to remove {:?}: {:?}", lock_path, e)
|
||||
}
|
||||
} else {
|
||||
eprintln!("Lockfile missing: {:?}", lock_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl InitializedValidator {
|
||||
/// Instantiate `self` from a `ValidatorDefinition`.
|
||||
///
|
||||
@ -88,10 +147,12 @@ impl InitializedValidator {
|
||||
/// ## Errors
|
||||
///
|
||||
/// If the validator is unable to be initialized for whatever reason.
|
||||
pub fn from_definition(
|
||||
async fn from_definition(
|
||||
def: ValidatorDefinition,
|
||||
delete_lockfiles: bool,
|
||||
log: &Logger,
|
||||
key_cache: &mut KeyCache,
|
||||
key_stores: &mut HashMap<PathBuf, Keystore>,
|
||||
) -> Result<Self, Error> {
|
||||
if !def.enabled {
|
||||
return Err(Error::UnableToInitializeDisabledValidator);
|
||||
@ -105,30 +166,55 @@ impl InitializedValidator {
|
||||
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)?;
|
||||
use std::collections::hash_map::Entry::*;
|
||||
let voting_keystore = match key_stores.entry(voting_keystore_path.clone()) {
|
||||
Vacant(entry) => entry.insert(open_keystore(&voting_keystore_path)?),
|
||||
Occupied(entry) => entry.into_mut(),
|
||||
};
|
||||
|
||||
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) => {
|
||||
unlock_keystore_via_stdin_password(&voting_keystore, &voting_keystore_path)?
|
||||
}
|
||||
let voting_keypair = if let Some(keypair) = key_cache.get(voting_keystore.uuid()) {
|
||||
keypair
|
||||
} else {
|
||||
let keystore = voting_keystore.clone();
|
||||
let keystore_path = voting_keystore_path.clone();
|
||||
// Decoding a local keystore can take several seconds, therefore it's best
|
||||
// to keep if off the core executor. This also has the fortunate effect of
|
||||
// interrupting the potentially long-running task during shut down.
|
||||
let (password, keypair) = tokio::task::spawn_blocking(move || {
|
||||
Ok(
|
||||
match (voting_keystore_password_path, voting_keystore_password) {
|
||||
// If the password is supplied, use it and ignore the path
|
||||
// (if supplied).
|
||||
(_, Some(password)) => (
|
||||
password.as_ref().to_vec().into(),
|
||||
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)?;
|
||||
let keypair = keystore
|
||||
.decrypt_keypair(password.as_bytes())
|
||||
.map_err(Error::UnableToDecryptKeystore)?;
|
||||
(password, keypair)
|
||||
}
|
||||
// If there is no password available, maybe prompt for a password.
|
||||
(None, None) => {
|
||||
let (password, keypair) = unlock_keystore_via_stdin_password(
|
||||
&keystore,
|
||||
&keystore_path,
|
||||
)?;
|
||||
(password.as_ref().to_vec().into(), keypair)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(Error::TokioJoin)??;
|
||||
key_cache.add(keypair.clone(), voting_keystore.uuid(), password);
|
||||
keypair
|
||||
};
|
||||
|
||||
if voting_keypair.pk != def.voting_public_key {
|
||||
@ -139,47 +225,16 @@ impl InitializedValidator {
|
||||
}
|
||||
|
||||
// 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))
|
||||
})?;
|
||||
let voting_keystore_lockfile_path = get_lockfile_path(&voting_keystore_path)
|
||||
.ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone()))?;
|
||||
|
||||
if voting_keystore_lockfile_path.exists() {
|
||||
if delete_lockfiles {
|
||||
warn!(
|
||||
log,
|
||||
"Deleting validator lockfile";
|
||||
"file" => format!("{:?}", voting_keystore_lockfile_path)
|
||||
);
|
||||
|
||||
fs::remove_file(&voting_keystore_lockfile_path)
|
||||
.map_err(Error::UnableToDeleteLockfile)?;
|
||||
} else {
|
||||
return Err(Error::LockfileExists(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)?;
|
||||
}
|
||||
create_lock_file(&voting_keystore_lockfile_path, delete_lockfiles, &log)?;
|
||||
|
||||
Ok(Self {
|
||||
signing_method: SigningMethod::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_lockfile_path,
|
||||
voting_keystore,
|
||||
voting_keystore: voting_keystore.clone(),
|
||||
voting_keypair,
|
||||
},
|
||||
})
|
||||
@ -210,16 +265,7 @@ impl Drop for InitializedValidator {
|
||||
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)
|
||||
}
|
||||
remove_lock(voting_keystore_lockfile_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -229,7 +275,7 @@ impl Drop for InitializedValidator {
|
||||
fn unlock_keystore_via_stdin_password(
|
||||
keystore: &Keystore,
|
||||
keystore_path: &PathBuf,
|
||||
) -> Result<Keypair, Error> {
|
||||
) -> Result<(ZeroizeString, Keypair), Error> {
|
||||
eprintln!("");
|
||||
eprintln!(
|
||||
"The {} file does not contain either of the following fields for {:?}:",
|
||||
@ -255,7 +301,7 @@ fn unlock_keystore_via_stdin_password(
|
||||
eprintln!("");
|
||||
|
||||
match keystore.decrypt_keypair(password.as_ref()) {
|
||||
Ok(keystore) => break Ok(keystore),
|
||||
Ok(keystore) => break Ok((password, keystore)),
|
||||
Err(eth2_keystore::Error::InvalidPassword) => {
|
||||
eprintln!("Invalid password, try again (or press Ctrl+c to exit):");
|
||||
}
|
||||
@ -269,9 +315,8 @@ fn unlock_keystore_via_stdin_password(
|
||||
///
|
||||
/// 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,
|
||||
/// If `true`, delete any validator keystore lockfiles that would prevent starting.
|
||||
delete_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.
|
||||
@ -287,11 +332,11 @@ impl InitializedValidators {
|
||||
pub async fn from_definitions(
|
||||
definitions: ValidatorDefinitions,
|
||||
validators_dir: PathBuf,
|
||||
strict_lockfiles: bool,
|
||||
delete_lockfiles: bool,
|
||||
log: Logger,
|
||||
) -> Result<Self, Error> {
|
||||
let mut this = Self {
|
||||
strict_lockfiles,
|
||||
delete_lockfiles,
|
||||
validators_dir,
|
||||
definitions,
|
||||
validators: HashMap::default(),
|
||||
@ -393,6 +438,84 @@ impl InitializedValidators {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to decrypt the key cache.
|
||||
///
|
||||
/// Returns `Ok(true)` if decryption was successful, `Ok(false)` if it couldn't get decrypted
|
||||
/// and an error if a needed password couldn't get extracted.
|
||||
///
|
||||
async fn decrypt_key_cache(
|
||||
&self,
|
||||
mut cache: KeyCache,
|
||||
key_stores: &mut HashMap<PathBuf, Keystore>,
|
||||
) -> Result<KeyCache, Error> {
|
||||
//read relevant key_stores
|
||||
let mut definitions_map = HashMap::new();
|
||||
for def in self.definitions.as_slice() {
|
||||
match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
..
|
||||
} => {
|
||||
use std::collections::hash_map::Entry::*;
|
||||
let key_store = match key_stores.entry(voting_keystore_path.clone()) {
|
||||
Vacant(entry) => entry.insert(open_keystore(voting_keystore_path)?),
|
||||
Occupied(entry) => entry.into_mut(),
|
||||
};
|
||||
definitions_map.insert(*key_store.uuid(), def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//check if all paths are in the definitions_map
|
||||
for uuid in cache.uuids() {
|
||||
if !definitions_map.contains_key(uuid) {
|
||||
warn!(
|
||||
self.log,
|
||||
"Unknown uuid in cache";
|
||||
"uuid" => format!("{}", uuid)
|
||||
);
|
||||
return Ok(KeyCache::new());
|
||||
}
|
||||
}
|
||||
|
||||
//collect passwords
|
||||
let mut passwords = Vec::new();
|
||||
let mut public_keys = Vec::new();
|
||||
for uuid in cache.uuids() {
|
||||
let def = definitions_map.get(uuid).expect("Existence checked before");
|
||||
let pw = match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_password_path,
|
||||
voting_keystore_password,
|
||||
voting_keystore_path,
|
||||
} => {
|
||||
if let Some(p) = voting_keystore_password {
|
||||
p.as_ref().to_vec().into()
|
||||
} else if let Some(path) = voting_keystore_password_path {
|
||||
read_password(path).map_err(Error::UnableToReadVotingKeystorePassword)?
|
||||
} else {
|
||||
let keystore = open_keystore(voting_keystore_path)?;
|
||||
unlock_keystore_via_stdin_password(&keystore, &voting_keystore_path)?
|
||||
.0
|
||||
.as_ref()
|
||||
.to_vec()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
};
|
||||
passwords.push(pw);
|
||||
public_keys.push(def.voting_public_key.clone());
|
||||
}
|
||||
|
||||
//decrypt
|
||||
tokio::task::spawn_blocking(move || match cache.decrypt(passwords, public_keys) {
|
||||
Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => cache,
|
||||
_ => KeyCache::new(),
|
||||
})
|
||||
.await
|
||||
.map_err(Error::TokioJoin)
|
||||
}
|
||||
|
||||
/// Scans `self.definitions` and attempts to initialize and validators which are not already
|
||||
/// initialized.
|
||||
///
|
||||
@ -405,31 +528,48 @@ impl InitializedValidators {
|
||||
/// I.e., if there are two different definitions with the same public key then the second will
|
||||
/// be ignored.
|
||||
async fn update_validators(&mut self) -> Result<(), Error> {
|
||||
//use key cache if available
|
||||
let mut key_stores = HashMap::new();
|
||||
|
||||
// Create a lock file for the cache
|
||||
let key_cache_path = KeyCache::cache_file_path(&self.validators_dir);
|
||||
let cache_lockfile_path = get_lockfile_path(&key_cache_path)
|
||||
.ok_or_else(|| Error::BadKeyCachePath(key_cache_path))?;
|
||||
create_lock_file(&cache_lockfile_path, self.delete_lockfiles, &self.log)?;
|
||||
|
||||
let mut key_cache = self
|
||||
.decrypt_key_cache(
|
||||
KeyCache::open_or_create(&self.validators_dir)
|
||||
.map_err(Error::UnableToOpenKeyCache)?,
|
||||
&mut key_stores,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut disabled_uuids = HashSet::new();
|
||||
for def in self.definitions.as_slice() {
|
||||
if def.enabled {
|
||||
match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore { .. } => {
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
..
|
||||
} => {
|
||||
if self.validators.contains_key(&def.voting_public_key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decoding a local keystore can take several seconds, therefore it's best
|
||||
// to keep if off the core executor. This also has the fortunate effect of
|
||||
// interrupting the potentially long-running task during shut down.
|
||||
let inner_def = def.clone();
|
||||
let strict_lockfiles = self.strict_lockfiles;
|
||||
let inner_log = self.log.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
InitializedValidator::from_definition(
|
||||
inner_def,
|
||||
strict_lockfiles,
|
||||
&inner_log,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(Error::TokioJoin)?;
|
||||
if let Some(key_store) = key_stores.get(voting_keystore_path) {
|
||||
disabled_uuids.remove(key_store.uuid());
|
||||
}
|
||||
|
||||
match result {
|
||||
match InitializedValidator::from_definition(
|
||||
def.clone(),
|
||||
self.delete_lockfiles,
|
||||
&self.log,
|
||||
&mut key_cache,
|
||||
&mut key_stores,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(init) => {
|
||||
self.validators
|
||||
.insert(init.voting_public_key().clone(), init);
|
||||
@ -455,6 +595,17 @@ impl InitializedValidators {
|
||||
}
|
||||
} else {
|
||||
self.validators.remove(&def.voting_public_key);
|
||||
match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
..
|
||||
} => {
|
||||
if let Some(key_store) = key_stores.get(voting_keystore_path) {
|
||||
disabled_uuids.insert(*key_store.uuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
self.log,
|
||||
"Disabled validator";
|
||||
@ -462,6 +613,31 @@ impl InitializedValidators {
|
||||
);
|
||||
}
|
||||
}
|
||||
for uuid in disabled_uuids {
|
||||
key_cache.remove(&uuid);
|
||||
}
|
||||
|
||||
let validators_dir = self.validators_dir.clone();
|
||||
let log = self.log.clone();
|
||||
if key_cache.is_modified() {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
match key_cache.save(validators_dir) {
|
||||
Err(e) => warn!(
|
||||
log,
|
||||
"Error during saving of key_cache";
|
||||
"err" => format!("{:?}", e)
|
||||
),
|
||||
Ok(true) => info!(log, "Modified key_cache saved successfully"),
|
||||
_ => {}
|
||||
};
|
||||
remove_lock(&cache_lockfile_path);
|
||||
})
|
||||
.await
|
||||
.map_err(Error::TokioJoin)?;
|
||||
} else {
|
||||
debug!(log, "Key cache not modified");
|
||||
remove_lock(&cache_lockfile_path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
347
validator_client/src/key_cache.rs
Normal file
347
validator_client/src/key_cache.rs
Normal file
@ -0,0 +1,347 @@
|
||||
use account_utils::create_with_600_perms;
|
||||
use bls::{Keypair, PublicKey};
|
||||
use eth2_keystore::json_keystore::{
|
||||
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, KdfModule,
|
||||
Sha256Checksum,
|
||||
};
|
||||
use eth2_keystore::{
|
||||
decrypt, default_kdf, encrypt, keypair_from_secret, Error as KeystoreError, PlainText, Uuid,
|
||||
ZeroizeHash, IV_SIZE, SALT_SIZE,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, io};
|
||||
|
||||
/// The file name for the serialized `KeyCache` struct.
|
||||
pub const CACHE_FILENAME: &str = "validator_key_cache.json";
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum State {
|
||||
NotDecrypted,
|
||||
DecryptedAndSaved,
|
||||
DecryptedWithUnsavedUpdates,
|
||||
}
|
||||
|
||||
fn not_decrypted() -> State {
|
||||
State::NotDecrypted
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct KeyCache {
|
||||
crypto: Crypto,
|
||||
uuids: Vec<Uuid>,
|
||||
#[serde(skip)]
|
||||
pairs: HashMap<Uuid, Keypair>, //maps public keystore uuids to their corresponding Keypair
|
||||
#[serde(skip)]
|
||||
passwords: Vec<PlainText>,
|
||||
#[serde(skip)]
|
||||
#[serde(default = "not_decrypted")]
|
||||
state: State,
|
||||
}
|
||||
|
||||
type SerializedKeyMap = HashMap<Uuid, ZeroizeHash>;
|
||||
|
||||
impl KeyCache {
|
||||
pub fn new() -> Self {
|
||||
KeyCache {
|
||||
uuids: Vec::new(),
|
||||
crypto: Self::init_crypto(),
|
||||
pairs: HashMap::new(),
|
||||
passwords: Vec::new(),
|
||||
state: State::DecryptedWithUnsavedUpdates,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_crypto() -> Crypto {
|
||||
let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>();
|
||||
let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into();
|
||||
|
||||
let kdf = default_kdf(salt.to_vec());
|
||||
let cipher = Cipher::Aes128Ctr(Aes128Ctr { iv });
|
||||
|
||||
Crypto {
|
||||
kdf: KdfModule {
|
||||
function: kdf.function(),
|
||||
params: kdf,
|
||||
message: EmptyString,
|
||||
},
|
||||
checksum: ChecksumModule {
|
||||
function: Sha256Checksum::function(),
|
||||
params: EmptyMap,
|
||||
message: Vec::new().into(),
|
||||
},
|
||||
cipher: CipherModule {
|
||||
function: cipher.function(),
|
||||
params: cipher,
|
||||
message: Vec::new().into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_file_path<P: AsRef<Path>>(validators_dir: P) -> PathBuf {
|
||||
validators_dir.as_ref().join(CACHE_FILENAME)
|
||||
}
|
||||
|
||||
/// 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 cache_path = Self::cache_file_path(validators_dir.as_ref());
|
||||
if !cache_path.exists() {
|
||||
Ok(Self::new())
|
||||
} else {
|
||||
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 cache_path = validators_dir.as_ref().join(CACHE_FILENAME);
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.create_new(false)
|
||||
.open(&cache_path)
|
||||
.map_err(Error::UnableToOpenFile)?;
|
||||
serde_json::from_reader(file).map_err(Error::UnableToParseFile)
|
||||
}
|
||||
|
||||
fn encrypt(&mut self) -> Result<(), Error> {
|
||||
self.crypto = Self::init_crypto();
|
||||
let secret_map: SerializedKeyMap = self
|
||||
.pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (*k, v.sk.serialize()))
|
||||
.collect();
|
||||
|
||||
let raw = PlainText::from(
|
||||
bincode::serialize(&secret_map).map_err(Error::UnableToSerializeKeyMap)?,
|
||||
);
|
||||
let (cipher_text, checksum) = encrypt(
|
||||
raw.as_ref(),
|
||||
Self::password(&self.passwords).as_ref(),
|
||||
&self.crypto.kdf.params,
|
||||
&self.crypto.cipher.params,
|
||||
)
|
||||
.map_err(Error::UnableToEncrypt)?;
|
||||
|
||||
self.crypto.cipher.message = cipher_text.into();
|
||||
self.crypto.checksum.message = checksum.to_vec().into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores `Self` encrypted in json format.
|
||||
///
|
||||
/// Will create a new file if it does not exist or over-write any existing file.
|
||||
/// Returns false iff there are no unsaved changes
|
||||
pub fn save<P: AsRef<Path>>(&mut self, validators_dir: P) -> Result<bool, Error> {
|
||||
if self.is_modified() {
|
||||
self.encrypt()?;
|
||||
|
||||
let cache_path = validators_dir.as_ref().join(CACHE_FILENAME);
|
||||
let bytes = serde_json::to_vec(self).map_err(Error::UnableToEncodeFile)?;
|
||||
|
||||
let res = if cache_path.exists() {
|
||||
fs::write(cache_path, &bytes).map_err(Error::UnableToWriteFile)
|
||||
} else {
|
||||
create_with_600_perms(&cache_path, &bytes).map_err(Error::UnableToWriteFile)
|
||||
};
|
||||
if res.is_ok() {
|
||||
self.state = State::DecryptedAndSaved;
|
||||
}
|
||||
res.map(|_| true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_modified(&self) -> bool {
|
||||
self.state == State::DecryptedWithUnsavedUpdates
|
||||
}
|
||||
|
||||
pub fn uuids(&self) -> &Vec<Uuid> {
|
||||
&self.uuids
|
||||
}
|
||||
|
||||
fn password(passwords: &[PlainText]) -> PlainText {
|
||||
PlainText::from(passwords.iter().fold(Vec::new(), |mut v, p| {
|
||||
v.extend(p.as_ref());
|
||||
v
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn decrypt(
|
||||
&mut self,
|
||||
passwords: Vec<PlainText>,
|
||||
public_keys: Vec<PublicKey>,
|
||||
) -> Result<&HashMap<Uuid, Keypair>, Error> {
|
||||
match self.state {
|
||||
State::NotDecrypted => {
|
||||
let password = Self::password(&passwords);
|
||||
let text =
|
||||
decrypt(password.as_ref(), &self.crypto).map_err(Error::UnableToDecrypt)?;
|
||||
let key_map: SerializedKeyMap =
|
||||
bincode::deserialize(text.as_bytes()).map_err(Error::UnableToParseKeyMap)?;
|
||||
self.passwords = passwords;
|
||||
self.pairs = HashMap::new();
|
||||
if public_keys.len() != self.uuids.len() {
|
||||
return Err(Error::PublicKeyMismatch);
|
||||
}
|
||||
for (uuid, public_key) in self.uuids.iter().zip(public_keys.iter()) {
|
||||
if let Some(secret) = key_map.get(uuid) {
|
||||
let key_pair = keypair_from_secret(secret.as_ref())
|
||||
.map_err(Error::UnableToParseKeyPair)?;
|
||||
if &key_pair.pk != public_key {
|
||||
return Err(Error::PublicKeyMismatch);
|
||||
}
|
||||
self.pairs.insert(*uuid, key_pair);
|
||||
} else {
|
||||
return Err(Error::MissingUuidKey);
|
||||
}
|
||||
}
|
||||
self.state = State::DecryptedAndSaved;
|
||||
Ok(&self.pairs)
|
||||
}
|
||||
_ => Err(Error::AlreadyDecrypted),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, uuid: &Uuid) {
|
||||
//do nothing in not decrypted state
|
||||
if let State::NotDecrypted = self.state {
|
||||
return;
|
||||
}
|
||||
self.pairs.remove(uuid);
|
||||
if let Some(pos) = self.uuids.iter().position(|uuid2| uuid2 == uuid) {
|
||||
self.uuids.remove(pos);
|
||||
self.passwords.remove(pos);
|
||||
}
|
||||
self.state = State::DecryptedWithUnsavedUpdates;
|
||||
}
|
||||
|
||||
pub fn add(&mut self, keypair: Keypair, uuid: &Uuid, password: PlainText) {
|
||||
//do nothing in not decrypted state
|
||||
if let State::NotDecrypted = self.state {
|
||||
return;
|
||||
}
|
||||
self.pairs.insert(*uuid, keypair);
|
||||
self.uuids.push(*uuid);
|
||||
self.passwords.push(password);
|
||||
self.state = State::DecryptedWithUnsavedUpdates;
|
||||
}
|
||||
|
||||
pub fn get(&self, uuid: &Uuid) -> Option<Keypair> {
|
||||
self.pairs.get(uuid).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// The cache file could not be opened.
|
||||
UnableToOpenFile(io::Error),
|
||||
/// The cache file could not be parsed as JSON.
|
||||
UnableToParseFile(serde_json::Error),
|
||||
/// The cache file could not be serialized as YAML.
|
||||
UnableToEncodeFile(serde_json::Error),
|
||||
/// The cache file could not be written to the filesystem.
|
||||
UnableToWriteFile(io::Error),
|
||||
/// Couldn't decrypt the cache file
|
||||
UnableToDecrypt(KeystoreError),
|
||||
UnableToEncrypt(KeystoreError),
|
||||
/// Couldn't decode the decrypted hashmap
|
||||
UnableToParseKeyMap(bincode::Error),
|
||||
UnableToParseKeyPair(KeystoreError),
|
||||
UnableToSerializeKeyMap(bincode::Error),
|
||||
PublicKeyMismatch,
|
||||
MissingUuidKey,
|
||||
/// Cache file is already decrypted
|
||||
AlreadyDecrypted,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use eth2_keystore::json_keystore::{HexBytes, Kdf};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeyCacheTest {
|
||||
pub params: Kdf,
|
||||
//pub checksum: ChecksumModule,
|
||||
//pub cipher: CipherModule,
|
||||
uuids: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_serialization() {
|
||||
let mut key_cache = KeyCache::new();
|
||||
let key_pair = Keypair::random();
|
||||
let uuid = Uuid::from_u128(1);
|
||||
let password = PlainText::from(vec![1, 2, 3, 4, 5, 6]);
|
||||
key_cache.add(key_pair, &uuid, password);
|
||||
|
||||
key_cache.crypto.cipher.message = HexBytes::from(vec![7, 8, 9]);
|
||||
key_cache.crypto.checksum.message = HexBytes::from(vec![10, 11, 12]);
|
||||
|
||||
let binary = serde_json::to_vec(&key_cache).unwrap();
|
||||
let clone: KeyCache = serde_json::from_slice(binary.as_ref()).unwrap();
|
||||
|
||||
assert_eq!(clone.crypto, key_cache.crypto);
|
||||
assert_eq!(clone.uuids, key_cache.uuids);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_encryption() {
|
||||
let mut key_cache = KeyCache::new();
|
||||
let keypairs = vec![Keypair::random(), Keypair::random()];
|
||||
let uuids = vec![Uuid::from_u128(1), Uuid::from_u128(2)];
|
||||
let passwords = vec![
|
||||
PlainText::from(vec![1, 2, 3, 4, 5, 6]),
|
||||
PlainText::from(vec![7, 8, 9, 10, 11, 12]),
|
||||
];
|
||||
|
||||
for ((keypair, uuid), password) in keypairs.iter().zip(uuids.iter()).zip(passwords.iter()) {
|
||||
key_cache.add(keypair.clone(), uuid, password.clone());
|
||||
}
|
||||
|
||||
key_cache.encrypt().unwrap();
|
||||
key_cache.state = State::DecryptedAndSaved;
|
||||
|
||||
assert_eq!(&key_cache.uuids, &uuids);
|
||||
|
||||
let mut new_clone = KeyCache {
|
||||
crypto: key_cache.crypto.clone(),
|
||||
uuids: key_cache.uuids.clone(),
|
||||
pairs: Default::default(),
|
||||
passwords: vec![],
|
||||
state: State::NotDecrypted,
|
||||
};
|
||||
|
||||
new_clone
|
||||
.decrypt(passwords, keypairs.iter().map(|p| p.pk.clone()).collect())
|
||||
.unwrap();
|
||||
|
||||
let passwords_to_plain = |cache: &KeyCache| -> Vec<Vec<u8>> {
|
||||
cache
|
||||
.passwords
|
||||
.iter()
|
||||
.map(|x| x.as_bytes().to_vec())
|
||||
.collect()
|
||||
};
|
||||
|
||||
assert_eq!(key_cache.crypto, new_clone.crypto);
|
||||
assert_eq!(
|
||||
passwords_to_plain(&key_cache),
|
||||
passwords_to_plain(&new_clone)
|
||||
);
|
||||
assert_eq!(key_cache.uuids, new_clone.uuids);
|
||||
assert_eq!(key_cache.state, new_clone.state);
|
||||
assert_eq!(key_cache.pairs.len(), new_clone.pairs.len());
|
||||
for (key, value) in key_cache.pairs {
|
||||
assert!(new_clone.pairs.contains_key(&key));
|
||||
assert_eq!(
|
||||
format!("{:?}", value),
|
||||
format!("{:?}", new_clone.pairs[&key])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ mod duties_service;
|
||||
mod fork_service;
|
||||
mod initialized_validators;
|
||||
mod is_synced;
|
||||
mod key_cache;
|
||||
mod notifier;
|
||||
mod validator_duty;
|
||||
mod validator_store;
|
||||
|
Loading…
Reference in New Issue
Block a user