mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-20 05:14:35 +00:00
Add initial progress
This commit is contained in:
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