mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-11 04:31:51 +00:00
340 lines
14 KiB
Rust
340 lines
14 KiB
Rust
use super::common::*;
|
|
use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user};
|
|
use clap::{App, Arg, ArgMatches};
|
|
use environment::Environment;
|
|
use eth2::lighthouse_vc::std_types::KeystoreJsonStr;
|
|
use eth2_wallet::WalletBuilder;
|
|
use serde::Serialize;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use types::*;
|
|
|
|
pub const CMD: &str = "create";
|
|
pub const OUTPUT_PATH_FLAG: &str = "output-path";
|
|
pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei";
|
|
pub const DISABLE_DEPOSITS_FLAG: &str = "disable-deposits";
|
|
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 SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password";
|
|
pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address";
|
|
pub const GAS_LIMIT_FLAG: &str = "gas-limit";
|
|
pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient";
|
|
pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals";
|
|
|
|
pub const VALIDATORS_FILENAME: &str = "validators.json";
|
|
pub const DEPOSITS_FILENAME: &str = "deposits.json";
|
|
|
|
struct ValidatorsAndDeposits {
|
|
validators: Vec<ValidatorSpecification>,
|
|
deposits: Option<Vec<StandardDepositDataJson>>,
|
|
}
|
|
|
|
pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
|
App::new(CMD)
|
|
.about(
|
|
"Creates new validators from BIP-39 mnemonic. A JSON file will be created which \
|
|
contains all the validator keystores and other validator data. This file can then \
|
|
be imported to a validator client using the \"import-validators\" 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::with_name(OUTPUT_PATH_FLAG)
|
|
.long(OUTPUT_PATH_FLAG)
|
|
.value_name("DIRECTORY")
|
|
.help(
|
|
"The path to a directory where the validator and (optionally) deposits \
|
|
files will be created. The directory will be created if it does not exist.",
|
|
)
|
|
.conflicts_with(DISABLE_DEPOSITS_FLAG)
|
|
.required(true)
|
|
.takes_value(true),
|
|
)
|
|
.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)",
|
|
)
|
|
.conflicts_with(DISABLE_DEPOSITS_FLAG)
|
|
.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(DISABLE_DEPOSITS_FLAG)
|
|
.long(DISABLE_DEPOSITS_FLAG)
|
|
.value_name("PATH")
|
|
.help(
|
|
"When provided don't generate the deposits JSON file that is \
|
|
commonly used for submitting validator deposits via a web UI. \
|
|
Using this flag will save several seconds per validator if the \
|
|
user has an alternate strategy for submitting deposits.",
|
|
),
|
|
)
|
|
.arg(
|
|
Arg::with_name(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG)
|
|
.long(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG)
|
|
.value_name("STRING")
|
|
.takes_value(true)
|
|
.help(
|
|
"If present, the user will be prompted to enter the voting keystore \
|
|
password that will be used to encrypt the voting keystores. If this \
|
|
flag is not provided, a random password will be used. It is not \
|
|
necessary to keep backups of voting keystore passwords if the \
|
|
mnemonic is safely backed up.",
|
|
),
|
|
)
|
|
.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.",
|
|
)
|
|
.conflicts_with(DISABLE_DEPOSITS_FLAG)
|
|
.takes_value(true),
|
|
)
|
|
.arg(
|
|
Arg::with_name(GAS_LIMIT_FLAG)
|
|
.long(GAS_LIMIT_FLAG)
|
|
.value_name("UINT64")
|
|
.help(
|
|
"All created validators will use this gas limit. It is recommended \
|
|
to leave this as the default value by not specifying this flag.",
|
|
)
|
|
.required(false)
|
|
.takes_value(true),
|
|
)
|
|
.arg(
|
|
Arg::with_name(FEE_RECIPIENT_FLAG)
|
|
.long(FEE_RECIPIENT_FLAG)
|
|
.value_name("ETH1_ADDRESS")
|
|
.help(
|
|
"All created validators will use this value for the suggested \
|
|
fee recipient. Omit this flag to use the default value from the VC.",
|
|
)
|
|
.required(false)
|
|
.takes_value(true),
|
|
)
|
|
.arg(
|
|
Arg::with_name(BUILDER_PROPOSALS_FLAG)
|
|
.long(BUILDER_PROPOSALS_FLAG)
|
|
.help(
|
|
"When provided, all created validators will attempt to create \
|
|
blocks via builder rather than the local EL.",
|
|
)
|
|
.required(false),
|
|
)
|
|
}
|
|
|
|
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 output_path: PathBuf = clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?;
|
|
|
|
if !output_path.exists() {
|
|
fs::create_dir(&output_path)
|
|
.map_err(|e| format!("Failed to create {:?} directory: {:?}", output_path, e))?;
|
|
} else if !output_path.is_dir() {
|
|
return Err(format!("{:?} must be a directory", output_path));
|
|
}
|
|
|
|
let validators_path = output_path.join(VALIDATORS_FILENAME);
|
|
if validators_path.exists() {
|
|
return Err(format!(
|
|
"{:?} already exists, refusing to overwrite",
|
|
validators_path
|
|
));
|
|
}
|
|
let deposits_path = output_path.join(DEPOSITS_FILENAME);
|
|
if deposits_path.exists() {
|
|
return Err(format!(
|
|
"{:?} already exists, refusing to overwrite",
|
|
deposits_path
|
|
));
|
|
}
|
|
|
|
let validators_and_deposits = build_validator_spec_from_cli(matches, spec)?;
|
|
|
|
write_to_json_file(&validators_path, &validators_and_deposits.validators)?;
|
|
|
|
if let Some(deposits) = &validators_and_deposits.deposits {
|
|
write_to_json_file(&deposits_path, deposits)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_to_json_file<P: AsRef<Path>, S: Serialize>(path: P, contents: &S) -> Result<(), String> {
|
|
let mut file = fs::OpenOptions::new()
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(&path)
|
|
.map_err(|e| format!("Failed to open {:?}: {:?}", path.as_ref(), e))?;
|
|
serde_json::to_writer(&mut file, contents)
|
|
.map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e))
|
|
}
|
|
|
|
fn build_validator_spec_from_cli<'a>(
|
|
matches: &'a ArgMatches<'a>,
|
|
spec: &ChainSpec,
|
|
) -> Result<ValidatorsAndDeposits, String> {
|
|
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 disable_deposits = matches.is_present(DISABLE_DEPOSITS_FLAG);
|
|
let specify_voting_keystore_password =
|
|
matches.is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG);
|
|
let eth1_withdrawal_address: Option<Address> =
|
|
clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?;
|
|
let builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG);
|
|
let fee_recipient: Option<Address> = clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?;
|
|
let gas_limit: Option<u64> = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?;
|
|
|
|
let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?;
|
|
let voting_keystore_password = if specify_voting_keystore_password {
|
|
eprintln!("Please enter a voting keystore password when prompted.");
|
|
Some(read_password_from_user(stdin_inputs)?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
/*
|
|
* Generate a wallet to be used for HD key generation.
|
|
*/
|
|
|
|
// A random password is always appropriate for the wallet since it is ephemeral.
|
|
let wallet_password = random_password_string();
|
|
// A random password is always appropriate for the withdrawal keystore since we don't ever store
|
|
// it anywhere.
|
|
let withdrawal_keystore_password = random_password_string();
|
|
let mut wallet =
|
|
WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string())
|
|
.map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))?
|
|
.build()
|
|
.map_err(|e| format!("Unable to create wallet: {:?}", e))?;
|
|
|
|
/*
|
|
* Start deriving individual validators.
|
|
*/
|
|
|
|
let mut validators = Vec::with_capacity(count as usize);
|
|
let mut deposits = disable_deposits.then(|| vec![]);
|
|
|
|
for (i, derivation_index) in (first_index..first_index + count).enumerate() {
|
|
// If the voting keystore password was not provided by the user then use a unique random
|
|
// string for each validator.
|
|
let voting_keystore_password = voting_keystore_password
|
|
.clone()
|
|
.unwrap_or_else(|| random_password_string());
|
|
|
|
// Set the wallet to the appropriate derivation index.
|
|
wallet
|
|
.set_nextaccount(derivation_index)
|
|
.map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?;
|
|
|
|
// Derive the keystore from the HD wallet.
|
|
let keystores = wallet
|
|
.next_validator(
|
|
wallet_password.as_ref(),
|
|
voting_keystore_password.as_ref(),
|
|
withdrawal_keystore_password.as_ref(),
|
|
)
|
|
.map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?;
|
|
let voting_keystore = keystores.voting;
|
|
|
|
if let Some(deposits) = &mut deposits {
|
|
// Decrypt the voting keystore so a deposit message can be signed.
|
|
let voting_keypair = voting_keystore
|
|
.decrypt_keypair(voting_keystore_password.as_ref())
|
|
.map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?;
|
|
|
|
let withdrawal_credentials = if let Some(eth1_withdrawal_address) =
|
|
eth1_withdrawal_address
|
|
{
|
|
WithdrawalCredentials::eth1(eth1_withdrawal_address, &spec)
|
|
} else {
|
|
// Decrypt the withdrawal keystore so withdrawal credentials can be created. It's
|
|
// not strictly necessary to decrypt the keystore since we can read the pubkey
|
|
// directly from the keystore. However we decrypt the keystore to be more certain
|
|
// that we have access to the withdrawal keys.
|
|
let withdrawal_keypair = keystores
|
|
.withdrawal
|
|
.decrypt_keypair(withdrawal_keystore_password.as_ref())
|
|
.map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?;
|
|
WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec)
|
|
};
|
|
|
|
// Create a JSON structure equivalent to the one generated by
|
|
// `ethereum/staking-deposit-cli`.
|
|
let json_deposit = StandardDepositDataJson::new(
|
|
&voting_keypair,
|
|
withdrawal_credentials.into(),
|
|
deposit_gwei,
|
|
&spec,
|
|
)?;
|
|
|
|
deposits.push(json_deposit);
|
|
}
|
|
|
|
let validator = ValidatorSpecification {
|
|
voting_keystore: KeystoreJsonStr(voting_keystore),
|
|
voting_keystore_password: voting_keystore_password.clone(),
|
|
// New validators have no slashing protection history.
|
|
slashing_protection: None,
|
|
fee_recipient,
|
|
gas_limit,
|
|
builder_proposals: Some(builder_proposals),
|
|
enabled: Some(true),
|
|
};
|
|
validators.push(validator);
|
|
}
|
|
|
|
Ok(ValidatorsAndDeposits {
|
|
validators,
|
|
deposits,
|
|
})
|
|
}
|