Add export endpoint to LH

This commit is contained in:
Paul Hauner
2022-08-23 17:33:02 +10:00
parent bcbafc07bd
commit c0e9087c4b
7 changed files with 176 additions and 32 deletions

View File

@@ -3,7 +3,9 @@
//! Serves as the source-of-truth of which validators this validator client should attempt (or not //! 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. //! 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 directory::ensure_dir_exists;
use eth2_keystore::Keystore; use eth2_keystore::Keystore;
use regex::Regex; use regex::Regex;
@@ -43,6 +45,8 @@ pub enum Error {
UnableToOpenKeystore(eth2_keystore::Error), UnableToOpenKeystore(eth2_keystore::Error),
/// The validator directory could not be created. /// The validator directory could not be created.
UnableToCreateValidatorDir(PathBuf), UnableToCreateValidatorDir(PathBuf),
UnableToReadKeystorePassword(String),
KeystoreWithoutPassword,
} }
#[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] #[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)]
@@ -92,6 +96,24 @@ impl SigningDefinition {
pub fn is_local_keystore(&self) -> bool { pub fn is_local_keystore(&self) -> bool {
matches!(self, SigningDefinition::LocalKeystore { .. }) matches!(self, SigningDefinition::LocalKeystore { .. })
} }
pub fn voting_keystore_password(&self) -> Result<Option<ZeroizeString>, 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. /// A validator that may be initialized by this validator client.

View File

@@ -487,6 +487,21 @@ impl ValidatorClientHttpClient {
.await .await
} }
/// `DELETE eth/v1/keystores`
pub async fn delete_lighthouse_keystores(
&self,
req: &DeleteKeystoresRequest,
) -> Result<ExportKeystoresResponse, Error> {
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<Url, Error> { fn make_keystores_url(&self) -> Result<Url, Error> {
let mut url = self.server.full.clone(); let mut url = self.server.full.clone();
url.path_segments_mut() url.path_segments_mut()

View File

@@ -144,3 +144,19 @@ pub struct UpdateGasLimitRequest {
#[serde(with = "eth2_serde_utils::quoted_u64")] #[serde(with = "eth2_serde_utils::quoted_u64")]
pub gas_limit: u64, pub gas_limit: u64,
} }
#[derive(Deserialize, Serialize)]
pub struct ExportKeystoresResponse {
pub data: Vec<SingleExportKeystoresResponse>,
#[serde(with = "eth2_serde_utils::json_str")]
pub slashing_protection: Interchange,
}
#[derive(Deserialize, Serialize)]
pub struct SingleExportKeystoresResponse {
pub status: Status<DeleteKeystoreStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validating_keystore: Option<KeystoreJsonStr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validating_keystore_password: Option<ZeroizeString>,
}

View File

@@ -4,10 +4,13 @@ use crate::{
ValidatorStore, ValidatorStore,
}; };
use account_utils::ZeroizeString; use account_utils::ZeroizeString;
use eth2::lighthouse_vc::std_types::{ use eth2::lighthouse_vc::{
DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, ImportKeystoreStatus, std_types::{
ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse,
ListKeystoresResponse, SingleKeystoreResponse, Status, ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr,
KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status,
},
types::{ExportKeystoresResponse, SingleExportKeystoresResponse},
}; };
use eth2_keystore::Keystore; use eth2_keystore::Keystore;
use slog::{info, warn, Logger}; use slog::{info, warn, Logger};
@@ -219,11 +222,28 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>(
task_executor: TaskExecutor, task_executor: TaskExecutor,
log: Logger, log: Logger,
) -> Result<DeleteKeystoresResponse, Rejection> { ) -> Result<DeleteKeystoresResponse, Rejection> {
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<T: SlotClock + 'static, E: EthSpec>(
request: DeleteKeystoresRequest,
validator_store: Arc<ValidatorStore<T, E>>,
task_executor: TaskExecutor,
log: Logger,
) -> Result<ExportKeystoresResponse, Rejection> {
// Remove from initialized validators. // Remove from initialized validators.
let initialized_validators_rwlock = validator_store.initialized_validators(); let initialized_validators_rwlock = validator_store.initialized_validators();
let mut initialized_validators = initialized_validators_rwlock.write(); let mut initialized_validators = initialized_validators_rwlock.write();
let mut statuses = request let mut responses = request
.pubkeys .pubkeys
.iter() .iter()
.map(|pubkey_bytes| { .map(|pubkey_bytes| {
@@ -232,7 +252,7 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>(
&mut initialized_validators, &mut initialized_validators,
task_executor.clone(), task_executor.clone(),
) { ) {
Ok(status) => Status::ok(status), Ok(status) => status,
Err(error) => { Err(error) => {
warn!( warn!(
log, log,
@@ -240,7 +260,11 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>(
"pubkey" => ?pubkey_bytes, "pubkey" => ?pubkey_bytes,
"error" => ?error, "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<T: SlotClock + 'static, E: EthSpec>(
})?; })?;
// Update stasuses based on availability of slashing protection data. // Update stasuses based on availability of slashing protection data.
for (pubkey, status) in request.pubkeys.iter().zip(statuses.iter_mut()) { for (pubkey, response) in request.pubkeys.iter().zip(responses.iter_mut()) {
if status.status == DeleteKeystoreStatus::NotFound if response.status.status == DeleteKeystoreStatus::NotFound
&& slashing_protection && slashing_protection
.data .data
.iter() .iter()
.any(|interchange_data| interchange_data.pubkey == *pubkey) .any(|interchange_data| interchange_data.pubkey == *pubkey)
{ {
status.status = DeleteKeystoreStatus::NotActive; response.status.status = DeleteKeystoreStatus::NotActive;
} }
} }
Ok(DeleteKeystoresResponse { Ok(ExportKeystoresResponse {
data: statuses, data: responses,
slashing_protection, slashing_protection,
}) })
} }
@@ -284,7 +308,7 @@ fn delete_single_keystore(
pubkey_bytes: &PublicKeyBytes, pubkey_bytes: &PublicKeyBytes,
initialized_validators: &mut InitializedValidators, initialized_validators: &mut InitializedValidators,
task_executor: TaskExecutor, task_executor: TaskExecutor,
) -> Result<DeleteKeystoreStatus, String> { ) -> Result<SingleExportKeystoresResponse, String> {
if let Some(handle) = task_executor.handle() { if let Some(handle) = task_executor.handle() {
let pubkey = pubkey_bytes let pubkey = pubkey_bytes
.decompress() .decompress()
@@ -292,9 +316,22 @@ fn delete_single_keystore(
match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, true)) 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 { 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)), _ => Err(format!("unable to disable and delete: {:?}", e)),
}, },
} }

View File

@@ -77,6 +77,7 @@ pub struct Config {
pub listen_addr: IpAddr, pub listen_addr: IpAddr,
pub listen_port: u16, pub listen_port: u16,
pub allow_origin: Option<String>, pub allow_origin: Option<String>,
pub allow_keystore_export: bool,
} }
impl Default for Config { impl Default for Config {
@@ -86,6 +87,7 @@ impl Default for Config {
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
listen_port: 5062, listen_port: 5062,
allow_origin: None, allow_origin: None,
allow_keystore_export: false,
} }
} }
} }
@@ -110,6 +112,7 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
shutdown: impl Future<Output = ()> + Send + Sync + 'static, shutdown: impl Future<Output = ()> + Send + Sync + 'static,
) -> Result<(SocketAddr, impl Future<Output = ()>), Error> { ) -> Result<(SocketAddr, impl Future<Output = ()>), Error> {
let config = &ctx.config; let config = &ctx.config;
let allow_keystore_export = config.allow_keystore_export;
let log = ctx.log.clone(); let log = ctx.log.clone();
// Configure CORS. // Configure CORS.
@@ -580,6 +583,29 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
}) })
}); });
// 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. // Standard key-manager endpoints.
let eth_v1 = warp::path("eth").and(warp::path("v1")); let eth_v1 = warp::path("eth").and(warp::path("v1"));
let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end());
@@ -913,7 +939,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
)) ))
.or(warp::patch().and(patch_validators)) .or(warp::patch().and(patch_validators))
.or(warp::delete().and( .or(warp::delete().and(
delete_fee_recipient delete_lighthouse_keystores
.or(delete_fee_recipient)
.or(delete_gas_limit) .or(delete_gas_limit)
.or(delete_std_keystores) .or(delete_std_keystores)
.or(delete_std_remotekeys), .or(delete_std_remotekeys),

View File

@@ -126,6 +126,7 @@ impl ApiTester {
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
listen_port: 0, listen_port: 0,
allow_origin: None, allow_origin: None,
allow_keystore_export: true,
}, },
log, log,
_phantom: PhantomData, _phantom: PhantomData,

View File

@@ -8,7 +8,7 @@
use crate::signing_method::SigningMethod; use crate::signing_method::SigningMethod;
use account_utils::{ use account_utils::{
read_password, read_password_from_user, read_password, read_password_from_user, read_password_string,
validator_definitions::{ validator_definitions::{
self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition,
CONFIG_FILENAME, 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. // Use TTY instead of stdin to capture passwords from users.
const USE_STDIN: bool = false; const USE_STDIN: bool = false;
pub struct KeystoreAndPassword {
pub keystore: Keystore,
pub password: ZeroizeString,
}
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// Refused to open a validator with an existing lockfile since that validator may be in-use by /// 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), UnableToBuildWeb3SignerClient(ReqwestError),
/// Unable to apply an action to a validator. /// Unable to apply an action to a validator.
InvalidActionOnValidator, InvalidActionOnValidator,
UnableToReadValidatorPassword(String),
MissingKeystorePassword,
UnableToReadKeystoreFile(eth2_keystore::Error),
} }
impl From<LockfileError> for Error { impl From<LockfileError> for Error {
@@ -534,31 +542,49 @@ impl InitializedValidators {
&mut self, &mut self,
pubkey: &PublicKey, pubkey: &PublicKey,
is_local_keystore: bool, is_local_keystore: bool,
) -> Result<(), Error> { ) -> Result<Option<KeystoreAndPassword>, Error> {
// 1. Disable the validator definition. // 1. Disable the validator definition.
// //
// We disable before removing so that in case of a crash the auto-discovery mechanism // We disable before removing so that in case of a crash the auto-discovery mechanism
// won't re-activate the keystore. // won't re-activate the keystore.
if let Some(def) = self let keystore_and_password = if let Some(def) = self
.definitions .definitions
.as_mut_slice() .as_mut_slice()
.iter_mut() .iter_mut()
.find(|def| &def.voting_public_key == pubkey) .find(|def| &def.voting_public_key == pubkey)
{ {
// Update definition for local keystore match &def.signing_definition {
if def.signing_definition.is_local_keystore() && is_local_keystore { 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; def.enabled = false;
self.definitions self.definitions
.save(&self.validators_dir) .save(&self.validators_dir)
.map_err(Error::UnableToSaveDefinitions)?; .map_err(Error::UnableToSaveDefinitions)?;
} else if !def.signing_definition.is_local_keystore() && !is_local_keystore {
Some(KeystoreAndPassword { keystore, password })
}
SigningDefinition::Web3Signer(_) if !is_local_keystore => {
def.enabled = false; def.enabled = false;
} else { None
return Err(Error::InvalidActionOnValidator); }
_ => return Err(Error::InvalidActionOnValidator),
} }
} else { } else {
return Err(Error::ValidatorNotInitialized(pubkey.clone())); return Err(Error::ValidatorNotInitialized(pubkey.clone()));
} };
// 2. Delete from `self.validators`, which holds the signing method. // 2. Delete from `self.validators`, which holds the signing method.
// Delete the keystore files. // Delete the keystore files.
@@ -585,7 +611,7 @@ impl InitializedValidators {
.save(&self.validators_dir) .save(&self.validators_dir)
.map_err(Error::UnableToSaveDefinitions)?; .map_err(Error::UnableToSaveDefinitions)?;
Ok(()) Ok(keystore_and_password)
} }
/// Attempt to delete the voting keystore file, or its entire validator directory. /// Attempt to delete the voting keystore file, or its entire validator directory.