Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validator manager commands for standard key-manager APIs #5347

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0fa74cd
Validator manager commands for standard key-manager APIs
pahor167 Mar 4, 2024
f6d11bc
Merge latest unstable
chong-he Jul 29, 2024
83967a5
Fix Some in lib.rs
chong-he Jul 31, 2024
bf6e3b7
Replace Arg::with_name with Arg::new
chong-he Jul 31, 2024
1337a2f
Update takes_value
chong-he Jul 31, 2024
1082426
Remove clap::App
chong-he Jul 31, 2024
526603c
Change App to Command
chong-he Jul 31, 2024
0075fdd
Add command in use
chong-he Jul 31, 2024
6803092
Remove generic in ArgMatches
chong-he Jul 31, 2024
1630435
Fix matches.get_flag
chong-he Aug 1, 2024
1c8b3b3
Fixes
chong-he Aug 1, 2024
b80d16e
fix error handling
chong-he Aug 1, 2024
509d590
SetTrue in import
chong-he Aug 2, 2024
bb283a2
Fix
chong-he Aug 2, 2024
e1452f8
Fix builder-proposal flag (will delete the flag later)
chong-he Aug 2, 2024
e0918a8
Minor fix
chong-he Aug 2, 2024
97afc09
Fix prefer_builder_proposals
chong-he Aug 2, 2024
2212dd4
Remove unwrap
chong-he Aug 5, 2024
47382a7
Error handling from Michael
chong-he Aug 6, 2024
5f56ba5
Add cli help text
chong-he Aug 7, 2024
c6c8954
Use None in import to simplify
chong-he Aug 7, 2024
82d578f
Delete unwrap
chong-he Aug 7, 2024
d71cca9
Revert flags option
chong-he Aug 7, 2024
98ac81b
Simplify help command code
chong-he Aug 7, 2024
ad7cb23
Remove flag header in move
chong-he Aug 7, 2024
2f9f0dd
Merge remote-tracking branch 'origin/unstable' into pahor/validator-m…
michaelsproul Aug 8, 2024
6069c94
Add log in VC when keystore is deleted
chong-he Aug 12, 2024
ff0c841
Delete duplicated log when validator does not exist
chong-he Aug 12, 2024
0e4e9a4
Simplify log code
chong-he Aug 12, 2024
eadec07
Rename remove to delete
chong-he Aug 13, 2024
97196cc
cargo-fmt
chong-he Aug 13, 2024
6fb9f83
Try to remove a function
chong-he Aug 13, 2024
bed168c
make-cli
chong-he Aug 13, 2024
a55cc5b
Error handling
chong-he Aug 13, 2024
b5b9537
Merge branch 'vm' of https://github.com/chong-he/lighthouse into vm
chong-he Aug 13, 2024
100ee22
Update CLI hel text
chong-he Aug 13, 2024
610f971
make-cli
chong-he Aug 13, 2024
64a4f67
Fix checks
chong-he Aug 15, 2024
9ba60be
Merge branch 'vm' of https://github.com/chong-he/lighthouse into vm
chong-he Aug 15, 2024
e079eb4
Try to fix check errors
chong-he Aug 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions book/src/help_vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Commands:
"create-validators" command. This command only supports validators
signing via a keystore on the local file system (i.e., not Web3Signer
validators).
list
Lists all validators in a validator client using the HTTP API.
delete
Deletes validator from a validator client using the HTTP API.
import-standard
Uploads validators to a validator client using the HTTP API. The
validators are defined in a JSON file which can be generated using the
staking deposit CLI.
help
Print this message or the help of the given subcommand(s)

Expand Down
13 changes: 12 additions & 1 deletion common/account_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ use eth2_wallet::{
use filesystem::{create_with_600_perms, Error as FsError};
use rand::{distributions::Alphanumeric, Rng};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::thread::sleep;
use std::time::Duration;
use std::{
fs::{self, File},
str::FromStr,
};
use zeroize::Zeroize;

pub mod validator_definitions;
Expand Down Expand Up @@ -213,6 +216,14 @@ pub fn mnemonic_from_phrase(phrase: &str) -> Result<Mnemonic, String> {
#[serde(transparent)]
pub struct ZeroizeString(String);

impl FromStr for ZeroizeString {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_owned()))
}
}

