diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 66e3b73547..579195299c 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -3,7 +3,9 @@ //! 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 crate::{default_keystore_password_path, write_file_via_temporary, ZeroizeString}; +use crate::{ + default_keystore_password_path, read_password_string, write_file_via_temporary, ZeroizeString, +}; use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; @@ -43,6 +45,8 @@ pub enum Error { UnableToOpenKeystore(eth2_keystore::Error), /// The validator directory could not be created. UnableToCreateValidatorDir(PathBuf), + UnableToReadKeystorePassword(String), + KeystoreWithoutPassword, } #[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] @@ -92,6 +96,24 @@ impl SigningDefinition { pub fn is_local_keystore(&self) -> bool { matches!(self, SigningDefinition::LocalKeystore { .. }) } + + pub fn voting_keystore_password(&self) -> Result, Error> { + match self { + SigningDefinition::LocalKeystore { + voting_keystore_password: Some(password), + .. + } => Ok(Some(password.clone())), + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => read_password_string(path) + .map(Into::into) + .map(Option::Some) + .map_err(Error::UnableToReadKeystorePassword), + SigningDefinition::LocalKeystore { .. } => Err(Error::KeystoreWithoutPassword), + SigningDefinition::Web3Signer(_) => Ok(None), + } + } } /// A validator that may be initialized by this validator client. diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 88b5b68401..a2e3e3f6ff 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -487,6 +487,21 @@ impl ValidatorClientHttpClient { .await } + /// `DELETE eth/v1/keystores` + pub async fn delete_lighthouse_keystores( + &self, + req: &DeleteKeystoresRequest, + ) -> Result { + let mut path = self.server.full.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("keystores"); + + self.delete_with_unsigned_response(path, req).await + } + fn make_keystores_url(&self) -> Result { let mut url = self.server.full.clone(); url.path_segments_mut() diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index 92439337f6..2d9f01c292 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -144,3 +144,19 @@ pub struct UpdateGasLimitRequest { #[serde(with = "eth2_serde_utils::quoted_u64")] pub gas_limit: u64, } + +#[derive(Deserialize, Serialize)] +pub struct ExportKeystoresResponse { + pub data: Vec, + #[serde(with = "eth2_serde_utils::json_str")] + pub slashing_protection: Interchange, +} + +#[derive(Deserialize, Serialize)] +pub struct SingleExportKeystoresResponse { + pub status: Status, + #[serde(skip_serializing_if = "Option::is_none")] + pub validating_keystore: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub validating_keystore_password: Option, +} diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/src/http_api/keystores.rs index b886f60435..982b5f0171 100644 --- a/validator_client/src/http_api/keystores.rs +++ b/validator_client/src/http_api/keystores.rs @@ -4,10 +4,13 @@ use crate::{ ValidatorStore, }; use account_utils::ZeroizeString; -use eth2::lighthouse_vc::std_types::{ - DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, ImportKeystoreStatus, - ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, - ListKeystoresResponse, SingleKeystoreResponse, Status, +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 slog::{info, warn, Logger}; @@ -219,11 +222,28 @@ pub fn delete( task_executor: TaskExecutor, log: Logger, ) -> Result { + let export_response = export(request, validator_store, task_executor, log)?; + Ok(DeleteKeystoresResponse { + data: export_response + .data + .into_iter() + .map(|response| response.status) + .collect(), + slashing_protection: export_response.slashing_protection, + }) +} + +pub fn export( + request: DeleteKeystoresRequest, + validator_store: Arc>, + task_executor: TaskExecutor, + log: Logger, +) -> Result { // Remove from initialized validators. let initialized_validators_rwlock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rwlock.write(); - let mut statuses = request + let mut responses = request .pubkeys .iter() .map(|pubkey_bytes| { @@ -232,7 +252,7 @@ pub fn delete( &mut initialized_validators, task_executor.clone(), ) { - Ok(status) => Status::ok(status), + Ok(status) => status, Err(error) => { warn!( log, @@ -240,7 +260,11 @@ pub fn delete( "pubkey" => ?pubkey_bytes, "error" => ?error, ); - Status::error(DeleteKeystoreStatus::Error, error) + SingleExportKeystoresResponse { + status: Status::error(DeleteKeystoreStatus::Error, error), + validating_keystore: None, + validating_keystore_password: None, + } } } }) @@ -263,19 +287,19 @@ pub fn delete( })?; // Update stasuses based on availability of slashing protection data. - for (pubkey, status) in request.pubkeys.iter().zip(statuses.iter_mut()) { - if status.status == DeleteKeystoreStatus::NotFound + 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) { - status.status = DeleteKeystoreStatus::NotActive; + response.status.status = DeleteKeystoreStatus::NotActive; } } - Ok(DeleteKeystoresResponse { - data: statuses, + Ok(ExportKeystoresResponse { + data: responses, slashing_protection, }) } @@ -284,7 +308,7 @@ fn delete_single_keystore( pubkey_bytes: &PublicKeyBytes, initialized_validators: &mut InitializedValidators, task_executor: TaskExecutor, -) -> Result { +) -> Result { if let Some(handle) = task_executor.handle() { let pubkey = pubkey_bytes .decompress() @@ -292,9 +316,22 @@ fn delete_single_keystore( match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, true)) { - Ok(_) => Ok(DeleteKeystoreStatus::Deleted), + Ok(Some(keystore_and_password)) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::Deleted), + validating_keystore: Some(KeystoreJsonStr(keystore_and_password.keystore)), + validating_keystore_password: Some(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(DeleteKeystoreStatus::NotFound), + Error::ValidatorNotInitialized(_) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::NotFound), + validating_keystore: None, + validating_keystore_password: None, + }), _ => Err(format!("unable to disable and delete: {:?}", e)), }, } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 4bb8f33a06..ec7b00ac2b 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -77,6 +77,7 @@ pub struct Config { pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, + pub allow_keystore_export: bool, } impl Default for Config { @@ -86,6 +87,7 @@ impl Default for Config { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, allow_origin: None, + allow_keystore_export: false, } } } @@ -110,6 +112,7 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; + let allow_keystore_export = config.allow_keystore_export; let log = ctx.log.clone(); // Configure CORS. @@ -580,6 +583,29 @@ pub fn serve( }) }); + // DELETE /lighthouse/keystores + let delete_lighthouse_keystores = warp::path("lighthouse") + .and(warp::path("keystores")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(signer.clone()) + .and(validator_store_filter.clone()) + .and(task_executor_filter.clone()) + .and(log_filter.clone()) + .and_then( + move |request, signer, validator_store, task_executor, log| { + blocking_signed_json_task(signer, move || { + if allow_keystore_export { + keystores::export(request, validator_store, task_executor, log) + } else { + return Err(warp_utils::reject::custom_bad_request( + "keystore export is disabled".to_string(), + )); + } + }) + }, + ); + // Standard key-manager endpoints. let eth_v1 = warp::path("eth").and(warp::path("v1")); let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); @@ -913,7 +939,8 @@ pub fn serve( )) .or(warp::patch().and(patch_validators)) .or(warp::delete().and( - delete_fee_recipient + delete_lighthouse_keystores + .or(delete_fee_recipient) .or(delete_gas_limit) .or(delete_std_keystores) .or(delete_std_remotekeys), diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 25af9be6fe..174235b888 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -126,6 +126,7 @@ impl ApiTester { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, + allow_keystore_export: true, }, log, _phantom: PhantomData, diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 8d9fbe281f..6213304a44 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -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, @@ -43,6 +43,11 @@ 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 struct KeystoreAndPassword { + pub keystore: Keystore, + pub password: ZeroizeString, +} + #[derive(Debug)] pub enum Error { /// Refused to open a validator with an existing lockfile since that validator may be in-use by @@ -97,6 +102,9 @@ pub enum Error { UnableToBuildWeb3SignerClient(ReqwestError), /// Unable to apply an action to a validator. InvalidActionOnValidator, + UnableToReadValidatorPassword(String), + MissingKeystorePassword, + UnableToReadKeystoreFile(eth2_keystore::Error), } impl From for Error { @@ -534,31 +542,49 @@ impl InitializedValidators { &mut self, pubkey: &PublicKey, is_local_keystore: bool, - ) -> Result<(), Error> { + ) -> Result, 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 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), _) => password.clone(), + (_, Some(path)) => read_password_string(path) + .map_err(Error::UnableToReadValidatorPassword)?, + (None, None) => return Err(Error::MissingKeystorePassword), + }; + let keystore = Keystore::from_json_file(voting_keystore_path) + .map_err(Error::UnableToReadKeystoreFile)?; + + 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. Delete from `self.validators`, which holds the signing method. // Delete the keystore files. @@ -585,7 +611,7 @@ impl InitializedValidators { .save(&self.validators_dir) .map_err(Error::UnableToSaveDefinitions)?; - Ok(()) + Ok(keystore_and_password) } /// Attempt to delete the voting keystore file, or its entire validator directory.