mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-10 04:01:51 +00:00
370 lines
14 KiB
Rust
370 lines
14 KiB
Rust
//! Implementation of the standard keystore management API.
|
|
use account_utils::validator_definitions::PasswordStorage;
|
|
use eth2::lighthouse_vc::{
|
|
std_types::{
|
|
DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse,
|
|
ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr,
|
|
KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status,
|
|
},
|
|
types::{ExportKeystoresResponse, SingleExportKeystoresResponse},
|
|
};
|
|
use eth2_keystore::Keystore;
|
|
use initialized_validators::{Error, InitializedValidators};
|
|
use lighthouse_validator_store::LighthouseValidatorStore;
|
|
use signing_method::SigningMethod;
|
|
use slot_clock::SlotClock;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use task_executor::TaskExecutor;
|
|
use tokio::runtime::Handle;
|
|
use tracing::{info, warn};
|
|
use types::{EthSpec, PublicKeyBytes};
|
|
use validator_dir::{Builder as ValidatorDirBuilder, keystore_password_path};
|
|
use warp::Rejection;
|
|
use warp_utils::reject::{custom_bad_request, custom_server_error};
|
|
use zeroize::Zeroizing;
|
|
|
|
pub fn list<T: SlotClock + 'static, E: EthSpec>(
|
|
validator_store: Arc<LighthouseValidatorStore<T, E>>,
|
|
) -> ListKeystoresResponse {
|
|
let initialized_validators_rwlock = validator_store.initialized_validators();
|
|
let initialized_validators = initialized_validators_rwlock.read();
|
|
|
|
let keystores = initialized_validators
|
|
.validator_definitions()
|
|
.iter()
|
|
.filter(|def| def.enabled)
|
|
.map(|def| {
|
|
let validating_pubkey = def.voting_public_key.compress();
|
|
|
|
let (derivation_path, readonly) = initialized_validators
|
|
.signing_method(&validating_pubkey)
|
|
.map_or((None, None), |signing_method| match *signing_method {
|
|
SigningMethod::LocalKeystore {
|
|
ref voting_keystore,
|
|
..
|
|
} => (voting_keystore.path(), Some(false)),
|
|
SigningMethod::Web3Signer { .. } => (None, Some(true)),
|
|
});
|
|
|
|
SingleKeystoreResponse {
|
|
validating_pubkey,
|
|
derivation_path,
|
|
readonly,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
ListKeystoresResponse { data: keystores }
|
|
}
|
|
|
|
pub fn import<T: SlotClock + 'static, E: EthSpec>(
|
|
request: ImportKeystoresRequest,
|
|
validator_dir: PathBuf,
|
|
secrets_dir: Option<PathBuf>,
|
|
validator_store: Arc<LighthouseValidatorStore<T, E>>,
|
|
task_executor: TaskExecutor,
|
|
) -> Result<ImportKeystoresResponse, Rejection> {
|
|
// Check request validity. This is the only cases in which we should return a 4xx code.
|
|
if request.keystores.len() != request.passwords.len() {
|
|
return Err(custom_bad_request(format!(
|
|
"mismatched numbers of keystores ({}) and passwords ({})",
|
|
request.keystores.len(),
|
|
request.passwords.len(),
|
|
)));
|
|
}
|
|
|
|
// Import slashing protection data before keystores, so that new keystores don't start signing
|
|
// without it. Do not return early on failure, propagate the failure to each key.
|
|
let slashing_protection_status =
|
|
if let Some(InterchangeJsonStr(slashing_protection)) = request.slashing_protection {
|
|
// Warn for missing slashing protection.
|
|
for KeystoreJsonStr(keystore) in &request.keystores {
|
|
if let Some(public_key) = keystore.public_key() {
|
|
let pubkey_bytes = public_key.compress();
|
|
if !slashing_protection
|
|
.data
|
|
.iter()
|
|
.any(|data| data.pubkey == pubkey_bytes)
|
|
{
|
|
warn!(?public_key, "Slashing protection data not provided");
|
|
}
|
|
}
|
|
}
|
|
|
|
validator_store.import_slashing_protection(slashing_protection)
|
|
} else {
|
|
warn!("No slashing protection data provided with keystores");
|
|
Ok(())
|
|
};
|
|
|
|
// Import each keystore. Some keystores may fail to be imported, so we record a status for each.
|
|
let mut statuses = Vec::with_capacity(request.keystores.len());
|
|
|
|
for (KeystoreJsonStr(keystore), password) in request
|
|
.keystores
|
|
.into_iter()
|
|
.zip(request.passwords.into_iter())
|
|
{
|
|
let pubkey_str = keystore.pubkey().to_string();
|
|
|
|
let status = if let Err(e) = &slashing_protection_status {
|
|
// Slashing protection import failed, do not attempt to import the key. Record an
|
|
// error status.
|
|
Status::error(
|
|
ImportKeystoreStatus::Error,
|
|
format!("slashing protection import failed: {:?}", e),
|
|
)
|
|
} else if let Some(handle) = task_executor.handle() {
|
|
// Import the keystore.
|
|
match import_single_keystore::<_, E>(
|
|
keystore,
|
|
password,
|
|
validator_dir.clone(),
|
|
secrets_dir.clone(),
|
|
&validator_store,
|
|
handle,
|
|
) {
|
|
Ok(status) => Status::ok(status),
|
|
Err(e) => {
|
|
warn!(
|
|
pubkey = pubkey_str,
|
|
error = ?e,
|
|
"Error importing keystore, skipped"
|
|
);
|
|
Status::error(ImportKeystoreStatus::Error, e)
|
|
}
|
|
}
|
|
} else {
|
|
Status::error(
|
|
ImportKeystoreStatus::Error,
|
|
"validator client shutdown".into(),
|
|
)
|
|
};
|
|
statuses.push(status);
|
|
}
|
|
|
|
let successful_import = statuses
|
|
.iter()
|
|
.filter(|status| matches!(status.status, ImportKeystoreStatus::Imported))
|
|
.count();
|
|
|
|
if successful_import > 0 {
|
|
info!(
|
|
count = successful_import,
|
|
"Imported keystores via standard HTTP API"
|
|
);
|
|
}
|
|
|
|
Ok(ImportKeystoresResponse { data: statuses })
|
|
}
|
|
|
|
fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
|
|
keystore: Keystore,
|
|
password: Zeroizing<String>,
|
|
validator_dir_path: PathBuf,
|
|
secrets_dir: Option<PathBuf>,
|
|
validator_store: &LighthouseValidatorStore<T, E>,
|
|
handle: Handle,
|
|
) -> Result<ImportKeystoreStatus, String> {
|
|
// Check if the validator key already exists, erroring if it is a remote signer validator.
|
|
let pubkey = keystore
|
|
.public_key()
|
|
.ok_or_else(|| format!("invalid pubkey: {}", keystore.pubkey()))?;
|
|
if let Some(def) = validator_store
|
|
.initialized_validators()
|
|
.read()
|
|
.validator_definitions()
|
|
.iter()
|
|
.find(|def| def.voting_public_key == pubkey)
|
|
{
|
|
if !def.signing_definition.is_local_keystore() {
|
|
return Err("cannot import duplicate of existing remote signer validator".into());
|
|
} else if def.enabled {
|
|
return Ok(ImportKeystoreStatus::Duplicate);
|
|
}
|
|
}
|
|
|
|
let password_storage = if let Some(secrets_dir) = &secrets_dir {
|
|
let password_path = keystore_password_path(secrets_dir, &keystore);
|
|
if password_path.exists() {
|
|
return Ok(ImportKeystoreStatus::Duplicate);
|
|
}
|
|
PasswordStorage::File(password_path)
|
|
} else {
|
|
PasswordStorage::ValidatorDefinitions(password.clone())
|
|
};
|
|
|
|
// Check that the password is correct.
|
|
// In future we should re-structure to avoid the double decryption here. It's not as simple
|
|
// as removing this check because `add_validator_keystore` will break if provided with an
|
|
// invalid validator definition (`update_validators` will get stuck trying to decrypt with the
|
|
// wrong password indefinitely).
|
|
keystore
|
|
.decrypt_keypair(password.as_ref())
|
|
.map_err(|e| format!("incorrect password: {:?}", e))?;
|
|
|
|
let validator_dir = ValidatorDirBuilder::new(validator_dir_path)
|
|
.password_dir_opt(secrets_dir)
|
|
.voting_keystore(keystore, password.as_ref())
|
|
.store_withdrawal_keystore(false)
|
|
.build()
|
|
.map_err(|e| format!("failed to build validator directory: {:?}", e))?;
|
|
|
|
// Drop validator dir so that `add_validator_keystore` can re-lock the keystore.
|
|
let voting_keystore_path = validator_dir.voting_keystore_path();
|
|
drop(validator_dir);
|
|
|
|
handle
|
|
.block_on(validator_store.add_validator_keystore(
|
|
voting_keystore_path,
|
|
password_storage,
|
|
true,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
))
|
|
.map_err(|e| format!("failed to initialize validator: {:?}", e))?;
|
|
|
|
Ok(ImportKeystoreStatus::Imported)
|
|
}
|
|
|
|
pub fn delete<T: SlotClock + 'static, E: EthSpec>(
|
|
request: DeleteKeystoresRequest,
|
|
validator_store: Arc<LighthouseValidatorStore<T, E>>,
|
|
task_executor: TaskExecutor,
|
|
) -> Result<DeleteKeystoresResponse, Rejection> {
|
|
let export_response = export(request, validator_store, task_executor)?;
|
|
|
|
// Check the status is Deleted to confirm deletion is successful, then only display the log
|
|
let successful_deletion = export_response
|
|
.data
|
|
.iter()
|
|
.filter(|response| matches!(response.status.status, DeleteKeystoreStatus::Deleted))
|
|
.count();
|
|
|
|
if successful_deletion > 0 {
|
|
info!(
|
|
count = successful_deletion,
|
|
"Deleted keystore via standard HTTP API"
|
|
);
|
|
}
|
|
|
|
Ok(DeleteKeystoresResponse {
|
|
data: export_response
|
|
.data
|
|
.into_iter()
|
|
.map(|response| response.status)
|
|
.collect(),
|
|
slashing_protection: export_response.slashing_protection,
|
|
})
|
|
}
|
|
|
|
pub fn export<T: SlotClock + 'static, E: EthSpec>(
|
|
request: DeleteKeystoresRequest,
|
|
validator_store: Arc<LighthouseValidatorStore<T, E>>,
|
|
task_executor: TaskExecutor,
|
|
) -> Result<ExportKeystoresResponse, Rejection> {
|
|
// Remove from initialized validators.
|
|
let initialized_validators_rwlock = validator_store.initialized_validators();
|
|
let mut initialized_validators = initialized_validators_rwlock.write();
|
|
|
|
let mut responses = request
|
|
.pubkeys
|
|
.iter()
|
|
.map(|pubkey_bytes| {
|
|
match delete_single_keystore(
|
|
pubkey_bytes,
|
|
&mut initialized_validators,
|
|
task_executor.clone(),
|
|
) {
|
|
Ok(status) => status,
|
|
Err(error) => {
|
|
warn!(
|
|
pubkey = ?pubkey_bytes,
|
|
?error,
|
|
"Error deleting keystore"
|
|
);
|
|
SingleExportKeystoresResponse {
|
|
status: Status::error(DeleteKeystoreStatus::Error, error),
|
|
validating_keystore: None,
|
|
validating_keystore_password: None,
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// Use `update_validators` to update the key cache. It is safe to let the key cache get a bit out
|
|
// of date as it resets when it can't be decrypted. We update it just a single time to avoid
|
|
// continually resetting it after each key deletion.
|
|
if let Some(handle) = task_executor.handle() {
|
|
handle
|
|
.block_on(initialized_validators.update_validators())
|
|
.map_err(|e| custom_server_error(format!("unable to update key cache: {:?}", e)))?;
|
|
}
|
|
|
|
// Export the slashing protection data.
|
|
let slashing_protection = validator_store
|
|
.export_slashing_protection_for_keys(&request.pubkeys)
|
|
.map_err(|e| {
|
|
custom_server_error(format!("error exporting slashing protection: {:?}", e))
|
|
})?;
|
|
|
|
// Update stasuses based on availability of slashing protection data.
|
|
for (pubkey, response) in request.pubkeys.iter().zip(responses.iter_mut()) {
|
|
if response.status.status == DeleteKeystoreStatus::NotFound
|
|
&& slashing_protection
|
|
.data
|
|
.iter()
|
|
.any(|interchange_data| interchange_data.pubkey == *pubkey)
|
|
{
|
|
response.status.status = DeleteKeystoreStatus::NotActive;
|
|
}
|
|
}
|
|
|
|
Ok(ExportKeystoresResponse {
|
|
data: responses,
|
|
slashing_protection,
|
|
})
|
|
}
|
|
|
|
fn delete_single_keystore(
|
|
pubkey_bytes: &PublicKeyBytes,
|
|
initialized_validators: &mut InitializedValidators,
|
|
task_executor: TaskExecutor,
|
|
) -> Result<SingleExportKeystoresResponse, String> {
|
|
if let Some(handle) = task_executor.handle() {
|
|
let pubkey = pubkey_bytes
|
|
.decompress()
|
|
.map_err(|e| format!("invalid pubkey, {:?}: {:?}", pubkey_bytes, e))?;
|
|
|
|
match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, true))
|
|
{
|
|
Ok(Some(keystore_and_password)) => Ok(SingleExportKeystoresResponse {
|
|
status: Status::ok(DeleteKeystoreStatus::Deleted),
|
|
validating_keystore: Some(KeystoreJsonStr(keystore_and_password.keystore)),
|
|
validating_keystore_password: keystore_and_password.password,
|
|
}),
|
|
Ok(None) => Ok(SingleExportKeystoresResponse {
|
|
status: Status::ok(DeleteKeystoreStatus::Deleted),
|
|
validating_keystore: None,
|
|
validating_keystore_password: None,
|
|
}),
|
|
Err(e) => match e {
|
|
Error::ValidatorNotInitialized(_) => Ok(SingleExportKeystoresResponse {
|
|
status: Status::ok(DeleteKeystoreStatus::NotFound),
|
|
validating_keystore: None,
|
|
validating_keystore_password: None,
|
|
}),
|
|
_ => Err(format!("unable to disable and delete: {:?}", e)),
|
|
},
|
|
}
|
|
} else {
|
|
Err("validator client shutdown".into())
|
|
}
|
|
}
|