impl From<String> for ZeroizeString {
fn from(s: String) -> Self {
Self(s)
Expand Down
9 changes: 8 additions & 1 deletion validator_client/src/http_api/keystores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,14 @@ pub fn delete<T: SlotClock + 'static, E: EthSpec>(
task_executor: TaskExecutor,
log: Logger,
) -> Result<DeleteKeystoresResponse, Rejection> {
let export_response = export(request, validator_store, task_executor, log)?;
let export_response = export(request, validator_store, task_executor, log.clone())?;

info!(
log,
"Deleting keystores via standard HTTP API";
"count" => export_response.data.len(),
);

Ok(DeleteKeystoresResponse {
data: export_response
.data
Expand Down
9 changes: 0 additions & 9 deletions validator_manager/src/create_validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ pub fn cli_app() -> Command {
Another, optional JSON file is created which contains a list of validator \
deposits in the same format as the \"ethereum/staking-deposit-cli\" tool.",
)
.arg(
Arg::new("help")
.long("help")
.short('h')
.help("Prints help information")
.action(ArgAction::HelpLong)
.display_order(0)
.help_heading(FLAG_HEADER),
)
.arg(
Arg::new(OUTPUT_PATH_FLAG)
.long(OUTPUT_PATH_FLAG)
Expand Down
263 changes: 263 additions & 0 deletions validator_manager/src/delete_validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
use std::path::PathBuf;

use clap::{Arg, ArgAction, ArgMatches, Command};
use eth2::{
lighthouse_vc::types::{DeleteKeystoreStatus, DeleteKeystoresRequest},
SensitiveUrl,
};
use serde::{Deserialize, Serialize};
use types::PublicKeyBytes;

use crate::{common::vc_http_client, DumpConfig};

pub const CMD: &str = "delete";
pub const VC_URL_FLAG: &str = "vc-url";
pub const VC_TOKEN_FLAG: &str = "vc-token";
pub const VALIDATOR_FLAG: &str = "validator";

#[derive(Debug)]
pub enum DeleteError {
InvalidPublicKey,
DeleteFailed(eth2::Error),
}

pub fn cli_app() -> Command {
Command::new(CMD)
.about("Deletes validator from a validator client using the HTTP API.")
.arg(
Arg::new(VC_URL_FLAG)
.long(VC_URL_FLAG)
.value_name("HTTP_ADDRESS")
.help(
"A HTTP(S) address of a validator client using the keymanager-API. \
If this value is not supplied then a 'dry run' will be conducted where \
no changes are made to the validator client.",
)
.default_value("http://localhost:5062")
.requires(VC_TOKEN_FLAG)
.action(ArgAction::Set),
)
.arg(
Arg::new(VC_TOKEN_FLAG)
.long(VC_TOKEN_FLAG)
.value_name("PATH")
.help("The file containing a token required by the validator client.")
.action(ArgAction::Set),
)
.arg(
Arg::new(VALIDATOR_FLAG)
.long(VALIDATOR_FLAG)
.value_name("STRING")
.help("Validator that will be deleted (pubkey).")
.action(ArgAction::Set),
)
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct DeleteConfig {
pub vc_url: SensitiveUrl,
pub vc_token_path: PathBuf,
pub validator_to_delete: PublicKeyBytes,
}

impl DeleteConfig {
fn from_cli(matches: &ArgMatches) -> Result<Self, String> {
Ok(Self {
vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?,
validator_to_delete: clap_utils::parse_required(matches, VALIDATOR_FLAG)?,
vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?,
})
}
}

pub async fn cli_run(matches: &ArgMatches, dump_config: DumpConfig) -> Result<(), String> {
let config = DeleteConfig::from_cli(matches)?;
if dump_config.should_exit_early(&config)? {
Ok(())
} else {
run(config).await
}
}

pub async fn run<'a>(config: DeleteConfig) -> Result<(), String> {
let DeleteConfig {
vc_url,
vc_token_path,
validator_to_delete,
} = config;

let (http_client, _keystores) = vc_http_client(vc_url.clone(), &vc_token_path).await?;

let validators = http_client.get_keystores().await.unwrap().data;

if !validators
.iter()
.any(|validator| validator.validating_pubkey == validator_to_delete)
{
return Err(format!("Validator {} doesn't exists", validator_to_delete));
}

let delete_request = DeleteKeystoresRequest {
pubkeys: vec![validator_to_delete],
};

let response = http_client
.delete_keystores(&delete_request)
.await
.map_err(|e| format!("Error deleting keystore {}", e))?
.data;

if response[0].status == DeleteKeystoreStatus::Error
|| response[0].status == DeleteKeystoreStatus::NotFound
|| response[0].status == DeleteKeystoreStatus::NotActive
{
eprintln!("Problem with removing validator {}", validator_to_delete);
return Err(format!(
"Problem with removing validator {}",
validator_to_delete
));
}

eprintln!("Validator deleted");
Ok(())
}

#[cfg(not(debug_assertions))]
#[cfg(test)]
mod test {
use std::{
fs::{self, File},
io::Write,
str::FromStr,
};

use super::*;
use crate::{
common::ValidatorSpecification, import_validators::tests::TestBuilder as ImportTestBuilder,
};
use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig};

struct TestBuilder {
delete_config: Option<DeleteConfig>,
src_import_builder: Option<ImportTestBuilder>,
http_config: HttpConfig,
vc_token: Option<String>,
validators: Vec<ValidatorSpecification>,
}

impl TestBuilder {
async fn new() -> Self {
Self {
delete_config: None,
src_import_builder: None,
http_config: ApiTester::default_http_config(),
vc_token: None,
validators: vec![],
}
}

async fn with_validators(
mut self,
count: u32,
first_index: u32,
index_of_validator_to_delete: usize,
) -> Self {
let builder = ImportTestBuilder::new_with_http_config(self.http_config.clone())
.await
.create_validators(count, first_index)
.await;

self.vc_token =
Some(fs::read_to_string(builder.get_import_config().vc_token_path).unwrap());

let local_validators: Vec<ValidatorSpecification> = {
let contents =
fs::read_to_string(builder.get_import_config().validators_file_path).unwrap();
serde_json::from_str(&contents).unwrap()
};

let import_config = builder.get_import_config();

self.delete_config = Some(DeleteConfig {
vc_url: import_config.vc_url,
vc_token_path: import_config.vc_token_path,
validator_to_delete: PublicKeyBytes::from_str(
format!(
"0x{}",
local_validators[index_of_validator_to_delete]
.voting_keystore
.pubkey()
)
.as_str(),
)
.unwrap(),
});

self.validators = local_validators.clone();
self.src_import_builder = Some(builder);
self
}

pub async fn run_test(self) -> TestResult {
let import_builder = self.src_import_builder.unwrap();
let import_test_result = import_builder.run_test().await;
assert!(import_test_result.result.is_ok());

let path = self.delete_config.clone().unwrap().vc_token_path;
let url = self.delete_config.clone().unwrap().vc_url;
let parent = path.parent().unwrap();

fs::create_dir_all(parent).expect("Was not able to create parent directory");

File::options()
.write(true)
.read(true)
.create(true)
.truncate(true)
.open(path.clone())
.unwrap()
.write_all(self.vc_token.clone().unwrap().as_bytes())
.unwrap();

let result = run(self.delete_config.clone().unwrap()).await;

if result.is_ok() {
let (http_client, _keystores) = vc_http_client(url, path.clone()).await.unwrap();
let list_keystores_response = http_client.get_keystores().await.unwrap().data;

assert_eq!(list_keystores_response.len(), self.validators.len() - 1);
assert!(list_keystores_response
.iter()
.all(|keystore| keystore.validating_pubkey
!= self.delete_config.clone().unwrap().validator_to_delete));

return TestResult { result: Ok(()) };
}

TestResult {
result: Err(result.unwrap_err()),
}
}
}

#[must_use]
struct TestResult {
result: Result<(), String>,
}

impl TestResult {
fn assert_ok(self) {
assert_eq!(self.result, Ok(()))
}
}
#[tokio::test]
async fn list_all_validators() {
TestBuilder::new()
.await
.with_validators(3, 0, 0)
.await
.run_test()
.await
.assert_ok();
}
}
Loading
Loading