Files
lighthouse/validator_client/http_api/src/keystores.rs
Mac L f3fd1f210b Remove consensus/types re-exports (#8540)
There are certain crates which we re-export within `types` which creates a fragmented DevEx, where there are various ways to import the same crates.

```rust
// consensus/types/src/lib.rs
pub use bls::{
AggregatePublicKey, AggregateSignature, Error as BlsError, Keypair, PUBLIC_KEY_BYTES_LEN,
PublicKey, PublicKeyBytes, SIGNATURE_BYTES_LEN, SecretKey, Signature, SignatureBytes,
get_withdrawal_credentials,
};
pub use context_deserialize::{ContextDeserialize, context_deserialize};
pub use fixed_bytes::FixedBytesExtended;
pub use milhouse::{self, List, Vector};
pub use ssz_types::{BitList, BitVector, FixedVector, VariableList, typenum, typenum::Unsigned};
pub use superstruct::superstruct;
```

This PR removes these re-exports and makes it explicit that these types are imported from a non-`consensus/types` crate.


Co-Authored-By: Mac L <mjladson@pm.me>
2025-12-09 07:13:41 +00:00

371 lines
14 KiB
Rust

//! Implementation of the standard keystore management API.
use account_utils::validator_definitions::PasswordStorage;
use bls::PublicKeyBytes;
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;
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())
}
}