Add initial progress

This commit is contained in:
Paul Hauner
2022-08-16 15:48:14 +10:00
parent 66e24e27f3
commit 1984c6bcbe
13 changed files with 605 additions and 52 deletions

20
Cargo.lock generated
View File

@@ -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"

View File

@@ -87,6 +87,8 @@ members = [
"validator_client",
"validator_client/slashing_protection",
"validator_manager",
]
[patch]

View File

@@ -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<PathBuf>,
stdin_inputs: bool,
) -> Result<Mnemonic, String> {
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(

View File

@@ -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};

View File

@@ -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;

View File

@@ -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<P: AsRef<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<PathBuf>,
stdin_inputs: bool,
) -> Result<Mnemonic, String> {
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::*;

View File

@@ -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

View File

@@ -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;

View File

@@ -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<WithdrawalCredentials> 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()
)
}
}

View File

@@ -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"]}

View File

@@ -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<T>,
) -> 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(())
}

View File

@@ -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<Self, String> {
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<T>,
) -> 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<PathBuf> = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?;
let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG);
let json_deposit_data_path: Option<PathBuf> =
clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?;
let wallet_password_path: Option<PathBuf> = clap_utils::parse_optional(matches, PASSWORD_FLAG)?;
let eth1_withdrawal_address: Option<Address> =
clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?;
let dry_run = matches.is_present(DRY_RUN_FLAG);
let vc_url: Option<SensitiveUrl> =
clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?;
let vc_token_path: Option<PathBuf> =
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!();
}

View File

@@ -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<T>,
) -> Result<(), String> {
match matches.subcommand() {
(create::CMD, Some(matches)) => create::cli_run::<T>(matches, env).await,
(unknown, _) => Err(format!(
"{} does not have a {} command. See --help",
CMD, unknown
)),
}
}