From 87c9df7b093630400210d48da6867a0f58ffc8b5 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 15 Oct 2024 18:13:42 -0700 Subject: [PATCH 01/12] Account manager migrated to clap derive --- Cargo.lock | 4 + account_manager/Cargo.toml | 2 + account_manager/src/lib.rs | 54 ++- account_manager/src/validator/cli.rs | 334 ++++++++++++++++++ account_manager/src/validator/create.rs | 150 +++----- account_manager/src/validator/exit.rs | 75 +--- account_manager/src/validator/import.rs | 82 +---- account_manager/src/validator/list.rs | 7 - account_manager/src/validator/mod.rs | 88 ++--- account_manager/src/validator/modify.rs | 80 +---- account_manager/src/validator/recover.rs | 95 +---- .../src/validator/slashing_protection.rs | 69 +--- account_manager/src/wallet/cli.rs | 204 +++++++++++ account_manager/src/wallet/create.rs | 174 ++------- account_manager/src/wallet/list.rs | 5 - account_manager/src/wallet/mod.rs | 60 +--- account_manager/src/wallet/recover.rs | 69 +--- common/clap_utils/src/lib.rs | 15 + common/directory/src/lib.rs | 18 + common/eth2_wallet_manager/Cargo.toml | 2 + .../eth2_wallet_manager/src/wallet_manager.rs | 3 + lighthouse/src/cli.rs | 3 + lighthouse/src/main.rs | 10 +- 23 files changed, 814 insertions(+), 789 deletions(-) create mode 100644 account_manager/src/validator/cli.rs create mode 100644 account_manager/src/wallet/cli.rs diff --git a/Cargo.lock b/Cargo.lock index ecbfd0cb8d1..55afc77f9da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,8 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde", + "serde_json", "slashing_protection", "slot_clock", "tempfile", @@ -2659,8 +2661,10 @@ dependencies = [ name = "eth2_wallet_manager" version = "0.1.0" dependencies = [ + "clap", "eth2_wallet", "lockfile", + "serde", "tempfile", ] diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 7f2fa05a888..d60bde55413 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -24,6 +24,8 @@ account_utils = { workspace = true } slashing_protection = { workspace = true } eth2 = { workspace = true } safe_arith = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } slot_clock = { workspace = true } filesystem = { workspace = true } sensitive_url = { workspace = true } diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 534939cf6bd..acd959d5c2c 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -2,12 +2,11 @@ mod common; pub mod validator; pub mod wallet; -use clap::Arg; -use clap::ArgAction; use clap::ArgMatches; -use clap::Command; -use clap_utils::FLAG_HEADER; +use clap::Parser; use environment::Environment; +use serde::Deserialize; +use serde::Serialize; use types::EthSpec; pub const CMD: &str = "account_manager"; @@ -16,36 +15,31 @@ pub const VALIDATOR_DIR_FLAG: &str = "validator-dir"; pub const VALIDATOR_DIR_FLAG_ALIAS: &str = "validators-dir"; pub const WALLETS_DIR_FLAG: &str = "wallets-dir"; -pub fn cli_app() -> Command { - Command::new(CMD) - .visible_aliases(["a", "am", "account"]) - .about("Utilities for generating and managing Ethereum 2.0 accounts.") - .display_order(0) - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER), - ) - .subcommand(wallet::cli_app()) - .subcommand(validator::cli_app()) +#[derive(Clone, Deserialize, Serialize, Debug, Parser)] +#[clap(visible_aliases = &["a", "am", "account"], about = "Utilities for generating and managing Ethereum 2.0 accounts.")] +pub struct AccountManager { + #[clap(subcommand)] + pub subcommand: AccountManagerSubcommand, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(rename_all = "kebab-case")] +pub enum AccountManagerSubcommand { + Wallet(wallet::cli::Wallet), + Validator(validator::cli::Validator), } /// Run the account manager, returning an error if the operation did not succeed. -pub fn run(matches: &ArgMatches, env: Environment) -> Result<(), String> { - match matches.subcommand() { - Some((wallet::CMD, matches)) => wallet::cli_run(matches)?, - Some((validator::CMD, matches)) => validator::cli_run(matches, env)?, - Some((unknown, _)) => { - return Err(format!( - "{} is not a valid {} command. See --help.", - unknown, CMD - )); +pub fn run( + account_manager: &AccountManager, + matches: &ArgMatches, + env: Environment, +) -> Result<(), String> { + match &account_manager.subcommand { + AccountManagerSubcommand::Wallet(wallet_config) => wallet::cli_run(wallet_config, matches)?, + AccountManagerSubcommand::Validator(validator_config) => { + validator::cli_run(validator_config, matches, env)? } - _ => return Err("No subcommand provided, see --help for options".to_string()), } Ok(()) diff --git a/account_manager/src/validator/cli.rs b/account_manager/src/validator/cli.rs new file mode 100644 index 00000000000..07613af4189 --- /dev/null +++ b/account_manager/src/validator/cli.rs @@ -0,0 +1,334 @@ +use bls::PublicKey; +pub use clap::Parser; +use clap::Subcommand; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +use super::exit::DEFAULT_BEACON_NODE; + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Provides commands for managing Eth2 validators.")] +pub struct Validator { + #[clap( + long, + value_name = "VALIDATOR_DIRECTORY", + conflicts_with = "datadir", + help = "The path to search for validator directories. \ + Defaults to ~/.lighthouse/{network}/validators" + )] + pub validator_dir: Option, + #[clap(subcommand)] + pub subcommand: ValidatorSubcommand, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(rename_all = "kebab-case")] +pub enum ValidatorSubcommand { + Create(Create), + Exit(Exit), + Import(Import), + List(List), + Recover(Recover), + #[clap(subcommand)] + Modify(Modify), + #[clap(subcommand)] + SlashingProtection(SlashingProtection), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + about = "Creates new validators from an existing EIP-2386 wallet using the EIP-2333 HD key \ + derivation scheme." +)] +pub struct Create { + #[clap( + long, + value_name = "WALLET_NAME", + help = "Use the wallet identified by this name" + )] + pub wallet_name: Option, + + #[clap( + long, + value_name = "WALLET_PASSWORD_PATH", + help = "A path to a file containing the password which will unlock the wallet." + )] + pub wallet_password: Option, + + #[clap( + long, + value_name = "WALLETS_DIR", + conflicts_with = "datadir", + help = "A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets" + )] + pub wallets_dir: Option, + + #[clap( + long, + value_name = "SECRETS_DIR", + help = "The path where the validator keystore passwords will be stored. \ + Defaults to ~/.lighthouse/{network}/secrets", + conflicts_with = "datadir" + )] + pub secrets_dir: Option, + + #[clap( + long, + 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)" + )] + pub deposit_gwei: Option, + + #[clap( + long, + help = "If present, the withdrawal keystore will be stored alongside the voting \ + keypair. It is generally recommended to *not* store the withdrawal key and \ + instead generate them from the wallet seed when required." + )] + pub store_withdrawal_keystore: bool, + + #[clap( + long, + value_name = "VALIDATOR_COUNT", + conflicts_with = "at_most", + help = "The number of validators to create, regardless of how many already exist" + )] + pub count: Option, + + #[clap( + long, + value_name = "AT_MOST_VALIDATORS", + conflicts_with = "count", + help = "Observe the number of validators in --validator-dir, only creating enough to \ + reach the given count. Never deletes an existing validator." + )] + pub at_most: Option, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Submits a VoluntaryExit to the beacon chain for a given validator keystore.")] +pub struct Exit { + #[clap( + long, + value_name = "KEYSTORE_PATH", + help = "The path to the EIP-2335 voting keystore for the validator" + )] + pub keystore: PathBuf, + + #[clap( + long, + value_name = "PASSWORD_FILE_PATH", + help = "The path to the password file which unlocks the validator voting keystore" + )] + pub password_file: Option, + + #[clap( + long, + value_name = "NETWORK_ADDRESS", + default_value_t = String::from(DEFAULT_BEACON_NODE), + help = "Address to a beacon node HTTP API", + )] + pub beacon_node: String, + + #[clap( + long, + help = "Exits after publishing the voluntary exit without waiting for confirmation that the exit was included in the beacon chain" + )] + pub no_wait: bool, + + #[clap( + long, + help = "Exits without prompting for confirmation that you understand the implications of a voluntary exit. This should be used with caution" + )] + pub no_confirmation: bool, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + about = "Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \ + requesting passwords interactively. The directory flag provides a convenient \ + method for importing a directory of keys generated by the eth2-deposit-cli \ + Python utility." +)] +pub struct Import { + #[clap( + long, + value_name = "KEYSTORE_PATH", + help = "Path to a single keystore to be imported.", + conflicts_with = "directory", + required_unless_present = "directory" + )] + pub keystore: Option, + + #[clap( + long, + value_name = "KEYSTORES_DIRECTORY", + conflicts_with = "keystore", + required_unless_present = "keystore", + help = "Path to a directory which contains zero or more keystores \ + for import. This directory and all sub-directories will be \ + searched and any file name which contains 'keystore' and \ + has the '.json' extension will be attempted to be imported." + )] + pub directory: Option, + + #[clap( + long, + help = "If present, the same password will be used for all imported keystores." + )] + pub reuse_password: bool, + + #[clap( + long, + value_name = "KEYSTORE_PASSWORD_PATH", + requires = "reuse_password", + help = "The path to the file containing the password which will unlock all \ + keystores being imported. This flag must be used with `--reuse-password`. \ + The password will be copied to the `validator_definitions.yml` file, so after \ + import we strongly recommend you delete the file at KEYSTORE_PASSWORD_PATH." + )] + pub password_file: Option, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Lists the public keys of all validators.")] +pub struct List {} + +#[derive(Subcommand, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Modify validator status in validator_definitions.yml.")] +#[clap(rename_all = "snake_case")] +pub enum Modify { + Enable(Enable), + Disable(Disable), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Enable validator(s) in validator_definitions.yml.")] +pub struct Enable { + #[clap(long, value_name = "PUBKEY", help = "Validator pubkey to enable")] + pub pubkey: Option, + + #[clap( + long, + help = "Enable all validators in the validator directory", + conflicts_with = "pubkey" + )] + pub all: bool, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Disable validator(s) in validator_definitions.yml.")] +pub struct Disable { + #[clap(long, value_name = "PUBKEY", help = "Validator pubkey to disable")] + pub pubkey: Option, + + #[clap( + long, + help = "Disable all validators in the validator directory", + conflicts_with = "pubkey" + )] + pub all: bool, +} + +pub trait Modifiable { + fn get_pubkey(&self) -> Option; + fn is_all(&self) -> bool; +} + +impl Modifiable for &Enable { + fn get_pubkey(&self) -> Option { + self.pubkey.clone() + } + fn is_all(&self) -> bool { + self.all + } +} + +impl Modifiable for &Disable { + fn get_pubkey(&self) -> Option { + self.pubkey.clone() + } + fn is_all(&self) -> bool { + self.all + } +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + about = "Recovers validator private keys given a BIP-39 mnemonic phrase. \ + If you did not specify a `--first-index` or count `--count`, by default this will \ + only recover the keys associated with the validator at index 0 for an HD wallet \ + in accordance with the EIP-2333 spec." +)] +pub struct Recover { + #[clap( + long, + value_name = "FIRST_INDEX", + help = "The first of consecutive key indexes you wish to recover.", + default_value_t = 0 + )] + pub first_index: u32, + #[clap( + long, + value_name = "COUNT", + help = "The number of validator keys you wish to recover. Counted consecutively from the provided `--first_index`.", + default_value_t = 1 + )] + pub count: u32, + #[clap( + long, + value_name = "MNEMONIC_PATH", + help = "If present, the mnemonic will be read in from this file." + )] + pub mnemonic_path: Option, + #[clap( + long, + value_name = "SECRETS_DIR", + help = "The path where the validator keystore passwords will be stored. \ + Defaults to ~/.lighthouse/{network}/secrets" + )] + pub secrets_dir: Option, + #[clap( + long, + help = "If present, the withdrawal keystore will be stored alongside the voting \ + keypair. It is generally recommended to *not* store the withdrawal key and \ + instead generate them from the wallet seed when required." + )] + pub store_withdrawal_keystore: bool, +} + +#[derive(Subcommand, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Import or export slashing protection data to or from another client")] +pub enum SlashingProtection { + Import(SlashingProtectionImport), + Export(SlashingProtectionExport), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Import an interchange file")] +pub struct SlashingProtectionImport { + #[clap( + value_name = "FILE", + help = "The slashing protection interchange file to import (.json)" + )] + pub import_file: PathBuf, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Export an interchange file")] +pub struct SlashingProtectionExport { + #[clap( + value_name = "FILE", + help = "The filename to export the interchange file to" + )] + pub export_file: PathBuf, + #[clap( + long, + value_name = "PUBKEYS", + value_delimiter = ',', + help = "List of public keys to export history for. Keys should be 0x-prefixed, \ + comma-separated. All known keys will be exported if omitted" + )] + pub pubkeys: Option>, +} diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index ec5af1e2ece..48805577624 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,15 +1,14 @@ use crate::common::read_wallet_name_from_cli; -use crate::{SECRETS_DIR_FLAG, WALLETS_DIR_FLAG}; +use crate::wallet::create::MNEMONIC_TYPES; +use crate::WALLETS_DIR_FLAG; use account_utils::{ random_password, read_password_from_user, strip_off_newlines, validator_definitions, PlainText, STDIN_INPUTS_FLAG, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; -use directory::{ - ensure_dir_exists, parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR, -}; +use clap::ArgMatches; +use directory::{ensure_dir_exists, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; use environment::Environment; +use eth2_wallet::bip39::MnemonicType; use eth2_wallet_manager::WalletManager; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::ffi::OsStr; @@ -18,131 +17,47 @@ use std::path::{Path, PathBuf}; use types::EthSpec; use validator_dir::Builder as ValidatorDirBuilder; -pub const CMD: &str = "create"; -pub const WALLET_NAME_FLAG: &str = "wallet-name"; -pub const WALLET_PASSWORD_FLAG: &str = "wallet-password"; -pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; -pub const STORE_WITHDRAW_FLAG: &str = "store-withdrawal-keystore"; +use super::cli::Create; + pub const COUNT_FLAG: &str = "count"; pub const AT_MOST_FLAG: &str = "at-most"; pub const WALLET_PASSWORD_PROMPT: &str = "Enter your wallet's password:"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about( - "Creates new validators from an existing EIP-2386 wallet using the EIP-2333 HD key \ - derivation scheme.", - ) - .arg( - Arg::new(WALLET_NAME_FLAG) - .long(WALLET_NAME_FLAG) - .value_name("WALLET_NAME") - .help("Use the wallet identified by this name") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(WALLET_PASSWORD_FLAG) - .long(WALLET_PASSWORD_FLAG) - .value_name("WALLET_PASSWORD_PATH") - .help("A path to a file containing the password which will unlock the wallet.") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(WALLETS_DIR_FLAG) - .long(WALLETS_DIR_FLAG) - .value_name(WALLETS_DIR_FLAG) - .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets") - .action(ArgAction::Set) - .conflicts_with("datadir") - .display_order(0) - ) - .arg( - Arg::new(SECRETS_DIR_FLAG) - .long(SECRETS_DIR_FLAG) - .value_name("SECRETS_DIR") - .help( - "The path where the validator keystore passwords will be stored. \ - Defaults to ~/.lighthouse/{network}/secrets", - ) - .conflicts_with("datadir") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(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)", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(STORE_WITHDRAW_FLAG) - .long(STORE_WITHDRAW_FLAG) - .help( - "If present, the withdrawal keystore will be stored alongside the voting \ - keypair. It is generally recommended to *not* store the withdrawal key and \ - instead generate them from the wallet seed when required.", - ) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new(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") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(AT_MOST_FLAG) - .long(AT_MOST_FLAG) - .value_name("AT_MOST_VALIDATORS") - .help( - "Observe the number of validators in --validator-dir, only creating enough to \ - reach the given count. Never deletes an existing validator.", - ) - .conflicts_with("count") - .action(ArgAction::Set) - .display_order(0) - ) -} - pub fn cli_run( + create_config: &Create, matches: &ArgMatches, env: Environment, validator_dir: PathBuf, ) -> Result<(), String> { let spec = env.core_context().eth2_config.spec; - let name: Option = clap_utils::parse_optional(matches, WALLET_NAME_FLAG)?; + let name: Option = create_config.wallet_name.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); let wallet_base_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_WALLET_DIR) } else { - parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? + create_config + .wallets_dir + .clone() + .unwrap_or(PathBuf::from(DEFAULT_WALLET_DIR)) }; let secrets_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_SECRET_DIR) } else { - parse_path_or_default_with_flag(matches, SECRETS_DIR_FLAG, DEFAULT_SECRET_DIR)? + create_config + .secrets_dir + .clone() + .unwrap_or(PathBuf::from(DEFAULT_SECRET_DIR)) }; - let deposit_gwei = clap_utils::parse_optional(matches, DEPOSIT_GWEI_FLAG)? + let deposit_gwei = create_config + .deposit_gwei .unwrap_or(spec.max_effective_balance); - let count: Option = clap_utils::parse_optional(matches, COUNT_FLAG)?; - let at_most: Option = clap_utils::parse_optional(matches, AT_MOST_FLAG)?; + let count = create_config.count; + let at_most = create_config.at_most; // The command will always fail if the wallet dir does not exist. if !wallet_base_dir.exists() { @@ -185,8 +100,7 @@ pub fn cli_run( return Ok(()); } - let wallet_password_path: Option = - clap_utils::parse_optional(matches, WALLET_PASSWORD_FLAG)?; + let wallet_password_path = create_config.wallet_password.clone(); let wallet_name = read_wallet_name_from_cli(name, stdin_inputs)?; let wallet_password = read_wallet_password_from_cli(wallet_password_path, stdin_inputs)?; @@ -250,7 +164,7 @@ pub fn cli_run( .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) .create_eth1_tx_data(deposit_gwei, &spec) - .store_withdrawal_keystore(matches.get_flag(STORE_WITHDRAW_FLAG)) + .store_withdrawal_keystore(create_config.store_withdrawal_keystore) .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; @@ -299,3 +213,21 @@ pub fn read_wallet_password_from_cli( } } } + +pub fn validate_mnemonic_length(len: &str) -> Result<(), String> { + match len + .parse::() + .ok() + .and_then(|words| MnemonicType::for_word_count(words).ok()) + { + Some(_) => Ok(()), + None => Err(format!( + "Mnemonic length must be one of {}", + MNEMONIC_TYPES + .iter() + .map(|t| t.word_count().to_string()) + .collect::>() + .join(", ") + )), + } +} diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 3fb0e50d225..bc863426956 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -1,7 +1,7 @@ +use crate::validator::cli::Exit; use account_utils::STDIN_INPUTS_FLAG; use bls::{Keypair, PublicKey}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; +use clap::ArgMatches; use environment::Environment; use eth2::{ types::{GenesisData, StateId, ValidatorData, ValidatorId, ValidatorStatus}, @@ -17,76 +17,25 @@ use std::time::Duration; use tokio::time::sleep; use types::{ChainSpec, Epoch, EthSpec, VoluntaryExit}; -pub const CMD: &str = "exit"; -pub const KEYSTORE_FLAG: &str = "keystore"; -pub const PASSWORD_FILE_FLAG: &str = "password-file"; -pub const BEACON_SERVER_FLAG: &str = "beacon-node"; -pub const NO_WAIT: &str = "no-wait"; -pub const NO_CONFIRMATION: &str = "no-confirmation"; pub const PASSWORD_PROMPT: &str = "Enter the keystore password"; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; pub const CONFIRMATION_PHRASE: &str = "Exit my validator"; pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/voluntary-exit.html"; -pub fn cli_app() -> Command { - Command::new("exit") - .about("Submits a VoluntaryExit to the beacon chain for a given validator keystore.") - .arg( - Arg::new(KEYSTORE_FLAG) - .long(KEYSTORE_FLAG) - .value_name("KEYSTORE_PATH") - .help("The path to the EIP-2335 voting keystore for the validator") - .action(ArgAction::Set) - .required(true) - .display_order(0) - ) - .arg( - Arg::new(PASSWORD_FILE_FLAG) - .long(PASSWORD_FILE_FLAG) - .value_name("PASSWORD_FILE_PATH") - .help("The path to the password file which unlocks the validator voting keystore") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(BEACON_SERVER_FLAG) - .long(BEACON_SERVER_FLAG) - .value_name("NETWORK_ADDRESS") - .help("Address to a beacon node HTTP API") - .default_value(DEFAULT_BEACON_NODE) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(NO_WAIT) - .long(NO_WAIT) - .help("Exits after publishing the voluntary exit without waiting for confirmation that the exit was included in the beacon chain") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new(NO_CONFIRMATION) - .long(NO_CONFIRMATION) - .help("Exits without prompting for confirmation that you understand the implications of a voluntary exit. This should be used with caution") - .display_order(0) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - ) -} - -pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { - let keystore_path: PathBuf = clap_utils::parse_required(matches, KEYSTORE_FLAG)?; - let password_file_path: Option = - clap_utils::parse_optional(matches, PASSWORD_FILE_FLAG)?; - +pub fn cli_run( + exit_config: &Exit, + matches: &ArgMatches, + env: Environment, +) -> Result<(), String> { + let keystore_path: PathBuf = exit_config.keystore.clone(); + let password_file_path: Option = exit_config.password_file.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); - let no_wait = matches.get_flag(NO_WAIT); - let no_confirmation = matches.get_flag(NO_CONFIRMATION); + let no_wait = exit_config.no_wait; + let no_confirmation = exit_config.no_confirmation; let spec = env.eth2_config().spec.clone(); - let server_url: String = clap_utils::parse_required(matches, BEACON_SERVER_FLAG)?; + let server_url = exit_config.beacon_node.clone(); let client = BeaconNodeHttpClient::new( SensitiveUrl::parse(&server_url) .map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?, diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 19ab5ad60ac..818fc80c59e 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -1,4 +1,3 @@ -use crate::wallet::create::PASSWORD_FLAG; use account_utils::validator_definitions::SigningDefinition; use account_utils::{ eth2_keystore::Keystore, @@ -9,87 +8,32 @@ use account_utils::{ }, ZeroizeString, STDIN_INPUTS_FLAG, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; +use clap::ArgMatches; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::fs; use std::path::PathBuf; use std::thread::sleep; use std::time::Duration; -pub const CMD: &str = "import"; -pub const KEYSTORE_FLAG: &str = "keystore"; -pub const DIR_FLAG: &str = "directory"; -pub const REUSE_PASSWORD_FLAG: &str = "reuse-password"; +use super::cli::Import; pub const PASSWORD_PROMPT: &str = "Enter the keystore password, or press enter to omit it:"; pub const KEYSTORE_REUSE_WARNING: &str = "DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH \ ANOTHER CLIENT, OR YOU WILL GET SLASHED."; -pub fn cli_app() -> Command { - Command::new(CMD) - .about( - "Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \ - requesting passwords interactively. The directory flag provides a convenient \ - method for importing a directory of keys generated by the eth2-deposit-cli \ - Python utility.", - ) - .arg( - Arg::new(KEYSTORE_FLAG) - .long(KEYSTORE_FLAG) - .value_name("KEYSTORE_PATH") - .help("Path to a single keystore to be imported.") - .conflicts_with(DIR_FLAG) - .required_unless_present(DIR_FLAG) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(DIR_FLAG) - .long(DIR_FLAG) - .value_name("KEYSTORES_DIRECTORY") - .help( - "Path to a directory which contains zero or more keystores \ - for import. This directory and all sub-directories will be \ - searched and any file name which contains 'keystore' and \ - has the '.json' extension will be attempted to be imported.", - ) - .conflicts_with(KEYSTORE_FLAG) - .required_unless_present(KEYSTORE_FLAG) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(REUSE_PASSWORD_FLAG) - .long(REUSE_PASSWORD_FLAG) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .help("If present, the same password will be used for all imported keystores.") - .display_order(0), - ) - .arg( - Arg::new(PASSWORD_FLAG) - .long(PASSWORD_FLAG) - .value_name("KEYSTORE_PASSWORD_PATH") - .requires(REUSE_PASSWORD_FLAG) - .help( - "The path to the file containing the password which will unlock all \ - keystores being imported. This flag must be used with `--reuse-password`. \ - The password will be copied to the `validator_definitions.yml` file, so after \ - import we strongly recommend you delete the file at KEYSTORE_PASSWORD_PATH.", - ) - .action(ArgAction::Set) - .display_order(0), - ) -} +pub const KEYSTORE_FLAG: &str = "keystore"; +pub const DIR_FLAG: &str = "directory"; -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { - let keystore: Option = clap_utils::parse_optional(matches, KEYSTORE_FLAG)?; - let keystores_dir: Option = clap_utils::parse_optional(matches, DIR_FLAG)?; +pub fn cli_run( + import_config: &Import, + matches: &ArgMatches, + validator_dir: PathBuf, +) -> Result<(), String> { + let keystore = import_config.keystore.clone(); + let keystores_dir = import_config.directory.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); - let reuse_password = matches.get_flag(REUSE_PASSWORD_FLAG); - let keystore_password_path: Option = - clap_utils::parse_optional(matches, PASSWORD_FLAG)?; + let reuse_password = import_config.reuse_password; + let keystore_password_path = import_config.password_file.clone(); let mut defs = ValidatorDefinitions::open_or_create(&validator_dir) .map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?; diff --git a/account_manager/src/validator/list.rs b/account_manager/src/validator/list.rs index d082a49590b..52c108138c0 100644 --- a/account_manager/src/validator/list.rs +++ b/account_manager/src/validator/list.rs @@ -1,13 +1,6 @@ use account_utils::validator_definitions::ValidatorDefinitions; -use clap::Command; use std::path::PathBuf; -pub const CMD: &str = "list"; - -pub fn cli_app() -> Command { - Command::new(CMD).about("Lists the public keys of all validators.") -} - pub fn cli_run(validator_dir: PathBuf) -> Result<(), String> { let validator_definitions = ValidatorDefinitions::open(&validator_dir).map_err(|e| { format!( diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 61584cbfbb6..129dab983da 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -1,3 +1,4 @@ +pub mod cli; pub mod create; pub mod exit; pub mod import; @@ -6,74 +7,47 @@ pub mod modify; pub mod recover; pub mod slashing_protection; -use crate::{VALIDATOR_DIR_FLAG, VALIDATOR_DIR_FLAG_ALIAS}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; -use directory::{parse_path_or_default_with_flag, DEFAULT_VALIDATOR_DIR}; +use clap::ArgMatches; +use cli::Validator; +use directory::{parse_path_or_default_with_flag_v2, DEFAULT_VALIDATOR_DIR}; use environment::Environment; use std::path::PathBuf; use types::EthSpec; -pub const CMD: &str = "validator"; - -pub fn cli_app() -> Command { - Command::new(CMD) - .display_order(0) - .about("Provides commands for managing Eth2 validators.") - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - .global(true), - ) - .arg( - Arg::new(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .alias(VALIDATOR_DIR_FLAG_ALIAS) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path to search for validator directories. \ - Defaults to ~/.lighthouse/{network}/validators", - ) - .action(ArgAction::Set) - .conflicts_with("datadir"), - ) - .subcommand(create::cli_app()) - .subcommand(modify::cli_app()) - .subcommand(import::cli_app()) - .subcommand(list::cli_app()) - .subcommand(recover::cli_app()) - .subcommand(slashing_protection::cli_app()) - .subcommand(exit::cli_app()) -} - -pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { +pub fn cli_run( + validator_config: &Validator, + matches: &ArgMatches, + env: Environment, +) -> Result<(), String> { let validator_base_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_VALIDATOR_DIR) } else { - parse_path_or_default_with_flag(matches, VALIDATOR_DIR_FLAG, DEFAULT_VALIDATOR_DIR)? + parse_path_or_default_with_flag_v2( + matches, + validator_config.validator_dir.clone(), + DEFAULT_VALIDATOR_DIR, + )? }; eprintln!("validator-dir path: {:?}", validator_base_dir); - match matches.subcommand() { - Some((create::CMD, matches)) => create::cli_run::(matches, env, validator_base_dir), - Some((modify::CMD, matches)) => modify::cli_run(matches, validator_base_dir), - Some((import::CMD, matches)) => import::cli_run(matches, validator_base_dir), - Some((list::CMD, _)) => list::cli_run(validator_base_dir), - Some((recover::CMD, matches)) => recover::cli_run(matches, validator_base_dir), - Some((slashing_protection::CMD, matches)) => { - slashing_protection::cli_run(matches, env, validator_base_dir) + match &validator_config.subcommand { + cli::ValidatorSubcommand::Create(create_config) => { + create::cli_run::(create_config, matches, env, validator_base_dir) + } + cli::ValidatorSubcommand::Exit(exit_config) => exit::cli_run(exit_config, matches, env), + cli::ValidatorSubcommand::Import(import_config) => { + import::cli_run(import_config, matches, validator_base_dir) + } + cli::ValidatorSubcommand::List(_) => list::cli_run(validator_base_dir), + cli::ValidatorSubcommand::Recover(recover_config) => { + recover::cli_run(recover_config, matches, validator_base_dir) + } + cli::ValidatorSubcommand::Modify(modify_config) => { + modify::cli_run(modify_config, validator_base_dir) + } + cli::ValidatorSubcommand::SlashingProtection(slashing_protection_config) => { + slashing_protection::cli_run(slashing_protection_config, env, validator_base_dir) } - Some((exit::CMD, matches)) => exit::cli_run(matches, env), - Some((unknown, _)) => Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )), - _ => Err(format!("No command provided for {}. See --help", CMD)), } } diff --git a/account_manager/src/validator/modify.rs b/account_manager/src/validator/modify.rs index 571cd28bf5e..e3f40486142 100644 --- a/account_manager/src/validator/modify.rs +++ b/account_manager/src/validator/modify.rs @@ -1,91 +1,35 @@ use account_utils::validator_definitions::ValidatorDefinitions; use bls::PublicKey; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; use std::{collections::HashSet, path::PathBuf}; -pub const CMD: &str = "modify"; -pub const ENABLE: &str = "enable"; -pub const DISABLE: &str = "disable"; +use crate::validator::cli::Modifiable; -pub const PUBKEY_FLAG: &str = "pubkey"; -pub const ALL: &str = "all"; +use super::cli::Modify; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Modify validator status in validator_definitions.yml.") - .display_order(0) - .subcommand( - Command::new(ENABLE) - .about("Enable validator(s) in validator_definitions.yml.") - .arg( - Arg::new(PUBKEY_FLAG) - .long(PUBKEY_FLAG) - .value_name("PUBKEY") - .help("Validator pubkey to enable") - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(ALL) - .long(ALL) - .help("Enable all validators in the validator directory") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .conflicts_with(PUBKEY_FLAG) - .display_order(0), - ), - ) - .subcommand( - Command::new(DISABLE) - .about("Disable validator(s) in validator_definitions.yml.") - .arg( - Arg::new(PUBKEY_FLAG) - .long(PUBKEY_FLAG) - .value_name("PUBKEY") - .help("Validator pubkey to disable") - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(ALL) - .long(ALL) - .help("Disable all validators in the validator directory") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .conflicts_with(PUBKEY_FLAG) - .display_order(0), - ), - ) -} - -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { +pub fn cli_run(modify_config: &Modify, validator_dir: PathBuf) -> Result<(), String> { // `true` implies we are setting `validator_definition.enabled = true` and // vice versa. - let (enabled, sub_matches) = match matches.subcommand() { - Some((ENABLE, sub_matches)) => (true, sub_matches), - Some((DISABLE, sub_matches)) => (false, sub_matches), - Some((unknown, _)) => { - return Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )) - } - _ => return Err(format!("No command provided for {}. See --help", CMD)), + let (enabled, sub_matches) = match modify_config { + Modify::Enable(sub_matches) => (true, Box::new(sub_matches) as Box), + Modify::Disable(sub_matches) => (false, Box::new(sub_matches) as Box), }; + let mut defs = ValidatorDefinitions::open(&validator_dir).map_err(|e| { format!( "No validator definitions found in {:?}: {:?}", validator_dir, e ) })?; - let pubkeys_to_modify = if sub_matches.get_flag(ALL) { + + let pubkeys_to_modify = if sub_matches.is_all() { defs.as_slice() .iter() .map(|def| def.voting_public_key.clone()) .collect::>() } else { - let public_key: PublicKey = clap_utils::parse_required(sub_matches, PUBKEY_FLAG)?; + let public_key = sub_matches + .get_pubkey() + .ok_or_else(|| "Pubkey flag must be provided.".to_string())?; std::iter::once(public_key).collect::>() }; diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index ddf754edac9..69ee19e944d 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,92 +1,33 @@ -use super::create::STORE_WITHDRAW_FLAG; -use crate::validator::create::COUNT_FLAG; -use crate::SECRETS_DIR_FLAG; use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; use account_utils::{random_password, read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; +use clap::ArgMatches; use directory::ensure_dir_exists; -use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; +use directory::{parse_path_or_default_with_flag_v2, DEFAULT_SECRET_DIR}; use eth2_wallet::bip39::Seed; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; use std::path::PathBuf; use validator_dir::Builder as ValidatorDirBuilder; -pub const CMD: &str = "recover"; -pub const FIRST_INDEX_FLAG: &str = "first-index"; -pub const MNEMONIC_FLAG: &str = "mnemonic-path"; - -pub fn cli_app() -> Command { - Command::new(CMD) - .about( - "Recovers validator private keys given a BIP-39 mnemonic phrase. \ - If you did not specify a `--first-index` or count `--count`, by default this will \ - only recover the keys associated with the validator at index 0 for an HD wallet \ - in accordance with the EIP-2333 spec.") - .arg( - Arg::new(FIRST_INDEX_FLAG) - .long(FIRST_INDEX_FLAG) - .value_name("FIRST_INDEX") - .help("The first of consecutive key indexes you wish to recover.") - .action(ArgAction::Set) - .required(false) - .default_value("0") - .display_order(0) - ) - .arg( - Arg::new(COUNT_FLAG) - .long(COUNT_FLAG) - .value_name("COUNT") - .help("The number of validator keys you wish to recover. Counted consecutively from the provided `--first_index`.") - .action(ArgAction::Set) - .required(false) - .default_value("1") - .display_order(0) - ) - .arg( - Arg::new(MNEMONIC_FLAG) - .long(MNEMONIC_FLAG) - .value_name("MNEMONIC_PATH") - .help( - "If present, the mnemonic will be read in from this file.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(SECRETS_DIR_FLAG) - .long(SECRETS_DIR_FLAG) - .value_name("SECRETS_DIR") - .help( - "The path where the validator keystore passwords will be stored. \ - Defaults to ~/.lighthouse/{network}/secrets", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(STORE_WITHDRAW_FLAG) - .long(STORE_WITHDRAW_FLAG) - .help( - "If present, the withdrawal keystore will be stored alongside the voting \ - keypair. It is generally recommended to *not* store the withdrawal key and \ - instead generate them from the wallet seed when required.", - ) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) -} -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { +use super::cli::Recover; + +pub fn cli_run( + recover_config: &Recover, + matches: &ArgMatches, + validator_dir: PathBuf, +) -> Result<(), String> { let secrets_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_SECRET_DIR) } else { - parse_path_or_default_with_flag(matches, SECRETS_DIR_FLAG, DEFAULT_SECRET_DIR)? + parse_path_or_default_with_flag_v2( + matches, + recover_config.secrets_dir.clone(), + DEFAULT_SECRET_DIR, + )? }; - 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 first_index = recover_config.first_index; + let count = recover_config.count; + let mnemonic_path = recover_config.mnemonic_path.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); eprintln!("secrets-dir path: {:?}", secrets_dir); @@ -131,7 +72,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) - .store_withdrawal_keystore(matches.get_flag(STORE_WITHDRAW_FLAG)) + .store_withdrawal_keystore(recover_config.store_withdrawal_keystore) .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index bcd860a4847..55ae80f93ef 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -1,4 +1,3 @@ -use clap::{Arg, ArgAction, ArgMatches, Command}; use environment::Environment; use slashing_protection::{ interchange::Interchange, InterchangeError, InterchangeImportOutcome, SlashingDatabase, @@ -9,56 +8,12 @@ use std::path::PathBuf; use std::str::FromStr; use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; -pub const CMD: &str = "slashing-protection"; -pub const IMPORT_CMD: &str = "import"; -pub const EXPORT_CMD: &str = "export"; - -pub const IMPORT_FILE_ARG: &str = "IMPORT-FILE"; -pub const EXPORT_FILE_ARG: &str = "EXPORT-FILE"; +use super::cli::SlashingProtection; pub const PUBKEYS_FLAG: &str = "pubkeys"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Import or export slashing protection data to or from another client") - .display_order(0) - .subcommand( - Command::new(IMPORT_CMD) - .about("Import an interchange file") - .arg( - Arg::new(IMPORT_FILE_ARG) - .action(ArgAction::Set) - .value_name("FILE") - .display_order(0) - .help("The slashing protection interchange file to import (.json)"), - ) - ) - .subcommand( - Command::new(EXPORT_CMD) - .about("Export an interchange file") - .arg( - Arg::new(EXPORT_FILE_ARG) - .action(ArgAction::Set) - .value_name("FILE") - .help("The filename to export the interchange file to") - .display_order(0) - ) - .arg( - Arg::new(PUBKEYS_FLAG) - .long(PUBKEYS_FLAG) - .action(ArgAction::Set) - .value_name("PUBKEYS") - .help( - "List of public keys to export history for. Keys should be 0x-prefixed, \ - comma-separated. All known keys will be exported if omitted", - ) - .display_order(0) - ) - ) -} - pub fn cli_run( - matches: &ArgMatches, + slashing_protection_config: &SlashingProtection, env: Environment, validator_base_dir: PathBuf, ) -> Result<(), String> { @@ -71,9 +26,9 @@ pub fn cli_run( .genesis_validators_root::()? .ok_or_else(|| "Unable to get genesis state, has genesis occurred?".to_string())?; - match matches.subcommand() { - Some((IMPORT_CMD, matches)) => { - let import_filename: PathBuf = clap_utils::parse_required(matches, IMPORT_FILE_ARG)?; + match slashing_protection_config { + SlashingProtection::Import(import_config) => { + let import_filename = import_config.import_file.clone(); let import_file = File::open(&import_filename).map_err(|e| { format!( "Unable to open import file at {}: {:?}", @@ -172,15 +127,13 @@ pub fn cli_run( Ok(()) } - Some((EXPORT_CMD, matches)) => { - let export_filename: PathBuf = clap_utils::parse_required(matches, EXPORT_FILE_ARG)?; + SlashingProtection::Export(export_config) => { + let export_filename = export_config.export_file.clone(); - let selected_pubkeys = if let Some(pubkeys) = - clap_utils::parse_optional::(matches, PUBKEYS_FLAG)? - { + let selected_pubkeys = if let Some(pubkeys) = export_config.pubkeys.clone() { let pubkeys = pubkeys - .split(',') - .map(PublicKeyBytes::from_str) + .iter() + .map(|s| PublicKeyBytes::from_str(s)) .collect::, _>>() .map_err(|e| format!("Invalid --{} value: {:?}", PUBKEYS_FLAG, e))?; Some(pubkeys) @@ -219,7 +172,5 @@ pub fn cli_run( Ok(()) } - Some((command, _)) => Err(format!("No such subcommand `{}`", command)), - _ => Err("No subcommand provided, see --help for options".to_string()), } } diff --git a/account_manager/src/wallet/cli.rs b/account_manager/src/wallet/cli.rs new file mode 100644 index 00000000000..e4c2f81958b --- /dev/null +++ b/account_manager/src/wallet/cli.rs @@ -0,0 +1,204 @@ +use crate::common::read_wallet_name_from_cli; +use crate::wallet::create::read_new_wallet_password_from_cli; +use crate::WALLETS_DIR_FLAG; +use account_utils::{random_password, PlainText, STDIN_INPUTS_FLAG}; +use clap::ArgMatches; +pub use clap::Parser; +use eth2_wallet::bip39::Mnemonic; +use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType}; +use filesystem::create_with_600_perms; +use serde::{Deserialize, Serialize}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use super::create::validate_mnemonic_length; + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Manage wallets, from which validator keys can be derived.")] +pub struct Wallet { + #[clap( + long, + value_name = "WALLETS_DIRECTORY", + conflicts_with = "datadir", + help = "A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets" + )] + pub wallets_dir: Option, + #[clap(subcommand)] + pub subcommand: WalletSubcommand, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +pub enum WalletSubcommand { + Create(Create), + List(List), + Recover(Recover), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Creates a new HD (hierarchical-deterministic) EIP-2386 wallet.")] +pub struct Create { + #[clap( + long, + value_name = "WALLET_NAME", + help = "The wallet will be created with this name. It is not allowed to \ + create two wallets with the same name for the same --base-dir." + )] + pub name: Option, + + #[clap( + long, + value_name = "WALLET_PASSWORD_PATH", + 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." + )] + pub password_file: Option, + + #[clap( + long = "type", + value_name = "WALLET_TYPE", + value_enum, + default_value_t = WalletType::Hd, + help = "The type of wallet to create. Only HD (hierarchical-deterministic) \ + wallets are supported presently..", + )] + pub r#type: WalletType, + + #[clap( + long, + value_name = "MNEMONIC_PATH", + help = "If present, the mnemonic will be saved to this file. DO NOT SHARE THE MNEMONIC." + )] + pub mnemonic_output_path: Option, + + #[clap( + long, + value_parser = validate_mnemonic_length, + default_value_t = 24, + value_name = "MNEMONIC_LENGTH", + help = "The number of words to use for the mnemonic phrase.", + )] + pub mnemonic_length: usize, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Lists the names of all wallets.")] +pub struct List {} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Recovers an EIP-2386 wallet from a given a BIP-39 mnemonic phrase.")] +pub struct Recover { + #[clap( + long, + value_name = "WALLET_NAME", + help = "The wallet will be created with this name. It is not allowed to \ + create two wallets with the same name for the same --base-dir." + )] + pub name: Option, + + #[clap( + long, + value_name = "PASSWORD_FILE_PATH", + help = "This will be the new password for your recovered wallet. \ + 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." + )] + pub password_file: Option, + + #[clap( + long, + value_name = "MNEMONIC_PATH", + help = "If present, the mnemonic will be read in from this file." + )] + pub mnemonic_path: Option, + + #[clap( + long = "type", + value_name = "WALLET_TYPE", + value_enum, + default_value_t = WalletType::Hd, + help = "The type of wallet to create. Only HD (hierarchical-deterministic) \ + wallets are supported presently..", + )] + pub r#type: WalletType, +} + +pub trait NewWallet { + fn get_name(&self) -> Option; + fn get_password(&self) -> Option; + fn get_type(&self) -> WalletType; + fn create_wallet_from_mnemonic( + &self, + wallet_base_dir: &Path, + matches: &ArgMatches, + mnemonic: &Mnemonic, + ) -> Result { + let name: Option = self.get_name(); + let wallet_password_path: Option = self.get_password(); + let wallet_type: WalletType = self.get_type(); + let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); + + let mgr = WalletManager::open(wallet_base_dir) + .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; + + let wallet_password: PlainText = match wallet_password_path { + Some(path) => { + // Create a random password if the file does not exist. + if !path.exists() { + // To prevent users from accidentally supplying their password to the PASSWORD_FLAG and + // create a file with that name, we require that the password has a .pass suffix. + if path.extension() != Some(OsStr::new("pass")) { + return Err(format!( + "Only creates a password file if that file ends in .pass: {:?}", + path + )); + } + + create_with_600_perms(&path, random_password().as_bytes()) + .map_err(|e| format!("Unable to write to {:?}: {:?}", path, e))?; + } + read_new_wallet_password_from_cli(Some(path), stdin_inputs)? + } + None => read_new_wallet_password_from_cli(None, stdin_inputs)?, + }; + + let wallet_name = read_wallet_name_from_cli(name, stdin_inputs)?; + + let wallet = mgr + .create_wallet( + wallet_name, + wallet_type, + mnemonic, + wallet_password.as_bytes(), + ) + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + Ok(wallet) + } +} + +impl NewWallet for Create { + fn get_name(&self) -> Option { + self.name.clone() + } + fn get_password(&self) -> Option { + self.password_file.clone() + } + fn get_type(&self) -> WalletType { + self.r#type + } +} + +impl NewWallet for Recover { + fn get_name(&self) -> Option { + self.name.clone() + } + fn get_password(&self) -> Option { + self.password_file.clone() + } + fn get_type(&self) -> WalletType { + self.r#type + } +} diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index b22007050fd..120ea8e487e 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -1,19 +1,17 @@ -use crate::common::read_wallet_name_from_cli; -use crate::WALLETS_DIR_FLAG; +use crate::wallet::cli::NewWallet; use account_utils::{ - is_password_sufficiently_complex, random_password, read_password_from_user, strip_off_newlines, - STDIN_INPUTS_FLAG, + is_password_sufficiently_complex, read_password_from_user, strip_off_newlines, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; use eth2_wallet::{ bip39::{Language, Mnemonic, MnemonicType}, PlainText, }; -use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType}; use filesystem::create_with_600_perms; -use std::ffi::OsStr; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; + +use super::cli::Create; pub const CMD: &str = "create"; pub const HD_TYPE: &str = "hd"; @@ -33,97 +31,24 @@ pub const NEW_WALLET_PASSWORD_PROMPT: &str = "Enter a password for your new wallet that is at least 12 characters long:"; pub const RETYPE_PASSWORD_PROMPT: &str = "Please re-enter your wallet's new password:"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Creates a new HD (hierarchical-deterministic) EIP-2386 wallet.") - .arg( - Arg::new(NAME_FLAG) - .long(NAME_FLAG) - .value_name("WALLET_NAME") - .help( - "The wallet will be created with this name. It is not allowed to \ - create two wallets with the same name for the same --base-dir.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(PASSWORD_FLAG) - .long(PASSWORD_FLAG) - .value_name("WALLET_PASSWORD_PATH") - .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.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(TYPE_FLAG) - .long(TYPE_FLAG) - .value_name("WALLET_TYPE") - .help( - "The type of wallet to create. Only HD (hierarchical-deterministic) \ - wallets are supported presently..", - ) - .action(ArgAction::Set) - .value_parser([HD_TYPE]) - .default_value(HD_TYPE) - .display_order(0) - ) - .arg( - Arg::new(MNEMONIC_FLAG) - .long(MNEMONIC_FLAG) - .value_name("MNEMONIC_PATH") - .help( - "If present, the mnemonic will be saved to this file. DO NOT SHARE THE MNEMONIC.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(MNEMONIC_LENGTH_FLAG) - .long(MNEMONIC_LENGTH_FLAG) - .value_name("MNEMONIC_LENGTH") - .help("The number of words to use for the mnemonic phrase.") - .action(ArgAction::Set) - .value_parser(|len: &str| { - match len - .parse::() - .ok() - .and_then(|words| MnemonicType::for_word_count(words).ok()) - { - Some(_) => Ok(len.to_string()), - None => Err(format!( - "Mnemonic length must be one of {}", - MNEMONIC_TYPES - .iter() - .map(|t| t.word_count().to_string()) - .collect::>() - .join(", ") - )), - } - }) - .default_value("24") - .display_order(0) - ) -} - -pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { - let mnemonic_output_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; +pub fn cli_run( + create_config: &Create, + matches: &ArgMatches, + wallet_base_dir: PathBuf, +) -> Result<(), String> { + let mnemonic_output_path = create_config.mnemonic_output_path.clone(); // Create a new random mnemonic. // // The `tiny-bip39` crate uses `thread_rng()` for this entropy. - let mnemonic_length = clap_utils::parse_required(matches, MNEMONIC_LENGTH_FLAG)?; + let mnemonic_length = create_config.mnemonic_length; let mnemonic = Mnemonic::new( MnemonicType::for_word_count(mnemonic_length).expect("Mnemonic length already validated"), Language::English, ); - let wallet = create_wallet_from_mnemonic(matches, wallet_base_dir.as_path(), &mnemonic)?; + let wallet = + create_config.create_wallet_from_mnemonic(wallet_base_dir.as_path(), matches, &mnemonic)?; if let Some(path) = mnemonic_output_path { create_with_600_perms(&path, mnemonic.phrase().as_bytes()) @@ -154,57 +79,6 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str Ok(()) } -pub fn create_wallet_from_mnemonic( - matches: &ArgMatches, - wallet_base_dir: &Path, - mnemonic: &Mnemonic, -) -> Result { - let name: Option = clap_utils::parse_optional(matches, NAME_FLAG)?; - let wallet_password_path: Option = clap_utils::parse_optional(matches, PASSWORD_FLAG)?; - let type_field: String = clap_utils::parse_required(matches, TYPE_FLAG)?; - let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); - let wallet_type = match type_field.as_ref() { - HD_TYPE => WalletType::Hd, - unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)), - }; - - let mgr = WalletManager::open(wallet_base_dir) - .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; - - let wallet_password: PlainText = match wallet_password_path { - Some(path) => { - // Create a random password if the file does not exist. - if !path.exists() { - // To prevent users from accidentally supplying their password to the PASSWORD_FLAG and - // create a file with that name, we require that the password has a .pass suffix. - if path.extension() != Some(OsStr::new("pass")) { - return Err(format!( - "Only creates a password file if that file ends in .pass: {:?}", - path - )); - } - - create_with_600_perms(&path, random_password().as_bytes()) - .map_err(|e| format!("Unable to write to {:?}: {:?}", path, e))?; - } - read_new_wallet_password_from_cli(Some(path), stdin_inputs)? - } - None => read_new_wallet_password_from_cli(None, stdin_inputs)?, - }; - - let wallet_name = read_wallet_name_from_cli(name, stdin_inputs)?; - - let wallet = mgr - .create_wallet( - wallet_name, - wallet_type, - mnemonic, - wallet_password.as_bytes(), - ) - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; - Ok(wallet) -} - /// Used when a user is creating a new wallet. Read in a wallet password from a file if the password file /// path is provided. Otherwise, read from an interactive prompt using tty unless the `--stdin-inputs` /// flag is provided. This verifies the password complexity and verifies the password is correctly re-entered. @@ -245,3 +119,21 @@ pub fn read_new_wallet_password_from_cli( }, } } + +pub fn validate_mnemonic_length(len: &str) -> Result<(), String> { + match len + .parse::() + .ok() + .and_then(|words| MnemonicType::for_word_count(words).ok()) + { + Some(_) => Ok(()), + None => Err(format!( + "Mnemonic length must be one of {}", + MNEMONIC_TYPES + .iter() + .map(|t| t.word_count().to_string()) + .collect::>() + .join(", ") + )), + } +} diff --git a/account_manager/src/wallet/list.rs b/account_manager/src/wallet/list.rs index a551ffae128..4cb29231cd8 100644 --- a/account_manager/src/wallet/list.rs +++ b/account_manager/src/wallet/list.rs @@ -1,14 +1,9 @@ use crate::WALLETS_DIR_FLAG; -use clap::Command; use eth2_wallet_manager::WalletManager; use std::path::PathBuf; pub const CMD: &str = "list"; -pub fn cli_app() -> Command { - Command::new(CMD).about("Lists the names of all wallets.") -} - pub fn cli_run(wallet_base_dir: PathBuf) -> Result<(), String> { let mgr = WalletManager::open(wallet_base_dir) .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index 020858db772..bb797cb2fa0 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -1,61 +1,37 @@ +pub mod cli; pub mod create; pub mod list; pub mod recover; -use crate::WALLETS_DIR_FLAG; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; -use directory::{ensure_dir_exists, parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; +use clap::ArgMatches; +use cli::Wallet; +use directory::{ensure_dir_exists, parse_path_or_default_with_flag_v2, DEFAULT_WALLET_DIR}; use std::path::PathBuf; pub const CMD: &str = "wallet"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Manage wallets, from which validator keys can be derived.") - .display_order(0) - .arg( - Arg::new("help") - .long("help") - .short('h') - .help("Prints help information") - .action(ArgAction::HelpLong) - .display_order(0) - .help_heading(FLAG_HEADER) - .global(true) - ) - .arg( - Arg::new(WALLETS_DIR_FLAG) - .long(WALLETS_DIR_FLAG) - .value_name("WALLETS_DIRECTORY") - .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets") - .action(ArgAction::Set) - .conflicts_with("datadir"), - ) - .subcommand(create::cli_app()) - .subcommand(list::cli_app()) - .subcommand(recover::cli_app()) -} - -pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { +pub fn cli_run(wallet_config: &Wallet, matches: &ArgMatches) -> Result<(), String> { let wallet_base_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_WALLET_DIR) } else { - parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? + parse_path_or_default_with_flag_v2( + matches, + wallet_config.wallets_dir.clone(), + DEFAULT_WALLET_DIR, + )? }; ensure_dir_exists(&wallet_base_dir)?; eprintln!("wallet-dir path: {:?}", wallet_base_dir); - match matches.subcommand() { - Some((create::CMD, matches)) => create::cli_run(matches, wallet_base_dir), - Some((list::CMD, _)) => list::cli_run(wallet_base_dir), - Some((recover::CMD, matches)) => recover::cli_run(matches, wallet_base_dir), - Some((unknown, _)) => Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )), - _ => Err("No subcommand provided, see --help for options".to_string()), + match &wallet_config.subcommand { + cli::WalletSubcommand::Create(create_config) => { + create::cli_run(create_config, matches, wallet_base_dir) + } + cli::WalletSubcommand::List(_) => list::cli_run(wallet_base_dir), + cli::WalletSubcommand::Recover(recover_config) => { + recover::cli_run(recover_config, matches, wallet_base_dir) + } } } diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs index 766d5dbe0cb..38873729f9f 100644 --- a/account_manager/src/wallet/recover.rs +++ b/account_manager/src/wallet/recover.rs @@ -1,65 +1,19 @@ -use crate::wallet::create::create_wallet_from_mnemonic; -use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; +use crate::wallet::cli::NewWallet; use account_utils::{read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; use std::path::PathBuf; +use super::cli::Recover; + pub const CMD: &str = "recover"; pub const MNEMONIC_FLAG: &str = "mnemonic-path"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Recovers an EIP-2386 wallet from a given a BIP-39 mnemonic phrase.") - .arg( - Arg::new(NAME_FLAG) - .long(NAME_FLAG) - .value_name("WALLET_NAME") - .help( - "The wallet will be created with this name. It is not allowed to \ - create two wallets with the same name for the same --base-dir.", - ) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(PASSWORD_FLAG) - .long(PASSWORD_FLAG) - .value_name("PASSWORD_FILE_PATH") - .help( - "This will be the new password for your recovered wallet. \ - 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.", - ) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(MNEMONIC_FLAG) - .long(MNEMONIC_FLAG) - .value_name("MNEMONIC_PATH") - .help("If present, the mnemonic will be read in from this file.") - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(TYPE_FLAG) - .long(TYPE_FLAG) - .value_name("WALLET_TYPE") - .help( - "The type of wallet to create. Only HD (hierarchical-deterministic) \ - wallets are supported presently..", - ) - .action(ArgAction::Set) - .value_parser([HD_TYPE]) - .default_value(HD_TYPE) - .display_order(0), - ) -} - -pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { - let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; +pub fn cli_run( + recover_config: &Recover, + matches: &ArgMatches, + wallet_base_dir: PathBuf, +) -> Result<(), String> { + let mnemonic_path = recover_config.mnemonic_path.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); eprintln!(); @@ -68,7 +22,8 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; - let wallet = create_wallet_from_mnemonic(matches, wallet_base_dir.as_path(), &mnemonic) + let wallet = recover_config + .create_wallet_from_mnemonic(wallet_base_dir.as_path(), matches, &mnemonic) .map_err(|e| format!("Unable to create wallet: {:?}", e))?; println!("Your wallet has been successfully recovered."); diff --git a/common/clap_utils/src/lib.rs b/common/clap_utils/src/lib.rs index cba7399c9bf..d26d3da5e4c 100644 --- a/common/clap_utils/src/lib.rs +++ b/common/clap_utils/src/lib.rs @@ -87,6 +87,21 @@ pub fn parse_hardcoded_network( Eth2NetworkConfig::constant(network_name.as_str()) } +/// If `name` is in `matches`, parses the value as a path. Otherwise, attempts to find the user's +/// home directory and appends `default` to it. +pub fn parse_path_with_default_in_home_dir_v2( + path: Option, + default: PathBuf, +) -> Result { + if let Some(p) = path { + Ok(p) + } else { + dirs::home_dir() + .map(|home| home.join(default)) + .ok_or_else(|| "Unable to locate home directory.".to_string()) + } +} + /// If `name` is in `matches`, parses the value as a path. Otherwise, attempts to find the user's /// home directory and appends `default` to it. pub fn parse_path_with_default_in_home_dir( diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs index df03b4f9a4e..01b2e87a31a 100644 --- a/common/directory/src/lib.rs +++ b/common/directory/src/lib.rs @@ -73,6 +73,24 @@ pub fn parse_path_or_default_with_flag( ) } +/// If `arg` is in `matches`, parses the value as a path. +/// +/// Otherwise, attempts to find the default directory for the `testnet` from the `matches` +/// and appends `flag` to it. +pub fn parse_path_or_default_with_flag_v2( + matches: &ArgMatches, + dir: Option, + flag: &str, +) -> Result { + clap_utils::parse_path_with_default_in_home_dir_v2( + dir, + PathBuf::new() + .join(DEFAULT_ROOT_DIR) + .join(get_network_dir(matches)) + .join(flag), + ) +} + /// Get the approximate size of a directory and its contents. /// /// Will skip unreadable files, and files. Not 100% accurate if files are being created and deleted diff --git a/common/eth2_wallet_manager/Cargo.toml b/common/eth2_wallet_manager/Cargo.toml index f4717570653..61e347b4d19 100644 --- a/common/eth2_wallet_manager/Cargo.toml +++ b/common/eth2_wallet_manager/Cargo.toml @@ -9,6 +9,8 @@ edition = { workspace = true } [dependencies] eth2_wallet = { workspace = true } lockfile = { workspace = true } +serde = { workspace = true } +clap = { workspace = true, features = ["derive"]} [dev-dependencies] tempfile = { workspace = true } diff --git a/common/eth2_wallet_manager/src/wallet_manager.rs b/common/eth2_wallet_manager/src/wallet_manager.rs index 3dd419a48b5..3c95bfaadd6 100644 --- a/common/eth2_wallet_manager/src/wallet_manager.rs +++ b/common/eth2_wallet_manager/src/wallet_manager.rs @@ -2,8 +2,10 @@ use crate::{ filesystem::{create, Error as FilesystemError}, LockedWallet, }; +use clap::ValueEnum; use eth2_wallet::{bip39::Mnemonic, Error as WalletError, Uuid, Wallet, WalletBuilder}; use lockfile::LockfileError; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ffi::OsString; use std::fs::{create_dir_all, read_dir, File}; @@ -54,6 +56,7 @@ impl From for Error { /// Defines the type of an EIP-2386 wallet. /// /// Presently only `Hd` wallets are supported. +#[derive(Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize, Debug, ValueEnum)] pub enum WalletType { /// Hierarchical-deterministic. Hd, diff --git a/lighthouse/src/cli.rs b/lighthouse/src/cli.rs index 90d3e811ebc..387c6b2b0f9 100644 --- a/lighthouse/src/cli.rs +++ b/lighthouse/src/cli.rs @@ -1,3 +1,4 @@ +use account_manager::AccountManager; use clap::Parser; use database_manager::cli::DatabaseManager; use serde::{Deserialize, Serialize}; @@ -6,4 +7,6 @@ use serde::{Deserialize, Serialize}; pub enum LighthouseSubcommands { #[clap(name = "database_manager")] DatabaseManager(DatabaseManager), + #[clap(name = "account_manager")] + AccountManager(AccountManager), } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index aad8860fccb..46b487125c0 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -423,7 +423,6 @@ fn main() { .subcommand(beacon_node::cli_app()) .subcommand(boot_node::cli_app()) .subcommand(validator_client::cli_app()) - .subcommand(account_manager::cli_app()) .subcommand(validator_manager::cli_app()); let cli = LighthouseSubcommands::augment_subcommands(cli); @@ -660,18 +659,19 @@ fn run( (Some(_), Some(_)) => panic!("CLI prevents both --network and --testnet-dir"), }; - if let Some(sub_matches) = matches.subcommand_matches(account_manager::CMD) { + if let Ok(LighthouseSubcommands::AccountManager(account_manager_config)) = + LighthouseSubcommands::from_arg_matches(matches) + { eprintln!("Running account manager for {} network", network_name); // Pass the entire `environment` to the account manager so it can run blocking operations. - account_manager::run(sub_matches, environment)?; + account_manager::run(&account_manager_config, matches, environment)?; // Exit as soon as account manager returns control. return Ok(()); - } + }; if let Some(sub_matches) = matches.subcommand_matches(validator_manager::CMD) { eprintln!("Running validator manager for {} network", network_name); - // Pass the entire `environment` to the account manager so it can run blocking operations. validator_manager::run::(sub_matches, environment)?; From b61da17404c554c136b5e042c05eed7ff2cec856 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 15 Oct 2024 18:58:23 -0700 Subject: [PATCH 02/12] Fix tests --- account_manager/src/validator/create.rs | 4 ++++ account_manager/src/validator/import.rs | 3 ++- account_manager/src/validator/mod.rs | 2 ++ account_manager/src/validator/modify.rs | 7 +++++++ account_manager/src/validator/slashing_protection.rs | 2 ++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 48805577624..82605e832c6 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -21,7 +21,11 @@ use super::cli::Create; pub const COUNT_FLAG: &str = "count"; pub const AT_MOST_FLAG: &str = "at-most"; +pub const WALLET_NAME_FLAG: &str = "wallet-name"; +pub const WALLET_PASSWORD_FLAG: &str = "wallet-password"; pub const WALLET_PASSWORD_PROMPT: &str = "Enter your wallet's password:"; +pub const STORE_WITHDRAW_FLAG: &str = "store-withdrawal-keystore"; +pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; pub fn cli_run( create_config: &Create, diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 818fc80c59e..897fdd59ec7 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -20,9 +20,10 @@ use super::cli::Import; pub const PASSWORD_PROMPT: &str = "Enter the keystore password, or press enter to omit it:"; pub const KEYSTORE_REUSE_WARNING: &str = "DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH \ ANOTHER CLIENT, OR YOU WILL GET SLASHED."; - +pub const CMD: &str = "import"; pub const KEYSTORE_FLAG: &str = "keystore"; pub const DIR_FLAG: &str = "directory"; +pub const REUSE_PASSWORD_FLAG: &str = "reuse-password"; pub fn cli_run( import_config: &Import, diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 129dab983da..f1055e8e5e4 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -14,6 +14,8 @@ use environment::Environment; use std::path::PathBuf; use types::EthSpec; +pub const CMD: &str = "validator"; + pub fn cli_run( validator_config: &Validator, matches: &ArgMatches, diff --git a/account_manager/src/validator/modify.rs b/account_manager/src/validator/modify.rs index e3f40486142..f0016fd321d 100644 --- a/account_manager/src/validator/modify.rs +++ b/account_manager/src/validator/modify.rs @@ -6,6 +6,13 @@ use crate::validator::cli::Modifiable; use super::cli::Modify; +pub const CMD: &str = "modify"; +pub const ENABLE: &str = "enable"; +pub const DISABLE: &str = "disable"; + +pub const PUBKEY_FLAG: &str = "pubkey"; +pub const ALL: &str = "all"; + pub fn cli_run(modify_config: &Modify, validator_dir: PathBuf) -> Result<(), String> { // `true` implies we are setting `validator_definition.enabled = true` and // vice versa. diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index 55ae80f93ef..108900a9bc8 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -10,6 +10,8 @@ use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; use super::cli::SlashingProtection; +pub const IMPORT_CMD: &str = "import"; +pub const EXPORT_CMD: &str = "export"; pub const PUBKEYS_FLAG: &str = "pubkeys"; pub fn cli_run( From eda8eff8eecae236dd01bb2fbfebb4a43598a655 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 15 Oct 2024 21:13:01 -0700 Subject: [PATCH 03/12] Fix docs --- lighthouse/src/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lighthouse/src/cli.rs b/lighthouse/src/cli.rs index 387c6b2b0f9..99b39dd4d5e 100644 --- a/lighthouse/src/cli.rs +++ b/lighthouse/src/cli.rs @@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize}; #[derive(Parser, Clone, Deserialize, Serialize, Debug)] pub enum LighthouseSubcommands { - #[clap(name = "database_manager")] + #[clap(name = "database_manager", display_order = 0)] DatabaseManager(DatabaseManager), - #[clap(name = "account_manager")] + #[clap(name = "account_manager", display_order = 0)] AccountManager(AccountManager), } From d63f4676537055c51e8cf1e681ae0285afcf489e Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 15 Oct 2024 21:19:49 -0700 Subject: [PATCH 04/12] Fix test --- account_manager/src/validator/create.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 82605e832c6..040ad199f5f 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -218,12 +218,8 @@ pub fn read_wallet_password_from_cli( } } -pub fn validate_mnemonic_length(len: &str) -> Result<(), String> { - match len - .parse::() - .ok() - .and_then(|words| MnemonicType::for_word_count(words).ok()) - { +pub fn validate_mnemonic_length(len: usize) -> Result<(), String> { + match MnemonicType::for_word_count(len).ok() { Some(_) => Ok(()), None => Err(format!( "Mnemonic length must be one of {}", From 08c86f3f9c0cb0a3bbb2b41ca9ec99f207cd65c2 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Tue, 15 Oct 2024 23:08:37 -0700 Subject: [PATCH 05/12] Fix test --- account_manager/src/validator/create.rs | 16 --------------- account_manager/src/wallet/create.rs | 26 ++++++++++++++----------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 040ad199f5f..46c9e60ce9d 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,5 +1,4 @@ use crate::common::read_wallet_name_from_cli; -use crate::wallet::create::MNEMONIC_TYPES; use crate::WALLETS_DIR_FLAG; use account_utils::{ random_password, read_password_from_user, strip_off_newlines, validator_definitions, PlainText, @@ -8,7 +7,6 @@ use account_utils::{ use clap::ArgMatches; use directory::{ensure_dir_exists, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; use environment::Environment; -use eth2_wallet::bip39::MnemonicType; use eth2_wallet_manager::WalletManager; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::ffi::OsStr; @@ -217,17 +215,3 @@ pub fn read_wallet_password_from_cli( } } } - -pub fn validate_mnemonic_length(len: usize) -> Result<(), String> { - match MnemonicType::for_word_count(len).ok() { - Some(_) => Ok(()), - None => Err(format!( - "Mnemonic length must be one of {}", - MNEMONIC_TYPES - .iter() - .map(|t| t.word_count().to_string()) - .collect::>() - .join(", ") - )), - } -} diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index 120ea8e487e..cd7b147a776 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -1,4 +1,3 @@ -use crate::wallet::cli::NewWallet; use account_utils::{ is_password_sufficiently_complex, read_password_from_user, strip_off_newlines, }; @@ -11,6 +10,8 @@ use filesystem::create_with_600_perms; use std::fs; use std::path::PathBuf; +use crate::wallet::cli::NewWallet; + use super::cli::Create; pub const CMD: &str = "create"; @@ -20,6 +21,10 @@ pub const PASSWORD_FLAG: &str = "password-file"; pub const TYPE_FLAG: &str = "type"; pub const MNEMONIC_FLAG: &str = "mnemonic-output-path"; pub const MNEMONIC_LENGTH_FLAG: &str = "mnemonic-length"; +pub const NEW_WALLET_PASSWORD_PROMPT: &str = + "Enter a password for your new wallet that is at least 12 characters long:"; +pub const RETYPE_PASSWORD_PROMPT: &str = "Please re-enter your wallet's new password:"; + pub const MNEMONIC_TYPES: &[MnemonicType] = &[ MnemonicType::Words12, MnemonicType::Words15, @@ -27,9 +32,6 @@ pub const MNEMONIC_TYPES: &[MnemonicType] = &[ MnemonicType::Words21, MnemonicType::Words24, ]; -pub const NEW_WALLET_PASSWORD_PROMPT: &str = - "Enter a password for your new wallet that is at least 12 characters long:"; -pub const RETYPE_PASSWORD_PROMPT: &str = "Please re-enter your wallet's new password:"; pub fn cli_run( create_config: &Create, @@ -120,13 +122,15 @@ pub fn read_new_wallet_password_from_cli( } } -pub fn validate_mnemonic_length(len: &str) -> Result<(), String> { - match len - .parse::() - .ok() - .and_then(|words| MnemonicType::for_word_count(words).ok()) - { - Some(_) => Ok(()), +pub fn validate_mnemonic_length(len: &str) -> Result { + match len.parse::().ok().and_then(|words| { + if MnemonicType::for_word_count(words).is_ok() { + Some(words) + } else { + None + } + }) { + Some(words) => Ok(words), None => Err(format!( "Mnemonic length must be one of {}", MNEMONIC_TYPES From 5aa568a61c894c3caea388088ffd1ab410be7cc8 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 20 Jan 2025 12:38:58 +0700 Subject: [PATCH 06/12] sort and fmt --- account_manager/Cargo.toml | 4 ++-- common/eth2_wallet_manager/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 6be2f0b74df..937e7622b55 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -19,11 +19,11 @@ eth2_keystore = { workspace = true } eth2_network_config = { workspace = true } eth2_wallet = { workspace = true } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } -serde = { workspace = true } -serde_json = { workspace = true } filesystem = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } diff --git a/common/eth2_wallet_manager/Cargo.toml b/common/eth2_wallet_manager/Cargo.toml index b40c81f3691..78fab5515d3 100644 --- a/common/eth2_wallet_manager/Cargo.toml +++ b/common/eth2_wallet_manager/Cargo.toml @@ -6,10 +6,10 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { workspace = true, features = ["derive"] } eth2_wallet = { workspace = true } lockfile = { workspace = true } serde = { workspace = true } -clap = { workspace = true, features = ["derive"]} [dev-dependencies] tempfile = { workspace = true } From 2fedc212358042b0c5d0d3bca160d28d060f6e34 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 10 Feb 2025 18:28:38 +0200 Subject: [PATCH 07/12] Fmt --- account_manager/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 0b78e0d4ae2..acd959d5c2c 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -29,7 +29,6 @@ pub enum AccountManagerSubcommand { Validator(validator::cli::Validator), } - /// Run the account manager, returning an error if the operation did not succeed. pub fn run( account_manager: &AccountManager, From 7faed0940318d2dc183ad3843b6051185f08b774 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 24 Apr 2025 16:33:02 -0700 Subject: [PATCH 08/12] update comments --- common/clap_utils/src/lib.rs | 3 +-- common/directory/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/common/clap_utils/src/lib.rs b/common/clap_utils/src/lib.rs index 8bb3d66f0a5..d5f1a913819 100644 --- a/common/clap_utils/src/lib.rs +++ b/common/clap_utils/src/lib.rs @@ -57,8 +57,7 @@ pub fn parse_hardcoded_network( Eth2NetworkConfig::constant(network_name.as_str()) } -/// If `name` is in `matches`, parses the value as a path. Otherwise, attempts to find the user's -/// home directory and appends `default` to it. +/// If `path` is `Some`, return it, else return the default path pub fn parse_path_with_default_in_home_dir_v2( path: Option, default: PathBuf, diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs index 28a5f52dad2..b1a10f4c6f2 100644 --- a/common/directory/src/lib.rs +++ b/common/directory/src/lib.rs @@ -62,10 +62,10 @@ pub fn parse_path_or_default_with_flag( ) } -/// If `arg` is in `matches`, parses the value as a path. +/// If `dir` is `Some`, return it. /// -/// Otherwise, attempts to find the default directory for the `testnet` from the `matches` -/// and appends `flag` to it. +/// Otherwise, construct the default directory for the `testnet` from the `matches` +/// and append `flag` to it. pub fn parse_path_or_default_with_flag_v2( matches: &ArgMatches, dir: Option, From 32e572385289c68b33564dac7fb828c0b90c4c1c Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 24 Apr 2025 16:34:49 -0700 Subject: [PATCH 09/12] update comments --- account_manager/src/validator/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_manager/src/validator/cli.rs b/account_manager/src/validator/cli.rs index 1b329f19beb..b00ec2c51ff 100644 --- a/account_manager/src/validator/cli.rs +++ b/account_manager/src/validator/cli.rs @@ -147,7 +147,7 @@ pub struct Exit { long, help = "Only presign the voluntary exit message without publishing it" )] - pub presign: bool + pub presign: bool, } #[derive(Parser, Clone, Deserialize, Serialize, Debug)] From c0ce1cceeab464925ea9d7a5cc4e2316fe35f186 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 25 Apr 2025 08:35:39 -0700 Subject: [PATCH 10/12] remove dyn box, ensure consistency in fn defs --- account_manager/src/lib.rs | 2 +- account_manager/src/validator/modify.rs | 14 +++++--------- lighthouse/src/main.rs | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index acd959d5c2c..e2808dcb117 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -31,8 +31,8 @@ pub enum AccountManagerSubcommand { /// Run the account manager, returning an error if the operation did not succeed. pub fn run( - account_manager: &AccountManager, matches: &ArgMatches, + account_manager: &AccountManager, env: Environment, ) -> Result<(), String> { match &account_manager.subcommand { diff --git a/account_manager/src/validator/modify.rs b/account_manager/src/validator/modify.rs index f0016fd321d..fad86c7e697 100644 --- a/account_manager/src/validator/modify.rs +++ b/account_manager/src/validator/modify.rs @@ -2,8 +2,6 @@ use account_utils::validator_definitions::ValidatorDefinitions; use bls::PublicKey; use std::{collections::HashSet, path::PathBuf}; -use crate::validator::cli::Modifiable; - use super::cli::Modify; pub const CMD: &str = "modify"; @@ -16,9 +14,9 @@ pub const ALL: &str = "all"; pub fn cli_run(modify_config: &Modify, validator_dir: PathBuf) -> Result<(), String> { // `true` implies we are setting `validator_definition.enabled = true` and // vice versa. - let (enabled, sub_matches) = match modify_config { - Modify::Enable(sub_matches) => (true, Box::new(sub_matches) as Box), - Modify::Disable(sub_matches) => (false, Box::new(sub_matches) as Box), + let (enabled, pubkey_opt, is_all) = match modify_config { + Modify::Enable(enable) => (true, enable.pubkey.clone(), enable.all), + Modify::Disable(disable) => (false, disable.pubkey.clone(), disable.all), }; let mut defs = ValidatorDefinitions::open(&validator_dir).map_err(|e| { @@ -28,15 +26,13 @@ pub fn cli_run(modify_config: &Modify, validator_dir: PathBuf) -> Result<(), Str ) })?; - let pubkeys_to_modify = if sub_matches.is_all() { + let pubkeys_to_modify = if is_all { defs.as_slice() .iter() .map(|def| def.voting_public_key.clone()) .collect::>() } else { - let public_key = sub_matches - .get_pubkey() - .ok_or_else(|| "Pubkey flag must be provided.".to_string())?; + let public_key = pubkey_opt.ok_or_else(|| "Pubkey flag must be provided.".to_string())?; std::iter::once(public_key).collect::>() }; diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index fb91432f956..bdb60a2b4cf 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -748,7 +748,7 @@ fn run( Ok(LighthouseSubcommands::AccountManager(account_manager_config)) => { eprintln!("Running account manager for {} network", network_name); // Pass the entire `environment` to the account manager so it can run blocking operations. - account_manager::run(&account_manager_config, matches, environment)?; + account_manager::run(matches, &account_manager_config, environment)?; // Exit as soon as account manager returns control. return Ok(()); From 9cfcc5991d22049f683a8a91964f3985d19dbc10 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Fri, 25 Apr 2025 08:39:03 -0700 Subject: [PATCH 11/12] remove modifiable trait --- account_manager/src/validator/cli.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/account_manager/src/validator/cli.rs b/account_manager/src/validator/cli.rs index b00ec2c51ff..7ec6115e940 100644 --- a/account_manager/src/validator/cli.rs +++ b/account_manager/src/validator/cli.rs @@ -237,29 +237,6 @@ pub struct Disable { pub all: bool, } -pub trait Modifiable { - fn get_pubkey(&self) -> Option; - fn is_all(&self) -> bool; -} - -impl Modifiable for &Enable { - fn get_pubkey(&self) -> Option { - self.pubkey.clone() - } - fn is_all(&self) -> bool { - self.all - } -} - -impl Modifiable for &Disable { - fn get_pubkey(&self) -> Option { - self.pubkey.clone() - } - fn is_all(&self) -> bool { - self.all - } -} - #[derive(Parser, Clone, Deserialize, Serialize, Debug)] #[clap( about = "Recovers validator private keys given a BIP-39 mnemonic phrase. \ From 0e0171a0c5e86c2e22623b89bcc8baeaff69ec84 Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Thu, 8 Jan 2026 14:58:10 -0600 Subject: [PATCH 12/12] Revert --- .../http_api/src/inclusion_list_duties.rs | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 beacon_node/http_api/src/inclusion_list_duties.rs diff --git a/beacon_node/http_api/src/inclusion_list_duties.rs b/beacon_node/http_api/src/inclusion_list_duties.rs deleted file mode 100644 index 51e85fff733..00000000000 --- a/beacon_node/http_api/src/inclusion_list_duties.rs +++ /dev/null @@ -1,105 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use eth2::types::{self as api_types}; -use slot_clock::SlotClock; -use types::{Epoch, EthSpec, Hash256, InclusionListDuty}; -use bls::PublicKeyBytes; - -/// The struct that is returned to the requesting HTTP client. -type ApiDuties = api_types::DutiesResponse>; - -/// Handles a request from the HTTP API for inclusion list duties. -pub fn inclusion_list_duties( - request_epoch: Epoch, - request_indices: &[u64], - chain: &BeaconChain, -) -> Result { - let current_epoch = chain.epoch().map_err(warp_utils::reject::unhandled_error)?; - let request_indices = request_indices - .iter() - .map(|i| *i as usize) - .collect::>(); - let indices_and_pubkeys: Vec<(usize, PublicKeyBytes)> = chain - .validator_pubkey_bytes_many(&request_indices) - .map_err(|_| warp_utils::reject::custom_server_error("unable to fetch pubkey".into()))? - .into_iter() - .collect(); - - // Determine what the current epoch would be if we fast-forward our system clock by - // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. - // - // Most of the time, `tolerant_current_epoch` will be equal to `current_epoch`. However, during - // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerant_current_epoch` - // will equal `current_epoch + 1` - let tolerant_current_epoch = chain - .slot_clock - .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) - .ok_or_else(|| warp_utils::reject::custom_server_error("unable to read slot clock".into()))? - .epoch(T::EthSpec::slots_per_epoch()); - - if request_epoch == current_epoch - || request_epoch == current_epoch + 1 - || request_epoch == tolerant_current_epoch + 1 - { - let head_block_root = chain.canonical_head.cached_head().head_block_root(); - let (duties, dependent_root) = chain - .validator_inclusion_list_duties(&indices_and_pubkeys, request_epoch, head_block_root) - .map_err(warp_utils::reject::unhandled_error)?; - //.map_err(warp_utils::reject::beacon_chain_error)?; - convert_to_api_response(duties, &request_indices, dependent_root, chain) - } else if request_epoch > current_epoch + 1 { - Err(warp_utils::reject::custom_bad_request(format!( - "request epoch {} is more than one epoch past the current epoch {}", - request_epoch, current_epoch - ))) - } else { - // request_epoch < current_epoch - // - // TODO: support historical inclusion list duties requests - Err(warp_utils::reject::custom_bad_request(format!( - "request epoch {} is earlier than the current epoch {}", - request_epoch, current_epoch - ))) - } -} - -// TODO(focil) unused chain -/// Convert the internal representation of inclusion duties into the format returned to the HTTP -/// client. -fn convert_to_api_response( - duties: Vec>, - indices: &[usize], - dependent_root: Hash256, - _chain: &BeaconChain, -) -> Result { - // Protect against an inconsistent slot clock. - if duties.len() != indices.len() { - return Err(warp_utils::reject::custom_server_error(format!( - "duties length {} does not match indices length {}", - duties.len(), - indices.len() - ))); - } - - // TODO(focil) - // let usize_indices = indices.iter().map(|i| *i as usize).collect::>(); - // let index_to_pubkey_map = chain - // .validator_pubkey_bytes_many(indices) - // .map_err(warp_utils::reject::unhandled_error)?; - // .map_err(warp_utils::reject::beacon_chain_error)?; - - let data = duties - .into_iter() - .zip(indices) - .filter_map(|(duty_opt, _)| { - let duty = duty_opt?; - Some(duty) - }) - .collect::>(); - - // TODO(focil): account for optimistic execution - Ok(api_types::DutiesResponse { - dependent_root, - execution_optimistic: None, - data, - }) -}