mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-11 18:04:18 +00:00
Add validator-manager (#3502)
## Issue Addressed Addresses #2557 ## Proposed Changes Adds the `lighthouse validator-manager` command, which provides: - `lighthouse validator-manager create` - Creates a `validators.json` file and a `deposits.json` (same format as https://github.com/ethereum/staking-deposit-cli) - `lighthouse validator-manager import` - Imports validators from a `validators.json` file to the VC via the HTTP API. - `lighthouse validator-manager move` - Moves validators from one VC to the other, utilizing only the VC API. ## Additional Info In98bcb947cI've reduced some VC `ERRO` and `CRIT` warnings to `WARN` or `DEBG` for the case where a pubkey is missing from the validator store. These were being triggered when we removed a validator but still had it in caches. It seems to me that `UnknownPubkey` will only happen in the case where we've removed a validator, so downgrading the logs is prudent. All the logs are `DEBG` apart from attestations and blocks which are `WARN`. I thought having *some* logging about this condition might help us down the track. In856cd7e37dI've made the VC delete the corresponding password file when it's deleting a keystore. This seemed like nice hygiene. Notably, it'll only delete that password file after it scans the validator definitions and finds that no other validator is also using that password file.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
use crate::signing_method::SigningMethod;
|
||||
use account_utils::{
|
||||
read_password, read_password_from_user,
|
||||
read_password, read_password_from_user, read_password_string,
|
||||
validator_definitions::{
|
||||
self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition,
|
||||
CONFIG_FILENAME,
|
||||
@@ -44,6 +44,19 @@ const DEFAULT_REMOTE_SIGNER_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
|
||||
// Use TTY instead of stdin to capture passwords from users.
|
||||
const USE_STDIN: bool = false;
|
||||
|
||||
pub enum OnDecryptFailure {
|
||||
/// If the key cache fails to decrypt, create a new cache.
|
||||
CreateNew,
|
||||
/// Return an error if the key cache fails to decrypt. This should only be
|
||||
/// used in testing.
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct KeystoreAndPassword {
|
||||
pub keystore: Keystore,
|
||||
pub password: Option<ZeroizeString>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Refused to open a validator with an existing lockfile since that validator may be in-use by
|
||||
@@ -98,6 +111,11 @@ pub enum Error {
|
||||
UnableToBuildWeb3SignerClient(ReqwestError),
|
||||
/// Unable to apply an action to a validator.
|
||||
InvalidActionOnValidator,
|
||||
UnableToReadValidatorPassword(String),
|
||||
UnableToReadKeystoreFile(eth2_keystore::Error),
|
||||
UnableToSaveKeyCache(key_cache::Error),
|
||||
UnableToDecryptKeyCache(key_cache::Error),
|
||||
UnableToDeletePasswordFile(PathBuf, io::Error),
|
||||
}
|
||||
|
||||
impl From<LockfileError> for Error {
|
||||
@@ -539,33 +557,78 @@ impl InitializedValidators {
|
||||
&mut self,
|
||||
pubkey: &PublicKey,
|
||||
is_local_keystore: bool,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Option<KeystoreAndPassword>, Error> {
|
||||
// 1. Disable the validator definition.
|
||||
//
|
||||
// We disable before removing so that in case of a crash the auto-discovery mechanism
|
||||
// won't re-activate the keystore.
|
||||
if let Some(def) = self
|
||||
let mut uuid_opt = None;
|
||||
let mut password_path_opt = None;
|
||||
let keystore_and_password = if let Some(def) = self
|
||||
.definitions
|
||||
.as_mut_slice()
|
||||
.iter_mut()
|
||||
.find(|def| &def.voting_public_key == pubkey)
|
||||
{
|
||||
// Update definition for local keystore
|
||||
if def.signing_definition.is_local_keystore() && is_local_keystore {
|
||||
def.enabled = false;
|
||||
self.definitions
|
||||
.save(&self.validators_dir)
|
||||
.map_err(Error::UnableToSaveDefinitions)?;
|
||||
} else if !def.signing_definition.is_local_keystore() && !is_local_keystore {
|
||||
def.enabled = false;
|
||||
} else {
|
||||
return Err(Error::InvalidActionOnValidator);
|
||||
match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_password,
|
||||
voting_keystore_password_path,
|
||||
..
|
||||
} if is_local_keystore => {
|
||||
let password = match (voting_keystore_password, voting_keystore_password_path) {
|
||||
(Some(password), _) => Some(password.clone()),
|
||||
(_, Some(path)) => {
|
||||
password_path_opt = Some(path.clone());
|
||||
read_password_string(path)
|
||||
.map(Option::Some)
|
||||
.map_err(Error::UnableToReadValidatorPassword)?
|
||||
}
|
||||
(None, None) => None,
|
||||
};
|
||||
let keystore = Keystore::from_json_file(voting_keystore_path)
|
||||
.map_err(Error::UnableToReadKeystoreFile)?;
|
||||
uuid_opt = Some(*keystore.uuid());
|
||||
|
||||
def.enabled = false;
|
||||
self.definitions
|
||||
.save(&self.validators_dir)
|
||||
.map_err(Error::UnableToSaveDefinitions)?;
|
||||
|
||||
Some(KeystoreAndPassword { keystore, password })
|
||||
}
|
||||
SigningDefinition::Web3Signer(_) if !is_local_keystore => {
|
||||
def.enabled = false;
|
||||
None
|
||||
}
|
||||
_ => return Err(Error::InvalidActionOnValidator),
|
||||
}
|
||||
} else {
|
||||
return Err(Error::ValidatorNotInitialized(pubkey.clone()));
|
||||
};
|
||||
|
||||
// 2. Remove the validator from the key cache. This ensures the key
|
||||
// cache is consistent next time the VC starts.
|
||||
//
|
||||
// It's not a big deal if this succeeds and something fails later in
|
||||
// this function because the VC will self-heal from a corrupt key cache.
|
||||
//
|
||||
// Do this before modifying `self.validators` or deleting anything from
|
||||
// the filesystem.
|
||||
if let Some(uuid) = uuid_opt {
|
||||
let key_cache = KeyCache::open_or_create(&self.validators_dir)
|
||||
.map_err(Error::UnableToOpenKeyCache)?;
|
||||
let mut decrypted_key_cache = self
|
||||
.decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::CreateNew)
|
||||
.await?;
|
||||
decrypted_key_cache.remove(&uuid);
|
||||
decrypted_key_cache
|
||||
.save(&self.validators_dir)
|
||||
.map_err(Error::UnableToSaveKeyCache)?;
|
||||
}
|
||||
|
||||
// 2. Delete from `self.validators`, which holds the signing method.
|
||||
// 3. Delete from `self.validators`, which holds the signing method.
|
||||
// Delete the keystore files.
|
||||
if let Some(initialized_validator) = self.validators.remove(&pubkey.compress()) {
|
||||
if let SigningMethod::LocalKeystore {
|
||||
@@ -583,14 +646,28 @@ impl InitializedValidators {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Delete from validator definitions entirely.
|
||||
// 4. Delete from validator definitions entirely.
|
||||
self.definitions
|
||||
.retain(|def| &def.voting_public_key != pubkey);
|
||||
self.definitions
|
||||
.save(&self.validators_dir)
|
||||
.map_err(Error::UnableToSaveDefinitions)?;
|
||||
|
||||
Ok(())
|
||||
// 5. Delete the keystore password if it's not being used by any definition.
|
||||
if let Some(password_path) = password_path_opt.and_then(|p| p.canonicalize().ok()) {
|
||||
if self
|
||||
.definitions
|
||||
.iter_voting_keystore_password_paths()
|
||||
// Require canonicalized paths so we can do a true equality check.
|
||||
.filter_map(|existing| existing.canonicalize().ok())
|
||||
.all(|existing| existing != password_path)
|
||||
{
|
||||
fs::remove_file(&password_path)
|
||||
.map_err(|e| Error::UnableToDeletePasswordFile(password_path, e))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keystore_and_password)
|
||||
}
|
||||
|
||||
/// Attempt to delete the voting keystore file, or its entire validator directory.
|
||||
@@ -900,10 +977,11 @@ impl InitializedValidators {
|
||||
/// filesystem accesses for keystores that are already known. In the case that a keystore
|
||||
/// from the validator definitions is not yet in this map, it will be loaded from disk and
|
||||
/// inserted into the map.
|
||||
async fn decrypt_key_cache(
|
||||
pub async fn decrypt_key_cache(
|
||||
&self,
|
||||
mut cache: KeyCache,
|
||||
key_stores: &mut HashMap<PathBuf, Keystore>,
|
||||
on_failure: OnDecryptFailure,
|
||||
) -> Result<KeyCache, Error> {
|
||||
// Read relevant key stores from the filesystem.
|
||||
let mut definitions_map = HashMap::new();
|
||||
@@ -971,11 +1049,13 @@ impl InitializedValidators {
|
||||
|
||||
//decrypt
|
||||
tokio::task::spawn_blocking(move || match cache.decrypt(passwords, public_keys) {
|
||||
Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => cache,
|
||||
_ => KeyCache::new(),
|
||||
Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => Ok(cache),
|
||||
_ if matches!(on_failure, OnDecryptFailure::CreateNew) => Ok(KeyCache::new()),
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
.await
|
||||
.map_err(Error::TokioJoin)
|
||||
.map_err(Error::TokioJoin)?
|
||||
.map_err(Error::UnableToDecryptKeyCache)
|
||||
}
|
||||
|
||||
/// Scans `self.definitions` and attempts to initialize and validators which are not already
|
||||
@@ -1013,7 +1093,8 @@ impl InitializedValidators {
|
||||
// Only decrypt cache when there is at least one local definition.
|
||||
// Decrypting cache is a very expensive operation which is never used for web3signer.
|
||||
let mut key_cache = if has_local_definitions {
|
||||
self.decrypt_key_cache(cache, &mut key_stores).await?
|
||||
self.decrypt_key_cache(cache, &mut key_stores, OnDecryptFailure::CreateNew)
|
||||
.await?
|
||||
} else {
|
||||
// Assign an empty KeyCache if all definitions are of the Web3Signer type.
|
||||
KeyCache::new()
|
||||
@@ -1191,4 +1272,41 @@ impl InitializedValidators {
|
||||
val.index = Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes any passwords stored in the validator definitions file and
|
||||
/// returns a map of pubkey to deleted password.
|
||||
///
|
||||
/// This should only be used for testing, it's rather destructive.
|
||||
pub fn delete_passwords_from_validator_definitions(
|
||||
&mut self,
|
||||
) -> Result<HashMap<PublicKey, ZeroizeString>, Error> {
|
||||
let mut passwords = HashMap::default();
|
||||
|
||||
for def in self.definitions.as_mut_slice() {
|
||||
match &mut def.signing_definition {
|
||||
SigningDefinition::LocalKeystore {
|
||||
ref mut voting_keystore_password,
|
||||
..
|
||||
} => {
|
||||
if let Some(password) = voting_keystore_password.take() {
|
||||
passwords.insert(def.voting_public_key.clone(), password);
|
||||
}
|
||||
}
|
||||
// Remote signers don't have passwords.
|
||||
SigningDefinition::Web3Signer { .. } => (),
|
||||
};
|
||||
}
|
||||
|
||||
self.definitions
|
||||
.save(&self.validators_dir)
|
||||
.map_err(Error::UnableToSaveDefinitions)?;
|
||||
|
||||
Ok(passwords)
|
||||
}
|
||||
|
||||
/// Prefer other methods in production. Arbitrarily modifying a validator
|
||||
/// definition manually may result in inconsistencies.
|
||||
pub fn as_mut_slice_testing_only(&mut self) -> &mut [ValidatorDefinition] {
|
||||
self.definitions.as_mut_slice()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user