diff --git a/Cargo.lock b/Cargo.lock index b8e830df83..3941aa7ec3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7355,6 +7355,26 @@ dependencies = [ "types", ] +[[package]] +name = "validator_manager" +version = "0.1.0" +dependencies = [ + "account_utils", + "bls", + "clap", + "clap_utils", + "environment", + "eth2", + "eth2_keystore", + "eth2_network_config", + "eth2_serde_utils", + "eth2_wallet", + "serde", + "serde_json", + "tree_hash", + "types", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 415c721d99..6ba70be57d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ members = [ "validator_client", "validator_client/slashing_protection", + + "validator_manager", ] [patch] diff --git a/account_manager/src/common.rs b/account_manager/src/common.rs index ce42615e50..0764db21f3 100644 --- a/account_manager/src/common.rs +++ b/account_manager/src/common.rs @@ -1,55 +1,7 @@ -use account_utils::PlainText; -use account_utils::{read_input_from_user, strip_off_newlines}; -use eth2_wallet::bip39::{Language, Mnemonic}; -use std::fs; -use std::path::PathBuf; -use std::str::from_utf8; -use std::thread::sleep; -use std::time::Duration; +use account_utils::read_input_from_user; -pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; pub const WALLET_NAME_PROMPT: &str = "Enter wallet name:"; -pub fn read_mnemonic_from_cli( - mnemonic_path: Option, - stdin_inputs: bool, -) -> Result { - let mnemonic = match mnemonic_path { - Some(path) => fs::read(&path) - .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) - .and_then(|bytes| { - let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); - let phrase = from_utf8(bytes_no_newlines.as_ref()) - .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; - Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { - format!( - "Unable to derive mnemonic from string {:?}: {:?}", - phrase, e - ) - }) - })?, - None => loop { - eprintln!(); - eprintln!("{}", MNEMONIC_PROMPT); - - let mnemonic = read_input_from_user(stdin_inputs)?; - - match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { - Ok(mnemonic_m) => { - eprintln!("Valid mnemonic provided."); - eprintln!(); - sleep(Duration::from_secs(1)); - break mnemonic_m; - } - Err(_) => { - eprintln!("Invalid mnemonic"); - } - } - }, - }; - Ok(mnemonic) -} - /// Reads in a wallet name from the user. If the `--wallet-name` flag is provided, use it. Otherwise /// read from an interactive prompt using tty unless the `--stdin-inputs` flag is provided. pub fn read_wallet_name_from_cli( diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index d9b05e7756..33d3b18926 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,10 +1,9 @@ use super::create::STORE_WITHDRAW_FLAG; -use crate::common::read_mnemonic_from_cli; use crate::validator::create::COUNT_FLAG; use crate::wallet::create::STDIN_INPUTS_FLAG; use crate::SECRETS_DIR_FLAG; use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; -use account_utils::random_password; +use account_utils::{random_password, read_mnemonic_from_cli}; use clap::{App, Arg, ArgMatches}; use directory::ensure_dir_exists; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs index f107c3638c..6e047aca8d 100644 --- a/account_manager/src/wallet/recover.rs +++ b/account_manager/src/wallet/recover.rs @@ -1,6 +1,6 @@ -use crate::common::read_mnemonic_from_cli; use crate::wallet::create::{create_wallet_from_mnemonic, STDIN_INPUTS_FLAG}; use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; +use account_utils::read_mnemonic_from_cli; use clap::{App, Arg, ArgMatches}; use std::path::PathBuf; diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 89de380385..be3b900838 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -13,6 +13,9 @@ 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 zeroize::Zeroize; pub mod validator_definitions; @@ -30,6 +33,8 @@ pub const MINIMUM_PASSWORD_LEN: usize = 12; /// array of length 32. const DEFAULT_PASSWORD_LEN: usize = 48; +pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; + /// Returns the "default" path where a wallet should store its password file. pub fn default_wallet_password_path>(wallet_name: &str, secrets_dir: P) -> PathBuf { secrets_dir.as_ref().join(format!("{}.pass", wallet_name)) @@ -220,6 +225,46 @@ impl AsRef<[u8]> for ZeroizeString { } } +pub fn read_mnemonic_from_cli( + mnemonic_path: Option, + stdin_inputs: bool, +) -> Result { + let mnemonic = match mnemonic_path { + Some(path) => fs::read(&path) + .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) + .and_then(|bytes| { + let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); + let phrase = from_utf8(bytes_no_newlines.as_ref()) + .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; + Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { + format!( + "Unable to derive mnemonic from string {:?}: {:?}", + phrase, e + ) + }) + })?, + None => loop { + eprintln!(); + eprintln!("{}", MNEMONIC_PROMPT); + + let mnemonic = read_input_from_user(stdin_inputs)?; + + match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { + Ok(mnemonic_m) => { + eprintln!("Valid mnemonic provided."); + eprintln!(); + sleep(Duration::from_secs(1)); + break mnemonic_m; + } + Err(_) => { + eprintln!("Invalid mnemonic"); + } + } + }, + }; + Ok(mnemonic) +} + #[cfg(test)] mod test { use super::*; diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b2ba24ac3e..1aaefab0a9 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -71,6 +71,7 @@ pub struct ChainSpec { */ pub genesis_fork_version: [u8; 4], pub bls_withdrawal_prefix_byte: u8, + pub eth1_address_withdrawal_prefix_byte: u8, /* * Time parameters @@ -481,6 +482,7 @@ impl ChainSpec { */ genesis_fork_version: [0; 4], bls_withdrawal_prefix_byte: 0, + eth1_address_withdrawal_prefix_byte: 1, /* * Time parameters @@ -686,6 +688,7 @@ impl ChainSpec { */ genesis_fork_version: [0x00, 0x00, 0x00, 0x64], bls_withdrawal_prefix_byte: 0, + eth1_address_withdrawal_prefix_byte: 1, /* * Time parameters diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 32300173eb..7112b2d01c 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -66,6 +66,7 @@ pub mod sync_duty; pub mod validator; pub mod validator_subscription; pub mod voluntary_exit; +pub mod withdrawal_credentials; #[macro_use] pub mod slot_epoch_macros; pub mod config_and_preset; @@ -165,6 +166,7 @@ pub use crate::validator::Validator; pub use crate::validator_registration_data::*; pub use crate::validator_subscription::ValidatorSubscription; pub use crate::voluntary_exit::VoluntaryExit; +pub use crate::withdrawal_credentials::WithdrawalCredentials; pub type CommitteeIndex = u64; pub type Hash256 = H256; diff --git a/consensus/types/src/withdrawal_credentials.rs b/consensus/types/src/withdrawal_credentials.rs new file mode 100644 index 0000000000..8d42d4eafd --- /dev/null +++ b/consensus/types/src/withdrawal_credentials.rs @@ -0,0 +1,57 @@ +use crate::*; +use bls::get_withdrawal_credentials; + +pub struct WithdrawalCredentials(Hash256); + +impl WithdrawalCredentials { + pub fn bls(withdrawal_public_key: &PublicKey, spec: &ChainSpec) -> Self { + let withdrawal_credentials = + get_withdrawal_credentials(withdrawal_public_key, spec.bls_withdrawal_prefix_byte); + Self(Hash256::from_slice(&withdrawal_credentials)) + } + + pub fn eth1(withdrawal_address: Address, spec: &ChainSpec) -> Self { + let mut withdrawal_credentials = [0; 32]; + withdrawal_credentials[0] = spec.eth1_address_withdrawal_prefix_byte; + withdrawal_credentials[12..].copy_from_slice(withdrawal_address.as_bytes()); + Self(Hash256::from_slice(&withdrawal_credentials)) + } +} + +impl From for Hash256 { + fn from(withdrawal_credentials: WithdrawalCredentials) -> Self { + withdrawal_credentials.0 + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::generate_deterministic_keypair; + use std::str::FromStr; + + #[test] + fn bls_withdrawal_credentials() { + let spec = &MainnetEthSpec::default_spec(); + let keypair = generate_deterministic_keypair(0); + let credentials = WithdrawalCredentials::bls(&keypair.pk, spec); + let manually_generated_credentials = + get_withdrawal_credentials(&keypair.pk, spec.bls_withdrawal_prefix_byte); + let hash: Hash256 = credentials.into(); + assert_eq!(hash[0], spec.bls_withdrawal_prefix_byte); + assert_eq!(hash.as_bytes(), &manually_generated_credentials); + } + + #[test] + fn eth1_withdrawal_credentials() { + let spec = &MainnetEthSpec::default_spec(); + let address = Address::from_str("0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b").unwrap(); + let credentials = WithdrawalCredentials::eth1(address, spec); + let hash: Hash256 = credentials.into(); + assert_eq!( + hash, + Hash256::from_str("0x01000000000000000000000025c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b") + .unwrap() + ) + } +} diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml new file mode 100644 index 0000000000..041d548bea --- /dev/null +++ b/validator_manager/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "validator_manager" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bls = { path = "../crypto/bls" } +clap = "2.33.3" +types = { path = "../consensus/types" } +environment = { path = "../lighthouse/environment" } +eth2_network_config = { path = "../common/eth2_network_config" } +clap_utils = { path = "../common/clap_utils" } +eth2_wallet = { path = "../crypto/eth2_wallet" } +eth2_keystore = { path = "../crypto/eth2_keystore" } +account_utils = { path = "../common/account_utils" } +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.58" +eth2_serde_utils = "0.1.1" +tree_hash = "0.4.1" +eth2 = { path = "../common/eth2", features = ["lighthouse"]} diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs new file mode 100644 index 0000000000..c5d41937b8 --- /dev/null +++ b/validator_manager/src/lib.rs @@ -0,0 +1,33 @@ +use clap::App; +use clap::ArgMatches; +use environment::Environment; +use types::EthSpec; + +mod validator; + +pub const CMD: &str = "validator_manager"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .visible_aliases(&["vm", CMD]) + .about("Utilities for managing a Lighthouse validator client via the HTTP API.") + .subcommand(validator::cli_app()) +} + +/// Run the account manager, returning an error if the operation did not succeed. +pub async fn run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + env: Environment, +) -> Result<(), String> { + match matches.subcommand() { + (validator::CMD, Some(matches)) => validator::cli_run(matches, env).await?, + (unknown, _) => { + return Err(format!( + "{} is not a valid {} command. See --help.", + unknown, CMD + )); + } + } + + Ok(()) +} diff --git a/validator_manager/src/validator/create.rs b/validator_manager/src/validator/create.rs new file mode 100644 index 0000000000..324a5bc3a7 --- /dev/null +++ b/validator_manager/src/validator/create.rs @@ -0,0 +1,392 @@ +use account_utils::{random_password, read_mnemonic_from_cli, read_password}; +use clap::{App, Arg, ArgMatches}; +use environment::Environment; +use eth2::{lighthouse_vc::http_client::ValidatorClientHttpClient, SensitiveUrl}; +use eth2_wallet::WalletBuilder; +use serde::Serialize; +use std::fs; +use std::path::PathBuf; +use tree_hash::TreeHash; +use types::*; + +pub const CMD: &str = "create"; +pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; +pub const JSON_DEPOSIT_DATA_PATH: &str = "json-deposit-data-path"; +pub const COUNT_FLAG: &str = "count"; +pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; +pub const FIRST_INDEX_FLAG: &str = "first-index"; +pub const MNEMONIC_FLAG: &str = "mnemonic-path"; +pub const PASSWORD_FLAG: &str = "password-file"; +pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address"; +pub const DRY_RUN_FLAG: &str = "dry-run"; +pub const VALIDATOR_CLIENT_URL_FLAG: &str = "validator-client-url"; +pub const VALIDATOR_CLIENT_TOKEN_FLAG: &str = "validator-client-token"; +pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; +pub const KEYSTORE_UPLOAD_BATCH_SIZE: &str = "keystore-upload-batch-size"; + +/// The structure generated by the `staking-deposit-cli` which has become a quasi-standard for +/// browser-based deposit submission tools (e.g., the Ethereum Launchpad and Lido). +/// +/// We assume this code as the canonical definition: +/// +/// https://github.com/ethereum/staking-deposit-cli/blob/76ed78224fdfe3daca788d12442b3d1a37978296/staking_deposit/credentials.py#L131-L144 +#[derive(Debug, PartialEq, Serialize)] +pub struct StandardDepositDataJson { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: Hash256, + #[serde(with = "eth2_serde_utils::quoted_u64")] + pub amount: u64, + pub signature: SignatureBytes, + #[serde(with = "eth2_serde_utils::bytes_4_hex")] + pub fork_version: [u8; 4], + pub eth2_network_name: String, + pub deposit_message_root: Hash256, + pub deposit_data_root: Hash256, +} + +impl StandardDepositDataJson { + fn new( + keypair: &Keypair, + withdrawal_credentials: Hash256, + amount: u64, + spec: &ChainSpec, + ) -> Result { + let deposit_data = { + let mut deposit_data = DepositData { + pubkey: keypair.pk.clone().into(), + withdrawal_credentials, + amount, + signature: SignatureBytes::empty(), + }; + deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec); + deposit_data + }; + + let domain = spec.get_deposit_domain(); + let deposit_message_root = deposit_data.as_deposit_message().signing_root(domain); + let deposit_data_root = deposit_data.tree_hash_root(); + + let DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + } = deposit_data; + + Ok(Self { + pubkey, + withdrawal_credentials, + amount, + signature, + fork_version: spec.genesis_fork_version, + eth2_network_name: spec + .config_name + .clone() + .ok_or("The network specification does not have a CONFIG_NAME set")?, + deposit_message_root, + deposit_data_root, + }) + } +} + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about("Creates new validators from BIP-39 mnemonic.") + .arg( + Arg::with_name(DEPOSIT_GWEI_FLAG) + .long(DEPOSIT_GWEI_FLAG) + .value_name("DEPOSIT_GWEI") + .help( + "The GWEI value of the deposit amount. Defaults to the minimum amount \ + required for an active validator (MAX_EFFECTIVE_BALANCE)", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(FIRST_INDEX_FLAG) + .long(FIRST_INDEX_FLAG) + .value_name("FIRST_INDEX") + .help("The first of consecutive key indexes you wish to recover.") + .takes_value(true) + .required(false) + .default_value("0"), + ) + .arg( + Arg::with_name(COUNT_FLAG) + .long(COUNT_FLAG) + .value_name("VALIDATOR_COUNT") + .help("The number of validators to create, regardless of how many already exist") + .conflicts_with("at-most") + .takes_value(true), + ) + .arg( + Arg::with_name(MNEMONIC_FLAG) + .long(MNEMONIC_FLAG) + .value_name("MNEMONIC_PATH") + .help("If present, the mnemonic will be read in from this file.") + .takes_value(true), + ) + .arg( + Arg::with_name(STDIN_INPUTS_FLAG) + .takes_value(false) + .hidden(cfg!(windows)) + .long(STDIN_INPUTS_FLAG) + .help("If present, read all user inputs from stdin instead of tty."), + ) + .arg( + Arg::with_name(JSON_DEPOSIT_DATA_PATH) + .long(JSON_DEPOSIT_DATA_PATH) + .value_name("PATH") + .help( + "When provided, outputs a JSON file containing deposit data which \ + is equivalent to the 'deposit-data-*.json' file used by the \ + staking-deposit-cli tool.", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(PASSWORD_FLAG) + .long(PASSWORD_FLAG) + .value_name("STRING") + .help( + "A path to a file containing the password which will unlock the wallet. \ + If the file does not exist, a random password will be generated and \ + saved at that path. To avoid confusion, if the file does not already \ + exist it must include a '.pass' suffix.", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(ETH1_WITHDRAWAL_ADDRESS_FLAG) + .long(ETH1_WITHDRAWAL_ADDRESS_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "If this field is set, the given eth1 address will be used to create the \ + withdrawal credentials. Otherwise, it will generate withdrawal credentials \ + with the mnemonic-derived withdrawal public key in EIP-2334 format.", + ) + .takes_value(true), + ) + .arg( + Arg::with_name(DRY_RUN_FLAG) + .takes_value(false) + .long(DRY_RUN_FLAG) + .help( + "If present, perform all actions without ever contacting the validator client.", + ), + ) + .arg( + Arg::with_name(VALIDATOR_CLIENT_URL_FLAG) + .long(VALIDATOR_CLIENT_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help("A HTTP(S) address of a validator client using the keymanager-API.") + .required_unless(DRY_RUN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(VALIDATOR_CLIENT_TOKEN_FLAG) + .long(VALIDATOR_CLIENT_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the validator client.") + .required_unless(DRY_RUN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(IGNORE_DUPLICATES_FLAG) + .takes_value(false) + .long(IGNORE_DUPLICATES_FLAG) + .help( + "If present, ignore any validators which already exist on the VC. \ + Without this flag, the process will terminate without making any changes. \ + This flag should be used with caution, whilst it does not directly cause \ + slashable conditions, it might be an indicator that something is amiss. \ + Users should also be careful to avoid submitting duplicate deposits for \ + validators that already exist on the VC.", + ), + ) + .arg( + Arg::with_name(KEYSTORE_UPLOAD_BATCH_SIZE) + .long(KEYSTORE_UPLOAD_BATCH_SIZE) + .value_name("INTEGER") + .help("The number of keystores to be submitted to the VC per request.") + .takes_value(true) + .default_value("16"), + ) +} + +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + mut env: Environment, +) -> Result<(), String> { + let spec = env.core_context().eth2_config.spec; + + let deposit_gwei = clap_utils::parse_optional(matches, DEPOSIT_GWEI_FLAG)? + .unwrap_or(spec.max_effective_balance); + let first_index: u32 = clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?; + let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?; + let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; + let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); + let json_deposit_data_path: Option = + clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; + let wallet_password_path: Option = clap_utils::parse_optional(matches, PASSWORD_FLAG)?; + let eth1_withdrawal_address: Option
= + clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; + let dry_run = matches.is_present(DRY_RUN_FLAG); + let vc_url: Option = + clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?; + let vc_token_path: Option = + clap_utils::parse_optional(matches, VALIDATOR_CLIENT_TOKEN_FLAG)?; + let ignore_duplicates = matches.is_present(IGNORE_DUPLICATES_FLAG); + let keystore_upload_batch_size: usize = + clap_utils::parse_required(matches, KEYSTORE_UPLOAD_BATCH_SIZE)?; + + let http_client = match (dry_run, vc_url, vc_token_path) { + (false, Some(vc_url), Some(vc_token_path)) => { + let token_bytes = fs::read(&vc_token_path) + .map_err(|e| format!("Failed to read {:?}: {:?}", vc_token_path, e))?; + let token_string = String::from_utf8(token_bytes) + .map_err(|e| format!("Failed to parse {:?} as utf8: {:?}", vc_token_path, e))?; + let http_client = ValidatorClientHttpClient::new(vc_url.clone(), token_string) + .map_err(|e| { + format!( + "Could not instantiate HTTP client from URL and secret: {:?}", + e + ) + })?; + + // Perform a request to check that the connection works + let remote_keystores = http_client + .get_keystores() + .await + .map_err(|e| format!("Failed to list keystores on VC: {:?}", e))?; + eprintln!( + "Validator client is reachable at {} and reports {} validators", + vc_url, + remote_keystores.data.len() + ); + + Some(http_client) + } + (true, None, None) => None, + _ => { + return Err(format!( + "Inconsistent use of {}, {} and {} flags", + DRY_RUN_FLAG, VALIDATOR_CLIENT_URL_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG + )) + } + }; + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; + // A random password is always appropriate for the wallet since it is ephemeral. + let wallet_password = random_password(); + let voting_keystore_password = if let Some(path) = wallet_password_path { + read_password(&path) + .map_err(|e| format!("Failed to read password from {:?}: {:?}", path, e))? + } else { + random_password() + }; + // A random password is always appropriate for the withdrawal keystore since we don't ever store + // it anywhere. + let withdrawal_keystore_password = random_password(); + + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_bytes(), "".to_string()) + .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? + .build() + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + wallet + .set_nextaccount(first_index) + .map_err(|e| format!("Failure to set --{}: {:?}", FIRST_INDEX_FLAG, e))?; + + let mut voting_keystores = Vec::with_capacity(count as usize); + let mut json_deposits = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); + + eprintln!("Starting key generation. Each validator may take several seconds."); + + for i in 0..count { + let keystores = wallet + .next_validator( + wallet_password.as_bytes(), + voting_keystore_password.as_bytes(), + withdrawal_keystore_password.as_bytes(), + ) + .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; + let voting_keystore = keystores.voting; + let voting_keypair = voting_keystore + .decrypt_keypair(voting_keystore_password.as_bytes()) + .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; + let voting_pubkey_bytes = voting_keypair.pk.clone().into(); + + // Check to see if this validator already exists in the VC. + if let Some(http_client) = &http_client { + let remote_keystores = http_client + .get_keystores() + .await + .map_err(|e| format!("Failed to list keystores on VC: {:?}", e))?; + + if remote_keystores + .data + .iter() + .find(|keystore| keystore.validating_pubkey == voting_pubkey_bytes) + .is_some() + { + if ignore_duplicates { + eprintln!( + "Validator {:?} already exists in the VC, be cautious of submitting \ + duplicate deposits", + IGNORE_DUPLICATES_FLAG + ); + } else { + return Err(format!( + "Duplicate validator {:?} detected, see --{} for more information", + voting_keypair.pk, IGNORE_DUPLICATES_FLAG + )); + } + } + } + + let withdrawal_credentials = if let Some(eth1_withdrawal_address) = eth1_withdrawal_address + { + WithdrawalCredentials::eth1(eth1_withdrawal_address, &spec) + } else { + let withdrawal_keypair = keystores + .withdrawal + .decrypt_keypair(withdrawal_keystore_password.as_bytes()) + .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; + WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec) + }; + + if let Some(json_deposits) = &mut json_deposits { + let json_deposit = StandardDepositDataJson::new( + &voting_keypair, + withdrawal_credentials.into(), + deposit_gwei, + &spec, + )?; + + json_deposits.push(json_deposit); + } + + eprintln!( + "{}/{}: {:?}", + i.saturating_add(1), + count, + &voting_keypair.pk + ); + + voting_keystores.push(voting_keystore); + } + + eprintln!( + "Generated {} keystores. Starting to submit keystores to VC, \ + each keystore may take several seconds", + count + ); + + for voting_keystore_chunk in voting_keystores.chunks(keystore_upload_batch_size) { + todo!("submit to VC") + } + + todo!(); +} diff --git a/validator_manager/src/validator/mod.rs b/validator_manager/src/validator/mod.rs new file mode 100644 index 0000000000..c778f394a4 --- /dev/null +++ b/validator_manager/src/validator/mod.rs @@ -0,0 +1,26 @@ +pub mod create; + +use clap::{App, ArgMatches}; +use environment::Environment; +use types::EthSpec; + +pub const CMD: &str = "validator"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about("Provides commands for managing validators in a Lighthouse Validator Client.") + .subcommand(create::cli_app()) +} + +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + env: Environment, +) -> Result<(), String> { + match matches.subcommand() { + (create::CMD, Some(matches)) => create::cli_run::(matches, env).await, + (unknown, _) => Err(format!( + "{} does not have a {} command. See --help", + CMD, unknown + )), + } +}