mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-20 21:34:46 +00:00
Add initial progress
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -87,6 +87,8 @@ members = [
|
||||
|
||||
"validator_client",
|
||||
"validator_client/slashing_protection",
|
||||
|
||||
"validator_manager",
|
||||
]
|
||||
|
||||
[patch]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
57
consensus/types/src/withdrawal_credentials.rs
Normal file
57
consensus/types/src/withdrawal_credentials.rs
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
22
validator_manager/Cargo.toml
Normal file
22
validator_manager/Cargo.toml
Normal 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"]}
|
||||
33
validator_manager/src/lib.rs
Normal file
33
validator_manager/src/lib.rs
Normal 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(())
|
||||
}
|
||||
392
validator_manager/src/validator/create.rs
Normal file
392
validator_manager/src/validator/create.rs
Normal 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!();
|
||||
}
|
||||
26
validator_manager/src/validator/mod.rs
Normal file
26
validator_manager/src/validator/mod.rs
Normal 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
|
||||
)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user