From 9503e338b560056ce07b5041182781cb6ef8f416 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 Aug 2022 14:31:01 +1000 Subject: [PATCH 001/138] Add JSON deposit data to `create` --- Cargo.lock | 4 ++ account_manager/Cargo.toml | 2 + account_manager/src/validator/create.rs | 42 ++++++++++++++- common/validator_dir/Cargo.toml | 2 + common/validator_dir/src/validator_dir.rs | 65 ++++++++++++++++++++++- 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cd7ff2ff97..b8e830df83d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,8 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde", + "serde_json", "slashing_protection", "slot_clock", "tempfile", @@ -7342,10 +7344,12 @@ dependencies = [ "deposit_contract", "derivative", "eth2_keystore", + "eth2_serde_utils", "filesystem", "hex", "lockfile", "rand 0.8.5", + "serde", "tempfile", "tree_hash", "types", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index ce863f91477..7d90cbb427d 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -24,6 +24,8 @@ safe_arith = {path = "../consensus/safe_arith"} slot_clock = { path = "../common/slot_clock" } filesystem = { path = "../common/filesystem" } sensitive_url = { path = "../common/sensitive_url" } +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.58" [dev-dependencies] tempfile = "3.1.0" diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index bbd2cbc9991..17ff97c9eaa 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -22,6 +22,7 @@ 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"; +pub const JSON_DEPOSIT_DATA_PATH: &str = "json-deposit-data-path"; 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:"; @@ -110,6 +111,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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), + ) } pub fn cli_run( @@ -140,6 +152,9 @@ pub fn cli_run( let count: Option = clap_utils::parse_optional(matches, COUNT_FLAG)?; let at_most: Option = clap_utils::parse_optional(matches, AT_MOST_FLAG)?; + let json_deposit_data_path: Option = + clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; + // The command will always fail if the wallet dir does not exist. if !wallet_base_dir.exists() { return Err(format!( @@ -212,6 +227,8 @@ pub fn cli_run( ) })?; + let mut json_deposit_data = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); + for i in 0..n { let voting_password = random_password(); let withdrawal_password = random_password(); @@ -241,7 +258,7 @@ pub fn cli_run( ) })?; - ValidatorDirBuilder::new(validator_dir.clone()) + let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) @@ -250,9 +267,32 @@ pub fn cli_run( .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; + if let Some(json_deposit_data) = &mut json_deposit_data { + let standard_deposit_data_json = validator_dir + .standard_deposit_data_json(&spec) + .map_err(|e| format!("Unable to create standard JSON deposit data: {:?}", e))?; + json_deposit_data.push(standard_deposit_data_json); + } + println!("{}/{}\t{}", i + 1, n, voting_pubkey.as_hex_string()); } + // If configured, create a single JSON file which contains deposit data information for all + // validators. + if let Some(json_deposit_data_path) = json_deposit_data_path { + let json_deposit_data = + json_deposit_data.ok_or("Internal error: JSON deposit data is None")?; + + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&json_deposit_data_path) + .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; + + serde_json::to_writer(&mut file, &json_deposit_data) + .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; + } + Ok(()) } diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 0eba4cf2327..1ce0806a186 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -20,6 +20,8 @@ tree_hash = "0.4.1" hex = "0.4.2" derivative = "2.1.1" lockfile = { path = "../lockfile" } +serde = { version = "1.0.116", features = ["derive"] } +eth2_serde_utils = "0.1.1" [dev-dependencies] tempfile = "3.1.0" diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index cb1ddde24a4..0ef0cb590ef 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -6,11 +6,12 @@ use deposit_contract::decode_eth1_tx_data; use derivative::Derivative; use eth2_keystore::{Error as KeystoreError, Keystore, PlainText}; use lockfile::{Lockfile, LockfileError}; +use serde::{Deserialize, Serialize}; use std::fs::{read, write, File}; use std::io; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; -use types::{DepositData, Hash256, Keypair}; +use types::*; /// The file used to save the Eth1 transaction hash from a deposit. pub const ETH1_DEPOSIT_TX_HASH_FILE: &str = "eth1-deposit-tx-hash.txt"; @@ -41,6 +42,8 @@ pub enum Error { Eth1DepositRootMismatch, #[cfg(feature = "unencrypted_keys")] SszKeypairError(String), + DepositDataMissing, + ConfigNameUnspecified, } /// Information required to submit a deposit to the Eth1 deposit contract. @@ -54,6 +57,26 @@ pub struct Eth1DepositData { pub root: Hash256, } +/// 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, Deserialize)] +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, +} + /// Provides a wrapper around a directory containing validator information. /// /// Holds a lockfile in `self.dir` to attempt to prevent concurrent access from multiple @@ -203,6 +226,46 @@ impl ValidatorDir { root, })) } + + /// Calls `Self::eth1_deposit_data` and then builds a `StandardDepositDataJson` from the result. + /// + /// The provided `spec` must match the value that was used to create the deposit, otherwise + /// an inconsistent result may be returned. + pub fn standard_deposit_data_json( + &self, + spec: &ChainSpec, + ) -> Result { + let deposit_data = self.eth1_deposit_data()?.ok_or(Error::DepositDataMissing)?; + + let domain = spec.get_deposit_domain(); + let deposit_message_root = deposit_data + .deposit_data + .as_deposit_message() + .signing_root(domain); + + let deposit_data_root = deposit_data.deposit_data.tree_hash_root(); + + let DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + } = deposit_data.deposit_data; + + Ok(StandardDepositDataJson { + pubkey, + withdrawal_credentials, + amount, + signature, + fork_version: spec.genesis_fork_version, + eth2_network_name: spec + .config_name + .clone() + .ok_or(Error::ConfigNameUnspecified)?, + deposit_message_root, + deposit_data_root, + }) + } } /// Attempts to load and decrypt a Keypair given path to the keystore. From 5af83be8b830805a35118c10211e19ec5aa2b60b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 Aug 2022 15:11:42 +1000 Subject: [PATCH 002/138] Add deposit data to recover function --- account_manager/src/validator/mod.rs | 2 +- account_manager/src/validator/recover.rs | 53 +++++++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 4f1bde07952..dccddf866fb 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -53,7 +53,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< (modify::CMD, Some(matches)) => modify::cli_run(matches, validator_base_dir), (import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir), (list::CMD, Some(_)) => list::cli_run(validator_base_dir), - (recover::CMD, Some(matches)) => recover::cli_run(matches, validator_base_dir), + (recover::CMD, Some(matches)) => recover::cli_run::(matches, env, validator_base_dir), (slashing_protection::CMD, Some(matches)) => { slashing_protection::cli_run(matches, env, validator_base_dir) } diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index d9b05e7756e..881d7b7dde7 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -8,13 +8,18 @@ use account_utils::random_password; use clap::{App, Arg, ArgMatches}; use directory::ensure_dir_exists; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; +use environment::Environment; use eth2_wallet::bip39::Seed; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; +use std::fs; use std::path::PathBuf; +use types::*; 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 const JSON_DEPOSIT_DATA_PATH: &str = "json-deposit-data-path"; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) @@ -76,9 +81,26 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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), + ) } -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { +pub fn cli_run( + matches: &ArgMatches, + mut env: Environment, + validator_dir: PathBuf, +) -> Result<(), String> { + let spec = env.core_context().eth2_config.spec; + let secrets_dir = if matches.value_of("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_SECRET_DIR) @@ -89,6 +111,8 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?; let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); + let json_deposit_data_path: Option = + clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; eprintln!("secrets-dir path: {:?}", secrets_dir); @@ -103,6 +127,8 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let seed = Seed::new(&mnemonic, ""); + let mut json_deposit_data = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); + for index in first_index..first_index + count { let voting_password = random_password(); let withdrawal_password = random_password(); @@ -128,7 +154,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let voting_pubkey = keystores.voting.pubkey().to_string(); - ValidatorDirBuilder::new(validator_dir.clone()) + let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) @@ -136,6 +162,13 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; + if let Some(json_deposit_data) = &mut json_deposit_data { + let standard_deposit_data_json = validator_dir + .standard_deposit_data_json(&spec) + .map_err(|e| format!("Unable to create standard JSON deposit data: {:?}", e))?; + json_deposit_data.push(standard_deposit_data_json); + } + println!( "{}/{}\tIndex: {}\t0x{}", index - first_index, @@ -145,5 +178,21 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin ); } + // If configured, create a single JSON file which contains deposit data information for all + // validators. + if let Some(json_deposit_data_path) = json_deposit_data_path { + let json_deposit_data = + json_deposit_data.ok_or("Internal error: JSON deposit data is None")?; + + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&json_deposit_data_path) + .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; + + serde_json::to_writer(&mut file, &json_deposit_data) + .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; + } + Ok(()) } From e3b20209e32475cda3665d7acd2c458460605a0e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 Aug 2022 15:11:54 +1000 Subject: [PATCH 003/138] Revert "Add deposit data to recover function" This reverts commit 0e335bd11ef32412ef9802bbbb9a2ccdda148b45. --- account_manager/src/validator/mod.rs | 2 +- account_manager/src/validator/recover.rs | 53 +----------------------- 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index dccddf866fb..4f1bde07952 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -53,7 +53,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< (modify::CMD, Some(matches)) => modify::cli_run(matches, validator_base_dir), (import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir), (list::CMD, Some(_)) => list::cli_run(validator_base_dir), - (recover::CMD, Some(matches)) => recover::cli_run::(matches, env, validator_base_dir), + (recover::CMD, Some(matches)) => recover::cli_run(matches, validator_base_dir), (slashing_protection::CMD, Some(matches)) => { slashing_protection::cli_run(matches, env, validator_base_dir) } diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index 881d7b7dde7..d9b05e7756e 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -8,18 +8,13 @@ use account_utils::random_password; use clap::{App, Arg, ArgMatches}; use directory::ensure_dir_exists; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; -use environment::Environment; use eth2_wallet::bip39::Seed; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; -use std::fs; use std::path::PathBuf; -use types::*; 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 const JSON_DEPOSIT_DATA_PATH: &str = "json-deposit-data-path"; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) @@ -81,26 +76,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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), - ) } -pub fn cli_run( - matches: &ArgMatches, - mut env: Environment, - validator_dir: PathBuf, -) -> Result<(), String> { - let spec = env.core_context().eth2_config.spec; - +pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { let secrets_dir = if matches.value_of("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_SECRET_DIR) @@ -111,8 +89,6 @@ pub fn cli_run( let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?; let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); - let json_deposit_data_path: Option = - clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; eprintln!("secrets-dir path: {:?}", secrets_dir); @@ -127,8 +103,6 @@ pub fn cli_run( let seed = Seed::new(&mnemonic, ""); - let mut json_deposit_data = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); - for index in first_index..first_index + count { let voting_password = random_password(); let withdrawal_password = random_password(); @@ -154,7 +128,7 @@ pub fn cli_run( let voting_pubkey = keystores.voting.pubkey().to_string(); - let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) + ValidatorDirBuilder::new(validator_dir.clone()) .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) @@ -162,13 +136,6 @@ pub fn cli_run( .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; - if let Some(json_deposit_data) = &mut json_deposit_data { - let standard_deposit_data_json = validator_dir - .standard_deposit_data_json(&spec) - .map_err(|e| format!("Unable to create standard JSON deposit data: {:?}", e))?; - json_deposit_data.push(standard_deposit_data_json); - } - println!( "{}/{}\tIndex: {}\t0x{}", index - first_index, @@ -178,21 +145,5 @@ pub fn cli_run( ); } - // If configured, create a single JSON file which contains deposit data information for all - // validators. - if let Some(json_deposit_data_path) = json_deposit_data_path { - let json_deposit_data = - json_deposit_data.ok_or("Internal error: JSON deposit data is None")?; - - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&json_deposit_data_path) - .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; - - serde_json::to_writer(&mut file, &json_deposit_data) - .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; - } - Ok(()) } From ec339513c5cdbdef690bfde05d1ac710a38c6684 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 Aug 2022 15:12:40 +1000 Subject: [PATCH 004/138] Add incomplete test script --- scripts/staking_deposit_cli/.gitignore | 1 + scripts/staking_deposit_cli/test.py | 75 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 scripts/staking_deposit_cli/.gitignore create mode 100644 scripts/staking_deposit_cli/test.py diff --git a/scripts/staking_deposit_cli/.gitignore b/scripts/staking_deposit_cli/.gitignore new file mode 100644 index 00000000000..3fec32c8427 --- /dev/null +++ b/scripts/staking_deposit_cli/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/scripts/staking_deposit_cli/test.py b/scripts/staking_deposit_cli/test.py new file mode 100644 index 00000000000..6cd9bcfad5d --- /dev/null +++ b/scripts/staking_deposit_cli/test.py @@ -0,0 +1,75 @@ +import os +import shutil +import subprocess + +NUM_VALIDATORS=3 +TEST_MNEMONIC = "test test test test test test test test test test test waste" +WALLET_NAME="test_wallet" + +tmp_dir = os.path.join(".", "tmp") +mnemonic_path = os.path.join(tmp_dir, "mnemonic.txt") +lh_dir = os.path.join(tmp_dir, "lh") +lh_json_path = os.path.join(lh_dir, "deposit-data.json") +lh_wallet_password_path = os.path.join(lh_dir, "wallet.pass") +sdc_dir = os.path.join(tmp_dir, "sdc") + + +def setup(): + if os.path.exists(tmp_dir): + cleanup() + os.mkdir(tmp_dir) + with open(mnemonic_path, "x") as file: + file.write(TEST_MNEMONIC) + + +def cleanup(): + shutil.rmtree(tmp_dir) + + +def lighthouse_generate(network): + result = subprocess.run([ + "lighthouse", + "--network", + network, + "account", + "wallet", + "recover", + "--datadir", + str(lh_dir), + "--name", + WALLET_NAME, + "--mnemonic-path", + str(mnemonic_path), + "--password-file", + str(lh_wallet_password_path) + ]) + assert(result.returncode == 0) + + result = subprocess.run([ + "lighthouse", + "--network", + network, + "account", + "validator", + "create", + "--datadir", + str(lh_dir), + "--wallet-name", + WALLET_NAME, + "--wallet-password", + str(lh_wallet_password_path), + "--count", + str(NUM_VALIDATORS), + "--json-deposit-data-path", + str(lh_json_path) + ]) + assert(result.returncode == 0) + + +def test(network): + setup() + lighthouse_generate(network) + # cleanup() + + +test("mainnet") From 66e24e27f3ff26b3e5a636b30dd63fd1a7f8fc88 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 16 Aug 2022 13:14:49 +1000 Subject: [PATCH 005/138] Add changes to test.py --- scripts/staking_deposit_cli/test.py | 43 ++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/staking_deposit_cli/test.py b/scripts/staking_deposit_cli/test.py index 6cd9bcfad5d..8cc2d501fed 100644 --- a/scripts/staking_deposit_cli/test.py +++ b/scripts/staking_deposit_cli/test.py @@ -1,6 +1,8 @@ import os +import sys import shutil import subprocess +from subprocess import Popen, PIPE, STDOUT NUM_VALIDATORS=3 TEST_MNEMONIC = "test test test test test test test test test test test waste" @@ -12,12 +14,18 @@ lh_json_path = os.path.join(lh_dir, "deposit-data.json") lh_wallet_password_path = os.path.join(lh_dir, "wallet.pass") sdc_dir = os.path.join(tmp_dir, "sdc") +sdc_git_dir = os.path.join(sdc_dir, "staking-deposit-cli") def setup(): if os.path.exists(tmp_dir): cleanup() + os.mkdir(tmp_dir) + os.mkdir(lh_dir) + os.mkdir(sdc_dir) + + setup_sdc() with open(mnemonic_path, "x") as file: file.write(TEST_MNEMONIC) @@ -26,6 +34,38 @@ def cleanup(): shutil.rmtree(tmp_dir) +def setup_sdc(): + result = subprocess.run([ + "git", + "clone", + "--single-branch", + "https://github.com/ethereum/staking-deposit-cli.git", + str(sdc_git_dir) + ]) + assert(result.returncode == 0) + result = subprocess.run([ + "pip", + "install", + "-r", + "requirements.txt", + ], cwd=sdc_git_dir) + assert(result.returncode == 0) + result = subprocess.run([ + "python", + "setup.py", + "install", + ], cwd=sdc_git_dir) + assert(result.returncode == 0) + + +def sdc_generate(network): + p = Popen([ + '/bin/sh', + 'deposit.sh', + ], stdin=PIPE, cwd=sdc_git_dir) + p.communicate(input=TEST_MNEMONIC.encode('utf-8'))[0] + + def lighthouse_generate(network): result = subprocess.run([ "lighthouse", @@ -68,7 +108,8 @@ def lighthouse_generate(network): def test(network): setup() - lighthouse_generate(network) + sdc_generate(network) + #lighthouse_generate(network) # cleanup() From 1984c6bcbe5cf908f1b270ca9b811cd343ba19d7 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 16 Aug 2022 15:48:14 +1000 Subject: [PATCH 006/138] Add initial progress --- Cargo.lock | 20 + Cargo.toml | 2 + account_manager/src/common.rs | 50 +-- account_manager/src/validator/recover.rs | 3 +- account_manager/src/wallet/recover.rs | 2 +- common/account_utils/src/lib.rs | 45 ++ consensus/types/src/chain_spec.rs | 3 + consensus/types/src/lib.rs | 2 + consensus/types/src/withdrawal_credentials.rs | 57 +++ validator_manager/Cargo.toml | 22 + validator_manager/src/lib.rs | 33 ++ validator_manager/src/validator/create.rs | 392 ++++++++++++++++++ validator_manager/src/validator/mod.rs | 26 ++ 13 files changed, 605 insertions(+), 52 deletions(-) create mode 100644 consensus/types/src/withdrawal_credentials.rs create mode 100644 validator_manager/Cargo.toml create mode 100644 validator_manager/src/lib.rs create mode 100644 validator_manager/src/validator/create.rs create mode 100644 validator_manager/src/validator/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b8e830df83d..3941aa7ec3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7355,6 +7355,26 @@ dependencies = [ "types", ] +[[package]] +name = "validator_manager" +version = "0.1.0" +dependencies = [ + "account_utils", + "bls", + "clap", + "clap_utils", + "environment", + "eth2", + "eth2_keystore", + "eth2_network_config", + "eth2_serde_utils", + "eth2_wallet", + "serde", + "serde_json", + "tree_hash", + "types", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 415c721d99d..6ba70be57d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ members = [ "validator_client", "validator_client/slashing_protection", + + "validator_manager", ] [patch] diff --git a/account_manager/src/common.rs b/account_manager/src/common.rs index ce42615e507..0764db21f37 100644 --- a/account_manager/src/common.rs +++ b/account_manager/src/common.rs @@ -1,55 +1,7 @@ -use account_utils::PlainText; -use account_utils::{read_input_from_user, strip_off_newlines}; -use eth2_wallet::bip39::{Language, Mnemonic}; -use std::fs; -use std::path::PathBuf; -use std::str::from_utf8; -use std::thread::sleep; -use std::time::Duration; +use account_utils::read_input_from_user; -pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; pub const WALLET_NAME_PROMPT: &str = "Enter wallet name:"; -pub fn read_mnemonic_from_cli( - mnemonic_path: Option, - stdin_inputs: bool, -) -> Result { - let mnemonic = match mnemonic_path { - Some(path) => fs::read(&path) - .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) - .and_then(|bytes| { - let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); - let phrase = from_utf8(bytes_no_newlines.as_ref()) - .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; - Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { - format!( - "Unable to derive mnemonic from string {:?}: {:?}", - phrase, e - ) - }) - })?, - None => loop { - eprintln!(); - eprintln!("{}", MNEMONIC_PROMPT); - - let mnemonic = read_input_from_user(stdin_inputs)?; - - match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { - Ok(mnemonic_m) => { - eprintln!("Valid mnemonic provided."); - eprintln!(); - sleep(Duration::from_secs(1)); - break mnemonic_m; - } - Err(_) => { - eprintln!("Invalid mnemonic"); - } - } - }, - }; - Ok(mnemonic) -} - /// Reads in a wallet name from the user. If the `--wallet-name` flag is provided, use it. Otherwise /// read from an interactive prompt using tty unless the `--stdin-inputs` flag is provided. pub fn read_wallet_name_from_cli( diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index d9b05e7756e..33d3b189266 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,10 +1,9 @@ use super::create::STORE_WITHDRAW_FLAG; -use crate::common::read_mnemonic_from_cli; use crate::validator::create::COUNT_FLAG; use crate::wallet::create::STDIN_INPUTS_FLAG; use crate::SECRETS_DIR_FLAG; use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; -use account_utils::random_password; +use account_utils::{random_password, read_mnemonic_from_cli}; use clap::{App, Arg, ArgMatches}; use directory::ensure_dir_exists; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs index f107c3638cc..6e047aca8d2 100644 --- a/account_manager/src/wallet/recover.rs +++ b/account_manager/src/wallet/recover.rs @@ -1,6 +1,6 @@ -use crate::common::read_mnemonic_from_cli; use crate::wallet::create::{create_wallet_from_mnemonic, STDIN_INPUTS_FLAG}; use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; +use account_utils::read_mnemonic_from_cli; use clap::{App, Arg, ArgMatches}; use std::path::PathBuf; diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index 89de3803856..be3b9008388 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -13,6 +13,9 @@ use std::fs::{self, File}; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; +use std::str::from_utf8; +use std::thread::sleep; +use std::time::Duration; use zeroize::Zeroize; pub mod validator_definitions; @@ -30,6 +33,8 @@ pub const MINIMUM_PASSWORD_LEN: usize = 12; /// array of length 32. const DEFAULT_PASSWORD_LEN: usize = 48; +pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:"; + /// Returns the "default" path where a wallet should store its password file. pub fn default_wallet_password_path>(wallet_name: &str, secrets_dir: P) -> PathBuf { secrets_dir.as_ref().join(format!("{}.pass", wallet_name)) @@ -220,6 +225,46 @@ impl AsRef<[u8]> for ZeroizeString { } } +pub fn read_mnemonic_from_cli( + mnemonic_path: Option, + stdin_inputs: bool, +) -> Result { + let mnemonic = match mnemonic_path { + Some(path) => fs::read(&path) + .map_err(|e| format!("Unable to read {:?}: {:?}", path, e)) + .and_then(|bytes| { + let bytes_no_newlines: PlainText = strip_off_newlines(bytes).into(); + let phrase = from_utf8(bytes_no_newlines.as_ref()) + .map_err(|e| format!("Unable to derive mnemonic: {:?}", e))?; + Mnemonic::from_phrase(phrase, Language::English).map_err(|e| { + format!( + "Unable to derive mnemonic from string {:?}: {:?}", + phrase, e + ) + }) + })?, + None => loop { + eprintln!(); + eprintln!("{}", MNEMONIC_PROMPT); + + let mnemonic = read_input_from_user(stdin_inputs)?; + + match Mnemonic::from_phrase(mnemonic.as_str(), Language::English) { + Ok(mnemonic_m) => { + eprintln!("Valid mnemonic provided."); + eprintln!(); + sleep(Duration::from_secs(1)); + break mnemonic_m; + } + Err(_) => { + eprintln!("Invalid mnemonic"); + } + } + }, + }; + Ok(mnemonic) +} + #[cfg(test)] mod test { use super::*; diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b2ba24ac3ee..1aaefab0a91 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -71,6 +71,7 @@ pub struct ChainSpec { */ pub genesis_fork_version: [u8; 4], pub bls_withdrawal_prefix_byte: u8, + pub eth1_address_withdrawal_prefix_byte: u8, /* * Time parameters @@ -481,6 +482,7 @@ impl ChainSpec { */ genesis_fork_version: [0; 4], bls_withdrawal_prefix_byte: 0, + eth1_address_withdrawal_prefix_byte: 1, /* * Time parameters @@ -686,6 +688,7 @@ impl ChainSpec { */ genesis_fork_version: [0x00, 0x00, 0x00, 0x64], bls_withdrawal_prefix_byte: 0, + eth1_address_withdrawal_prefix_byte: 1, /* * Time parameters diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 32300173ebc..7112b2d01ca 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -66,6 +66,7 @@ pub mod sync_duty; pub mod validator; pub mod validator_subscription; pub mod voluntary_exit; +pub mod withdrawal_credentials; #[macro_use] pub mod slot_epoch_macros; pub mod config_and_preset; @@ -165,6 +166,7 @@ pub use crate::validator::Validator; pub use crate::validator_registration_data::*; pub use crate::validator_subscription::ValidatorSubscription; pub use crate::voluntary_exit::VoluntaryExit; +pub use crate::withdrawal_credentials::WithdrawalCredentials; pub type CommitteeIndex = u64; pub type Hash256 = H256; diff --git a/consensus/types/src/withdrawal_credentials.rs b/consensus/types/src/withdrawal_credentials.rs new file mode 100644 index 00000000000..8d42d4eafd4 --- /dev/null +++ b/consensus/types/src/withdrawal_credentials.rs @@ -0,0 +1,57 @@ +use crate::*; +use bls::get_withdrawal_credentials; + +pub struct WithdrawalCredentials(Hash256); + +impl WithdrawalCredentials { + pub fn bls(withdrawal_public_key: &PublicKey, spec: &ChainSpec) -> Self { + let withdrawal_credentials = + get_withdrawal_credentials(withdrawal_public_key, spec.bls_withdrawal_prefix_byte); + Self(Hash256::from_slice(&withdrawal_credentials)) + } + + pub fn eth1(withdrawal_address: Address, spec: &ChainSpec) -> Self { + let mut withdrawal_credentials = [0; 32]; + withdrawal_credentials[0] = spec.eth1_address_withdrawal_prefix_byte; + withdrawal_credentials[12..].copy_from_slice(withdrawal_address.as_bytes()); + Self(Hash256::from_slice(&withdrawal_credentials)) + } +} + +impl From for Hash256 { + fn from(withdrawal_credentials: WithdrawalCredentials) -> Self { + withdrawal_credentials.0 + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_utils::generate_deterministic_keypair; + use std::str::FromStr; + + #[test] + fn bls_withdrawal_credentials() { + let spec = &MainnetEthSpec::default_spec(); + let keypair = generate_deterministic_keypair(0); + let credentials = WithdrawalCredentials::bls(&keypair.pk, spec); + let manually_generated_credentials = + get_withdrawal_credentials(&keypair.pk, spec.bls_withdrawal_prefix_byte); + let hash: Hash256 = credentials.into(); + assert_eq!(hash[0], spec.bls_withdrawal_prefix_byte); + assert_eq!(hash.as_bytes(), &manually_generated_credentials); + } + + #[test] + fn eth1_withdrawal_credentials() { + let spec = &MainnetEthSpec::default_spec(); + let address = Address::from_str("0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b").unwrap(); + let credentials = WithdrawalCredentials::eth1(address, spec); + let hash: Hash256 = credentials.into(); + assert_eq!( + hash, + Hash256::from_str("0x01000000000000000000000025c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b") + .unwrap() + ) + } +} diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml new file mode 100644 index 00000000000..041d548bea8 --- /dev/null +++ b/validator_manager/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "validator_manager" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bls = { path = "../crypto/bls" } +clap = "2.33.3" +types = { path = "../consensus/types" } +environment = { path = "../lighthouse/environment" } +eth2_network_config = { path = "../common/eth2_network_config" } +clap_utils = { path = "../common/clap_utils" } +eth2_wallet = { path = "../crypto/eth2_wallet" } +eth2_keystore = { path = "../crypto/eth2_keystore" } +account_utils = { path = "../common/account_utils" } +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.58" +eth2_serde_utils = "0.1.1" +tree_hash = "0.4.1" +eth2 = { path = "../common/eth2", features = ["lighthouse"]} diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs new file mode 100644 index 00000000000..c5d41937b82 --- /dev/null +++ b/validator_manager/src/lib.rs @@ -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, +) -> 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(()) +} diff --git a/validator_manager/src/validator/create.rs b/validator_manager/src/validator/create.rs new file mode 100644 index 00000000000..324a5bc3a7c --- /dev/null +++ b/validator_manager/src/validator/create.rs @@ -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 { + 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, +) -> 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 = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; + let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); + let json_deposit_data_path: Option = + clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; + let wallet_password_path: Option = clap_utils::parse_optional(matches, PASSWORD_FLAG)?; + let eth1_withdrawal_address: Option
= + clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; + let dry_run = matches.is_present(DRY_RUN_FLAG); + let vc_url: Option = + clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?; + let vc_token_path: Option = + 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!(); +} diff --git a/validator_manager/src/validator/mod.rs b/validator_manager/src/validator/mod.rs new file mode 100644 index 00000000000..c778f394a4f --- /dev/null +++ b/validator_manager/src/validator/mod.rs @@ -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, +) -> Result<(), String> { + match matches.subcommand() { + (create::CMD, Some(matches)) => create::cli_run::(matches, env).await, + (unknown, _) => Err(format!( + "{} does not have a {} command. See --help", + CMD, unknown + )), + } +} From ff3a025f7e5896c30ba569f0f26ade5e15d22f90 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 16 Aug 2022 17:17:42 +1000 Subject: [PATCH 007/138] Add progress --- common/account_utils/src/lib.rs | 12 +++ validator_manager/src/validator/create.rs | 109 +++++++++++++++++----- 2 files changed, 100 insertions(+), 21 deletions(-) diff --git a/common/account_utils/src/lib.rs b/common/account_utils/src/lib.rs index be3b9008388..e566d7cdda3 100644 --- a/common/account_utils/src/lib.rs +++ b/common/account_utils/src/lib.rs @@ -64,6 +64,18 @@ pub fn read_password>(path: P) -> Result { fs::read(path).map(strip_off_newlines).map(Into::into) } +/// Reads a password file into a `ZeroizeString` struct, with new-lines removed. +pub fn read_password_string>(path: P) -> Result { + fs::read(path) + .map_err(|e| format!("Error opening file: {:?}", e)) + .map(strip_off_newlines) + .and_then(|bytes| { + String::from_utf8(bytes) + .map_err(|e| format!("Error decoding utf8: {:?}", e)) + .map(Into::into) + }) +} + /// Write a file atomically by using a temporary file as an intermediate. /// /// Care is taken to preserve the permissions of the file at `file_path` being written. diff --git a/validator_manager/src/validator/create.rs b/validator_manager/src/validator/create.rs index 324a5bc3a7c..02087c7be52 100644 --- a/validator_manager/src/validator/create.rs +++ b/validator_manager/src/validator/create.rs @@ -1,7 +1,13 @@ -use account_utils::{random_password, read_mnemonic_from_cli, read_password}; +use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_string}; use clap::{App, Arg, ArgMatches}; use environment::Environment; -use eth2::{lighthouse_vc::http_client::ValidatorClientHttpClient, SensitiveUrl}; +use eth2::{ + lighthouse_vc::{ + http_client::ValidatorClientHttpClient, + std_types::{ImportKeystoresRequest, KeystoreJsonStr}, + }, + SensitiveUrl, +}; use eth2_wallet::WalletBuilder; use serde::Serialize; use std::fs; @@ -210,7 +216,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("INTEGER") .help("The number of keystores to be submitted to the VC per request.") .takes_value(true) - .default_value("16"), + .default_value("4"), ) } @@ -240,6 +246,10 @@ pub async fn cli_run<'a, T: EthSpec>( let keystore_upload_batch_size: usize = clap_utils::parse_required(matches, KEYSTORE_UPLOAD_BATCH_SIZE)?; + let num_batches = (count + (count - 1)) + .checked_div(keystore_upload_batch_size as u32) + .ok_or_else(|| format!("--{} cannot be zero", 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) @@ -278,19 +288,19 @@ pub async fn cli_run<'a, T: EthSpec>( 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 wallet_password = random_password_string(); let voting_keystore_password = if let Some(path) = wallet_password_path { - read_password(&path) + read_password_string(&path) .map_err(|e| format!("Failed to read password from {:?}: {:?}", path, e))? } else { - random_password() + random_password_string() }; // A random password is always appropriate for the withdrawal keystore since we don't ever store // it anywhere. - let withdrawal_keystore_password = random_password(); + let withdrawal_keystore_password = random_password_string(); let mut wallet = - WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_bytes(), "".to_string()) + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? .build() .map_err(|e| format!("Unable to create wallet: {:?}", e))?; @@ -307,14 +317,14 @@ pub async fn cli_run<'a, T: EthSpec>( for i in 0..count { let keystores = wallet .next_validator( - wallet_password.as_bytes(), - voting_keystore_password.as_bytes(), - withdrawal_keystore_password.as_bytes(), + wallet_password.as_ref(), + voting_keystore_password.as_ref(), + withdrawal_keystore_password.as_ref(), ) .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; let voting_keystore = keystores.voting; let voting_keypair = voting_keystore - .decrypt_keypair(voting_keystore_password.as_bytes()) + .decrypt_keypair(voting_keystore_password.as_ref()) .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; let voting_pubkey_bytes = voting_keypair.pk.clone().into(); @@ -352,7 +362,7 @@ pub async fn cli_run<'a, T: EthSpec>( } else { let withdrawal_keypair = keystores .withdrawal - .decrypt_keypair(withdrawal_keystore_password.as_bytes()) + .decrypt_keypair(withdrawal_keystore_password.as_ref()) .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec) }; @@ -378,15 +388,72 @@ pub async fn cli_run<'a, T: EthSpec>( voting_keystores.push(voting_keystore); } - eprintln!( - "Generated {} keystores. Starting to submit keystores to VC, \ - each keystore may take several seconds", - count - ); + if let Some(http_client) = http_client { + eprintln!( + "Generated {} keystores. Starting to submit keystores to VC, \ + each keystore may take several seconds", + count + ); + + for (i, chunk) in voting_keystores + .chunks(keystore_upload_batch_size) + .enumerate() + { + let keystores = chunk + .iter() + .cloned() + .map(KeystoreJsonStr) + .collect::>(); + let passwords = keystores + .iter() + .map(|_| voting_keystore_password.clone()) + .collect(); + let request = ImportKeystoresRequest { + keystores, + passwords, + // New validators have no slashing protection history. + slashing_protection: None, + }; + + if let Err(e) = http_client.post_keystores(&request).await { + eprintln!( + "Failed to upload batch {}. Some keys were imported whilst \ + others may not have been imported. A potential solution is to use the \ + --{} flag, however care should be taken to ensure that there are no \ + duplicate deposits submitted.", + i, IGNORE_DUPLICATES_FLAG + ); + // Return here *without* writing the deposit JSON file. This might help prevent + // users from submitting duplicate deposits or deposits for validators that weren't + // initialized on a VC. + // + // Next the the user runs with the --ignore-duplicates flag there should be a new, + // complete deposit JSON file created. + return Err(format!("Key upload failed: {:?}", e)); + } + + eprintln!( + "Uploaded keystore batch {} of {} to the VC", + i + 1, + num_batches + ); + } + } + + // If configured, create a single JSON file which contains deposit data information for all + // validators. + if let Some(json_deposit_data_path) = json_deposit_data_path { + let json_deposits = json_deposits.ok_or("Internal error: JSON deposit data is None")?; + + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&json_deposit_data_path) + .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; - for voting_keystore_chunk in voting_keystores.chunks(keystore_upload_batch_size) { - todo!("submit to VC") + serde_json::to_writer(&mut file, &json_deposits) + .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; } - todo!(); + Ok(()) } From e25526ea2aae17e2bff5b52d1f3f3caf8dd73efb Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 16 Aug 2022 18:43:33 +1000 Subject: [PATCH 008/138] Add progress --- validator_manager/src/validator/common/mod.rs | 27 ++ validator_manager/src/validator/create.rs | 280 +++++++++++++----- validator_manager/src/validator/mod.rs | 1 + 3 files changed, 230 insertions(+), 78 deletions(-) create mode 100644 validator_manager/src/validator/common/mod.rs diff --git a/validator_manager/src/validator/common/mod.rs b/validator_manager/src/validator/common/mod.rs new file mode 100644 index 00000000000..58bab59ba7d --- /dev/null +++ b/validator_manager/src/validator/common/mod.rs @@ -0,0 +1,27 @@ +use account_utils::ZeroizeString; +use eth2::SensitiveUrl; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use types::*; + +#[derive(Serialize, Deserialize)] +pub struct CreateValidatorSpec { + pub derivation_index: u32, + pub voting_keystore_password: Option, + pub deposit_gwei: u64, + pub eth1_withdrawal_address: Option
, + pub fee_recipient: Option
, + pub gas_limit: Option, + pub builder_proposals: Option, + pub enabled: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct CreateSpec { + pub mnemonic: String, + pub validator_client_url: Option, + pub validator_client_token_path: Option, + pub json_deposit_data_path: Option, + pub ignore_duplicates: bool, + pub validators: Vec, +} diff --git a/validator_manager/src/validator/create.rs b/validator_manager/src/validator/create.rs index 02087c7be52..db8de37d57a 100644 --- a/validator_manager/src/validator/create.rs +++ b/validator_manager/src/validator/create.rs @@ -1,14 +1,22 @@ -use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_string}; +use super::common::*; +use account_utils::{ + random_password_string, read_mnemonic_from_cli, read_password_from_user, ZeroizeString, +}; use clap::{App, Arg, ArgMatches}; use environment::Environment; use eth2::{ lighthouse_vc::{ http_client::ValidatorClientHttpClient, std_types::{ImportKeystoresRequest, KeystoreJsonStr}, + types::UpdateFeeRecipientRequest, }, SensitiveUrl, }; -use eth2_wallet::WalletBuilder; +use eth2_keystore::Keystore; +use eth2_wallet::{ + bip39::{Language, Mnemonic}, + WalletBuilder, +}; use serde::Serialize; use std::fs; use std::path::PathBuf; @@ -22,13 +30,24 @@ 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 SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; 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"; +pub const GAS_LIMIT_FLAG: &str = "gas-limit"; +pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; +pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; + +struct ValidatorKeystore { + voting_keystore: Keystore, + voting_keystore_password: ZeroizeString, + voting_pubkey_bytes: PublicKeyBytes, + fee_recipient: Option
, + gas_limit: Option, + builder_proposals: Option, + enabled: Option, +} /// 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). @@ -151,16 +170,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true), ) .arg( - Arg::with_name(PASSWORD_FLAG) - .long(PASSWORD_FLAG) + Arg::with_name(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) + .long(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) .value_name("STRING") + .takes_value(true) .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), + "If present, the user will be prompted to enter the voting keystore \ + password that will be used to encrypt the voting keystores. If this \ + flag is not provided, a random password will be used. It is not \ + necessary to keep backups of voting keystore passwords if the \ + mnemonic is safely backed up.", + ), ) .arg( Arg::with_name(ETH1_WITHDRAWAL_ADDRESS_FLAG) @@ -173,20 +193,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .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) + .help( + "A HTTP(S) address of a validator client using the keymanager-API. \ + If this value is not supplied then a 'dry run' will be conducted where \ + no changes are made to the validator client.", + ) + .requires(VALIDATOR_CLIENT_TOKEN_FLAG) .takes_value(true), ) .arg( @@ -194,7 +210,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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( @@ -211,21 +226,52 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ), ) .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("4"), + Arg::with_name(GAS_LIMIT_FLAG) + .long(GAS_LIMIT_FLAG) + .value_name("UINT64") + .help( + "All created validators will use this gas limit. It is recommended \ + to leave this as the default value by not specifying this flag.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(FEE_RECIPIENT_FLAG) + .long(FEE_RECIPIENT_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "All created validators will use this value for the suggested \ + fee recipient. Omit this flag to use the default value from the VC.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(BUILDER_PROPOSALS_FLAG) + .long(BUILDER_PROPOSALS_FLAG) + .help( + "When provided, all created validators will attempt to create \ + blocks via builder rather than the local EL.", + ) + .required(false) + .required(false), ) } - pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, mut env: Environment, ) -> Result<(), String> { - let spec = env.core_context().eth2_config.spec; + let spec = &env.core_context().eth2_config.spec; + let create_spec = build_spec_from_cli(matches, spec)?; + enact_spec(create_spec, spec).await +} + +pub fn build_spec_from_cli<'a>( + matches: &'a ArgMatches<'a>, + spec: &ChainSpec, +) -> Result { 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)?; @@ -234,24 +280,69 @@ pub async fn cli_run<'a, T: EthSpec>( let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); let json_deposit_data_path: Option = clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; - let wallet_password_path: Option = clap_utils::parse_optional(matches, PASSWORD_FLAG)?; + let specify_voting_keystore_password = + matches.is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG); let eth1_withdrawal_address: Option
= clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; - let dry_run = matches.is_present(DRY_RUN_FLAG); let vc_url: Option = clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?; let vc_token_path: Option = 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 builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG); + let fee_recipient: Option
= clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?; + let gas_limit: Option = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?; + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; + let voting_keystore_password = if specify_voting_keystore_password { + eprintln!("Please enter a voting keystore password when prompted."); + Some(read_password_from_user(stdin_inputs)?) + } else { + None + }; + + let mut validators = Vec::with_capacity(count as usize); + for derivation_index in first_index..first_index + count { + let validator = CreateValidatorSpec { + derivation_index, + voting_keystore_password: voting_keystore_password.clone(), + deposit_gwei, + eth1_withdrawal_address, + fee_recipient, + gas_limit, + builder_proposals: Some(builder_proposals), + enabled: Some(true), + }; + validators.push(validator); + } + + Ok(CreateSpec { + mnemonic: mnemonic.to_string(), + validator_client_url: vc_url, + validator_client_token_path: vc_token_path, + json_deposit_data_path, + ignore_duplicates, + validators, + }) +} - let num_batches = (count + (count - 1)) - .checked_div(keystore_upload_batch_size as u32) - .ok_or_else(|| format!("--{} cannot be zero", KEYSTORE_UPLOAD_BATCH_SIZE))?; +pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result<(), String> { + let CreateSpec { + mnemonic, + validator_client_url, + validator_client_token_path, + json_deposit_data_path, + ignore_duplicates, + validators, + } = create_spec; - let http_client = match (dry_run, vc_url, vc_token_path) { - (false, Some(vc_url), Some(vc_token_path)) => { + let count = validators.len(); + + let mnemonic = Mnemonic::from_phrase(&mnemonic, Language::English) + .map_err(|e| format!("Failed to parse mnemonic from create spec: {:?}", e))?; + + let http_client = match (validator_client_url, validator_client_token_path) { + (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) @@ -277,24 +368,17 @@ pub async fn cli_run<'a, T: EthSpec>( Some(http_client) } - (true, None, None) => None, + (None, None) => None, _ => { return Err(format!( - "Inconsistent use of {}, {} and {} flags", - DRY_RUN_FLAG, VALIDATOR_CLIENT_URL_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG + "Inconsistent use of {} and {}", + 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_string(); - let voting_keystore_password = if let Some(path) = wallet_password_path { - read_password_string(&path) - .map_err(|e| format!("Failed to read password from {:?}: {:?}", path, e))? - } else { - random_password_string() - }; // A random password is always appropriate for the withdrawal keystore since we don't ever store // it anywhere. let withdrawal_keystore_password = random_password_string(); @@ -305,16 +389,30 @@ pub async fn cli_run<'a, T: EthSpec>( .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 validator_keystores = Vec::with_capacity(count); 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 { + for (i, validator) in validators.into_iter().enumerate() { + let CreateValidatorSpec { + derivation_index, + voting_keystore_password, + deposit_gwei, + eth1_withdrawal_address, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = validator; + + let voting_keystore_password = + voting_keystore_password.unwrap_or_else(|| random_password_string()); + + wallet + .set_nextaccount(derivation_index) + .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; + let keystores = wallet .next_validator( wallet_password.as_ref(), @@ -385,7 +483,15 @@ pub async fn cli_run<'a, T: EthSpec>( &voting_keypair.pk ); - voting_keystores.push(voting_keystore); + validator_keystores.push(ValidatorKeystore { + voting_keystore, + voting_keystore_password, + voting_pubkey_bytes, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + }); } if let Some(http_client) = http_client { @@ -395,22 +501,20 @@ pub async fn cli_run<'a, T: EthSpec>( count ); - for (i, chunk) in voting_keystores - .chunks(keystore_upload_batch_size) - .enumerate() - { - let keystores = chunk - .iter() - .cloned() - .map(KeystoreJsonStr) - .collect::>(); - let passwords = keystores - .iter() - .map(|_| voting_keystore_password.clone()) - .collect(); + for (i, validator_keystore) in validator_keystores.into_iter().enumerate() { + let ValidatorKeystore { + voting_keystore, + voting_keystore_password, + voting_pubkey_bytes, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = validator_keystore; + let request = ImportKeystoresRequest { - keystores, - passwords, + keystores: vec![KeystoreJsonStr(voting_keystore)], + passwords: vec![voting_keystore_password], // New validators have no slashing protection history. slashing_protection: None, }; @@ -432,11 +536,31 @@ pub async fn cli_run<'a, T: EthSpec>( return Err(format!("Key upload failed: {:?}", e)); } - eprintln!( - "Uploaded keystore batch {} of {} to the VC", - i + 1, - num_batches - ); + if let Some(fee_recipient) = fee_recipient { + http_client + .post_fee_recipient( + &voting_pubkey_bytes, + &UpdateFeeRecipientRequest { + ethaddress: fee_recipient, + }, + ) + .await + .map_err(|e| format!("Failed to update fee recipient on VC: {:?}", e))?; + } + + if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { + http_client + .patch_lighthouse_validators( + &voting_pubkey_bytes, + enabled, + gas_limit, + builder_proposals, + ) + .await + .map_err(|e| format!("Failed to update lighthouse validator on VC: {:?}", e))?; + } + + eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count); } } diff --git a/validator_manager/src/validator/mod.rs b/validator_manager/src/validator/mod.rs index c778f394a4f..07c52b9b720 100644 --- a/validator_manager/src/validator/mod.rs +++ b/validator_manager/src/validator/mod.rs @@ -1,3 +1,4 @@ +pub mod common; pub mod create; use clap::{App, ArgMatches}; From e463518df695eab204ab78fa1397df9c56aa3fa5 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 11:12:08 +1000 Subject: [PATCH 009/138] Start refactoring into separate commands --- common/eth2/src/lighthouse_vc/std_types.rs | 3 +- testing/ef_tests/src/cases/fork_choice.rs | 2 +- validator_manager/src/validator/common/mod.rs | 77 +++- validator_manager/src/validator/create.rs | 195 ++++------ .../src/validator/create_validators.rs | 344 ++++++++++++++++++ .../src/validator/import_validators.rs | 322 ++++++++++++++++ validator_manager/src/validator/mod.rs | 2 + 7 files changed, 819 insertions(+), 126 deletions(-) create mode 100644 validator_manager/src/validator/create_validators.rs create mode 100644 validator_manager/src/validator/import_validators.rs diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index 887bcb99ea6..101d71c3b3d 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -1,9 +1,10 @@ use account_utils::ZeroizeString; use eth2_keystore::Keystore; use serde::{Deserialize, Serialize}; -use slashing_protection::interchange::Interchange; use types::{Address, PublicKeyBytes}; +pub use slashing_protection::interchange::Interchange; + #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct GetFeeRecipientResponse { pub pubkey: PublicKeyBytes, diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 650452d7831..2d22d9cb531 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -153,7 +153,7 @@ impl Case for ForkChoiceTest { self.description.clone() } - fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { + fn result(&self, case_index: usize, fork_name: ForkName) -> Result<(), Error> { let tester = Tester::new(self, testing_spec::(fork_name))?; for step in &self.steps { diff --git a/validator_manager/src/validator/common/mod.rs b/validator_manager/src/validator/common/mod.rs index 58bab59ba7d..d0431248e17 100644 --- a/validator_manager/src/validator/common/mod.rs +++ b/validator_manager/src/validator/common/mod.rs @@ -1,15 +1,15 @@ use account_utils::ZeroizeString; +use eth2::lighthouse_vc::std_types::KeystoreJsonStr; use eth2::SensitiveUrl; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use tree_hash::TreeHash; use types::*; #[derive(Serialize, Deserialize)] -pub struct CreateValidatorSpec { - pub derivation_index: u32, - pub voting_keystore_password: Option, - pub deposit_gwei: u64, - pub eth1_withdrawal_address: Option
, +pub struct ValidatorSpecification { + pub voting_keystore: KeystoreJsonStr, + pub voting_keystore_password: ZeroizeString, pub fee_recipient: Option
, pub gas_limit: Option, pub builder_proposals: Option, @@ -23,5 +23,70 @@ pub struct CreateSpec { pub validator_client_token_path: Option, pub json_deposit_data_path: Option, pub ignore_duplicates: bool, - pub validators: Vec, + pub validators: Vec, +} + +/// 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 { + pub fn new( + keypair: &Keypair, + withdrawal_credentials: Hash256, + amount: u64, + spec: &ChainSpec, + ) -> Result { + 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, + }) + } } diff --git a/validator_manager/src/validator/create.rs b/validator_manager/src/validator/create.rs index db8de37d57a..40725e9c4ed 100644 --- a/validator_manager/src/validator/create.rs +++ b/validator_manager/src/validator/create.rs @@ -20,7 +20,6 @@ use eth2_wallet::{ use serde::Serialize; use std::fs; use std::path::PathBuf; -use tree_hash::TreeHash; use types::*; pub const CMD: &str = "create"; @@ -49,69 +48,9 @@ struct ValidatorKeystore { enabled: Option, } -/// 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 { - 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, - }) - } +struct ValidatorsAndDeposits { + validators: Vec, + deposits: Option>, } pub fn cli_app<'a, 'b>() -> App<'a, 'b> { @@ -258,20 +197,21 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .required(false), ) } + pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, mut env: Environment, ) -> Result<(), String> { let spec = &env.core_context().eth2_config.spec; - let create_spec = build_spec_from_cli(matches, spec)?; + let create_spec = build_validator_spec_from_cli(matches, spec)?; enact_spec(create_spec, spec).await } -pub fn build_spec_from_cli<'a>( +pub fn build_validator_spec_from_cli<'a>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, -) -> Result { +) -> Result { 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)?; @@ -301,13 +241,74 @@ pub fn build_spec_from_cli<'a>( None }; + /* + * Generate a wallet to be used for HD key generation. + */ + + // A random password is always appropriate for the wallet since it is ephemeral. + let wallet_password = random_password_string(); + // A random password is always appropriate for the withdrawal keystore since we don't ever store + // it anywhere. + let withdrawal_keystore_password = random_password_string(); + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) + .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? + .build() + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + /* + * Start deriving individual validators. + */ + let mut validators = Vec::with_capacity(count as usize); - for derivation_index in first_index..first_index + count { - let validator = CreateValidatorSpec { - derivation_index, + let mut deposits = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); + + for (i, derivation_index) in (first_index..first_index + count).enumerate() { + let voting_keystore_password = + voting_keystore_password.unwrap_or_else(|| random_password_string()); + + wallet + .set_nextaccount(derivation_index) + .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; + + let keystores = wallet + .next_validator( + wallet_password.as_ref(), + voting_keystore_password.as_ref(), + withdrawal_keystore_password.as_ref(), + ) + .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; + let voting_keystore = keystores.voting; + let voting_keypair = voting_keystore + .decrypt_keypair(voting_keystore_password.as_ref()) + .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; + + if let Some(deposits) = &mut deposits { + 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_ref()) + .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; + WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec) + }; + + let json_deposit = StandardDepositDataJson::new( + &voting_keypair, + withdrawal_credentials.into(), + deposit_gwei, + &spec, + )?; + + deposits.push(json_deposit); + } + + let validator = ValidatorSpecification { + voting_keystore: KeystoreJsonStr(voting_keystore), voting_keystore_password: voting_keystore_password.clone(), - deposit_gwei, - eth1_withdrawal_address, fee_recipient, gas_limit, builder_proposals: Some(builder_proposals), @@ -316,13 +317,9 @@ pub fn build_spec_from_cli<'a>( validators.push(validator); } - Ok(CreateSpec { - mnemonic: mnemonic.to_string(), - validator_client_url: vc_url, - validator_client_token_path: vc_token_path, - json_deposit_data_path, - ignore_duplicates, + Ok(ValidatorsAndDeposits { validators, + deposits, }) } @@ -390,37 +387,21 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result .map_err(|e| format!("Unable to create wallet: {:?}", e))?; let mut validator_keystores = Vec::with_capacity(count); - 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, validator) in validators.into_iter().enumerate() { let CreateValidatorSpec { - derivation_index, + voting_keystore, voting_keystore_password, - deposit_gwei, - eth1_withdrawal_address, fee_recipient, gas_limit, builder_proposals, enabled, } = validator; - let voting_keystore_password = - voting_keystore_password.unwrap_or_else(|| random_password_string()); - - wallet - .set_nextaccount(derivation_index) - .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; + let voting_keystore = voting_keystore.0; - let keystores = wallet - .next_validator( - wallet_password.as_ref(), - voting_keystore_password.as_ref(), - withdrawal_keystore_password.as_ref(), - ) - .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; - let voting_keystore = keystores.voting; let voting_keypair = voting_keystore .decrypt_keypair(voting_keystore_password.as_ref()) .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; @@ -454,28 +435,6 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result } } - 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_ref()) - .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), diff --git a/validator_manager/src/validator/create_validators.rs b/validator_manager/src/validator/create_validators.rs new file mode 100644 index 00000000000..3c66c09481d --- /dev/null +++ b/validator_manager/src/validator/create_validators.rs @@ -0,0 +1,344 @@ +use super::common::*; +use account_utils::{ + random_password_string, read_mnemonic_from_cli, read_password_from_user, ZeroizeString, +}; +use clap::{App, Arg, ArgMatches}; +use environment::Environment; +use eth2::lighthouse_vc::std_types::KeystoreJsonStr; +use eth2_keystore::Keystore; +use eth2_wallet::WalletBuilder; +use serde::Serialize; +use std::fs; +use std::path::{Path, PathBuf}; +use types::*; + +pub const CMD: &str = "create"; +pub const OUTPUT_PATH_FLAG: &str = "output-path"; +pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; +pub const DISABLE_DEPOSITS_FLAG: &str = "disable-deposits"; +pub const COUNT_FLAG: &str = "count"; +pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; +pub const FIRST_INDEX_FLAG: &str = "first-index"; +pub const MNEMONIC_FLAG: &str = "mnemonic-path"; +pub const SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; +pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address"; +pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; +pub const GAS_LIMIT_FLAG: &str = "gas-limit"; +pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; +pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; + +pub const VALIDATORS_FILENAME: &str = "validators.json"; +pub const DEPOSITS_FILENAME: &str = "deposits.json"; + +struct ValidatorKeystore { + voting_keystore: Keystore, + voting_keystore_password: ZeroizeString, + voting_pubkey_bytes: PublicKeyBytes, + fee_recipient: Option
, + gas_limit: Option, + builder_proposals: Option, + enabled: Option, +} + +struct ValidatorsAndDeposits { + validators: Vec, + deposits: Option>, +} + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about("Creates new validators from BIP-39 mnemonic.") + .arg( + Arg::with_name(OUTPUT_PATH_FLAG) + .long(OUTPUT_PATH_FLAG) + .value_name("DIRECTORY") + .help( + "The path to a directory where the validator and (optionally) deposits \ + files will be created. The directory will be created if it does not exist.", + ) + .conflicts_with(DISABLE_DEPOSITS_FLAG) + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(DEPOSIT_GWEI_FLAG) + .long(DEPOSIT_GWEI_FLAG) + .value_name("DEPOSIT_GWEI") + .help( + "The GWEI value of the deposit amount. Defaults to the minimum amount \ + required for an active validator (MAX_EFFECTIVE_BALANCE)", + ) + .conflicts_with(DISABLE_DEPOSITS_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(FIRST_INDEX_FLAG) + .long(FIRST_INDEX_FLAG) + .value_name("FIRST_INDEX") + .help("The first of consecutive key indexes you wish to recover.") + .takes_value(true) + .required(false) + .default_value("0"), + ) + .arg( + Arg::with_name(COUNT_FLAG) + .long(COUNT_FLAG) + .value_name("VALIDATOR_COUNT") + .help("The number of validators to create, regardless of how many already exist") + .conflicts_with("at-most") + .takes_value(true), + ) + .arg( + Arg::with_name(MNEMONIC_FLAG) + .long(MNEMONIC_FLAG) + .value_name("MNEMONIC_PATH") + .help("If present, the mnemonic will be read in from this file.") + .takes_value(true), + ) + .arg( + Arg::with_name(STDIN_INPUTS_FLAG) + .takes_value(false) + .hidden(cfg!(windows)) + .long(STDIN_INPUTS_FLAG) + .help("If present, read all user inputs from stdin instead of tty."), + ) + .arg( + Arg::with_name(DISABLE_DEPOSITS_FLAG) + .long(DISABLE_DEPOSITS_FLAG) + .value_name("PATH") + .help( + "When provided don't generate the deposits JSON file that is \ + commonly used for submitting validator deposits via a web UI. \ + Using this flag will save several seconds per validator if the \ + user has an alternate strategy for submitting deposits.", + ), + ) + .arg( + Arg::with_name(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) + .long(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) + .value_name("STRING") + .takes_value(true) + .help( + "If present, the user will be prompted to enter the voting keystore \ + password that will be used to encrypt the voting keystores. If this \ + flag is not provided, a random password will be used. It is not \ + necessary to keep backups of voting keystore passwords if the \ + mnemonic is safely backed up.", + ), + ) + .arg( + Arg::with_name(ETH1_WITHDRAWAL_ADDRESS_FLAG) + .long(ETH1_WITHDRAWAL_ADDRESS_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "If this field is set, the given eth1 address will be used to create the \ + withdrawal credentials. Otherwise, it will generate withdrawal credentials \ + with the mnemonic-derived withdrawal public key in EIP-2334 format.", + ) + .conflicts_with(DISABLE_DEPOSITS_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(GAS_LIMIT_FLAG) + .long(GAS_LIMIT_FLAG) + .value_name("UINT64") + .help( + "All created validators will use this gas limit. It is recommended \ + to leave this as the default value by not specifying this flag.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(FEE_RECIPIENT_FLAG) + .long(FEE_RECIPIENT_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "All created validators will use this value for the suggested \ + fee recipient. Omit this flag to use the default value from the VC.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(BUILDER_PROPOSALS_FLAG) + .long(BUILDER_PROPOSALS_FLAG) + .help( + "When provided, all created validators will attempt to create \ + blocks via builder rather than the local EL.", + ) + .required(false), + ) +} + +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + mut env: Environment, +) -> Result<(), String> { + let spec = &env.core_context().eth2_config.spec; + let output_path: PathBuf = clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?; + + if !output_path.exists() { + fs::create_dir(&output_path) + .map_err(|e| format!("Failed to create {:?} directory: {:?}", output_path, e))?; + } else if !output_path.is_dir() { + return Err(format!("{:?} must be a directory", output_path)); + } + + let validators_path = output_path.join(VALIDATORS_FILENAME); + if validators_path.exists() { + return Err(format!( + "{:?} already exists, refusing to overwrite", + validators_path + )); + } + let deposits_path = output_path.join(DEPOSITS_FILENAME); + if deposits_path.exists() { + return Err(format!( + "{:?} already exists, refusing to overwrite", + deposits_path + )); + } + + let validators_and_deposits = build_validator_spec_from_cli(matches, spec)?; + + write_to_json_file(&validators_path, &validators_and_deposits.validators)?; + + if let Some(deposits) = validators_and_deposits.deposits { + write_to_json_file(&deposits_path, &validators_and_deposits.validators)?; + } + + Ok(()) +} + +fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Result<(), String> { + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .map_err(|e| format!("Failed to open {:?}: {:?}", path.as_ref(), e))?; + serde_json::to_writer(&mut file, contents) + .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) +} + +pub fn build_validator_spec_from_cli<'a>( + matches: &'a ArgMatches<'a>, + spec: &ChainSpec, +) -> Result { + 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 = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; + let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); + let disable_deposits = matches.is_present(DISABLE_DEPOSITS_FLAG); + let specify_voting_keystore_password = + matches.is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG); + let eth1_withdrawal_address: Option
= + clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; + let ignore_duplicates = matches.is_present(IGNORE_DUPLICATES_FLAG); + let builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG); + let fee_recipient: Option
= clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?; + let gas_limit: Option = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?; + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; + let voting_keystore_password = if specify_voting_keystore_password { + eprintln!("Please enter a voting keystore password when prompted."); + Some(read_password_from_user(stdin_inputs)?) + } else { + None + }; + + /* + * Generate a wallet to be used for HD key generation. + */ + + // A random password is always appropriate for the wallet since it is ephemeral. + let wallet_password = random_password_string(); + // A random password is always appropriate for the withdrawal keystore since we don't ever store + // it anywhere. + let withdrawal_keystore_password = random_password_string(); + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) + .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? + .build() + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + /* + * Start deriving individual validators. + */ + + let mut validators = Vec::with_capacity(count as usize); + let mut deposits = disable_deposits.then(|| vec![]); + + for (i, derivation_index) in (first_index..first_index + count).enumerate() { + // If the voting keystore password was not provided by the user then use a unique random + // string for each validator. + let voting_keystore_password = + voting_keystore_password.unwrap_or_else(|| random_password_string()); + + // Set the wallet to the appropriate derivation index. + wallet + .set_nextaccount(derivation_index) + .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; + + // Derive the keystore from the HD wallet. + let keystores = wallet + .next_validator( + wallet_password.as_ref(), + voting_keystore_password.as_ref(), + withdrawal_keystore_password.as_ref(), + ) + .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; + let voting_keystore = keystores.voting; + + if let Some(deposits) = &mut deposits { + // Decrypt the voting keystore so a deposit message can be signed. + let voting_keypair = voting_keystore + .decrypt_keypair(voting_keystore_password.as_ref()) + .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; + + let withdrawal_credentials = if let Some(eth1_withdrawal_address) = + eth1_withdrawal_address + { + WithdrawalCredentials::eth1(eth1_withdrawal_address, &spec) + } else { + // Decrypt the withdrawal keystore so withdrawal credentials can be created. It's + // not strictly necessary to decrypt the keystore since we can read the pubkey + // directly from the keystore. However we decrypt the keystore to be more certain + // that we have access to the withdrawal keys. + let withdrawal_keypair = keystores + .withdrawal + .decrypt_keypair(withdrawal_keystore_password.as_ref()) + .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; + WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec) + }; + + // Create a JSON structure equivalent to the one generated by + // `ethereum/staking-deposit-cli`. + let json_deposit = StandardDepositDataJson::new( + &voting_keypair, + withdrawal_credentials.into(), + deposit_gwei, + &spec, + )?; + + deposits.push(json_deposit); + } + + let validator = ValidatorSpecification { + voting_keystore: KeystoreJsonStr(voting_keystore), + voting_keystore_password: voting_keystore_password.clone(), + fee_recipient, + gas_limit, + builder_proposals: Some(builder_proposals), + enabled: Some(true), + }; + validators.push(validator); + } + + Ok(ValidatorsAndDeposits { + validators, + deposits, + }) +} diff --git a/validator_manager/src/validator/import_validators.rs b/validator_manager/src/validator/import_validators.rs new file mode 100644 index 00000000000..4c82229174e --- /dev/null +++ b/validator_manager/src/validator/import_validators.rs @@ -0,0 +1,322 @@ +use super::common::*; +use account_utils::{ + random_password_string, read_mnemonic_from_cli, read_password_from_user, ZeroizeString, +}; +use clap::{App, Arg, ArgMatches}; +use environment::Environment; +use eth2::{ + lighthouse_vc::{ + http_client::ValidatorClientHttpClient, + std_types::{ImportKeystoresRequest, KeystoreJsonStr}, + types::UpdateFeeRecipientRequest, + }, + SensitiveUrl, +}; +use eth2_keystore::Keystore; +use eth2_wallet::{ + bip39::{Language, Mnemonic}, + WalletBuilder, +}; +use serde::Serialize; +use std::fs; +use std::path::PathBuf; +use types::*; + +pub const CMD: &str = "import"; +pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; +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"; + +struct ValidatorKeystore { + voting_keystore: Keystore, + voting_keystore_password: ZeroizeString, + voting_pubkey_bytes: PublicKeyBytes, + fee_recipient: Option
, + gas_limit: Option, + builder_proposals: Option, + enabled: Option, +} + +struct ValidatorsAndDeposits { + validators: Vec, + deposits: Option>, +} + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about("Uploads validators to a validator client.") + .arg( + Arg::with_name(VALIDATORS_FILE_FLAG) + .long(VALIDATORS_FILE_FLAG) + .value_name("PATH_TO_JSON_FILE") + .help( + "The path to a JSON file containing a list of validators to be \ + imported to the validator client. This file is usually named \ + \"validators.json\".", + ) + .required(true) + .takes_value(true), + ) + .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. \ + If this value is not supplied then a 'dry run' will be conducted where \ + no changes are made to the validator client.", + ) + .default_value("http://localhost:5062") + .requires(VALIDATOR_CLIENT_TOKEN_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.") + .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.", + ), + ) +} + +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + mut env: Environment, +) -> Result<(), String> { + let spec = &env.core_context().eth2_config.spec; + + let create_spec = build_validator_spec_from_cli(matches, spec)?; + enact_spec(create_spec, spec).await +} + +pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result<(), String> { + let CreateSpec { + mnemonic, + validator_client_url, + validator_client_token_path, + json_deposit_data_path, + ignore_duplicates, + validators, + } = create_spec; + + let count = validators.len(); + + let mnemonic = Mnemonic::from_phrase(&mnemonic, Language::English) + .map_err(|e| format!("Failed to parse mnemonic from create spec: {:?}", e))?; + + let http_client = match (validator_client_url, validator_client_token_path) { + (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) + } + (None, None) => None, + _ => { + return Err(format!( + "Inconsistent use of {} and {}", + VALIDATOR_CLIENT_URL_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG + )) + } + }; + + // A random password is always appropriate for the wallet since it is ephemeral. + let wallet_password = random_password_string(); + // A random password is always appropriate for the withdrawal keystore since we don't ever store + // it anywhere. + let withdrawal_keystore_password = random_password_string(); + + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) + .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? + .build() + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + let mut validator_keystores = Vec::with_capacity(count); + + eprintln!("Starting key generation. Each validator may take several seconds."); + + for (i, validator) in validators.into_iter().enumerate() { + let CreateValidatorSpec { + voting_keystore, + voting_keystore_password, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = validator; + + let voting_keystore = voting_keystore.0; + + let voting_keypair = voting_keystore + .decrypt_keypair(voting_keystore_password.as_ref()) + .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 + )); + } + } + } + + eprintln!( + "{}/{}: {:?}", + i.saturating_add(1), + count, + &voting_keypair.pk + ); + + validator_keystores.push(ValidatorKeystore { + voting_keystore, + voting_keystore_password, + voting_pubkey_bytes, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + }); + } + + if let Some(http_client) = http_client { + eprintln!( + "Generated {} keystores. Starting to submit keystores to VC, \ + each keystore may take several seconds", + count + ); + + for (i, validator_keystore) in validator_keystores.into_iter().enumerate() { + let ValidatorKeystore { + voting_keystore, + voting_keystore_password, + voting_pubkey_bytes, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = validator_keystore; + + let request = ImportKeystoresRequest { + keystores: vec![KeystoreJsonStr(voting_keystore)], + passwords: vec![voting_keystore_password], + // New validators have no slashing protection history. + slashing_protection: None, + }; + + if let Err(e) = http_client.post_keystores(&request).await { + eprintln!( + "Failed to upload batch {}. Some keys were imported whilst \ + others may not have been imported. A potential solution is to use the \ + --{} flag, however care should be taken to ensure that there are no \ + duplicate deposits submitted.", + i, IGNORE_DUPLICATES_FLAG + ); + // Return here *without* writing the deposit JSON file. This might help prevent + // users from submitting duplicate deposits or deposits for validators that weren't + // initialized on a VC. + // + // Next the the user runs with the --ignore-duplicates flag there should be a new, + // complete deposit JSON file created. + return Err(format!("Key upload failed: {:?}", e)); + } + + if let Some(fee_recipient) = fee_recipient { + http_client + .post_fee_recipient( + &voting_pubkey_bytes, + &UpdateFeeRecipientRequest { + ethaddress: fee_recipient, + }, + ) + .await + .map_err(|e| format!("Failed to update fee recipient on VC: {:?}", e))?; + } + + if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { + http_client + .patch_lighthouse_validators( + &voting_pubkey_bytes, + enabled, + gas_limit, + builder_proposals, + ) + .await + .map_err(|e| format!("Failed to update lighthouse validator on VC: {:?}", e))?; + } + + eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count); + } + } + + // If configured, create a single JSON file which contains deposit data information for all + // validators. + if let Some(json_deposit_data_path) = json_deposit_data_path { + let json_deposits = json_deposits.ok_or("Internal error: JSON deposit data is None")?; + + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&json_deposit_data_path) + .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; + + serde_json::to_writer(&mut file, &json_deposits) + .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; + } + + Ok(()) +} diff --git a/validator_manager/src/validator/mod.rs b/validator_manager/src/validator/mod.rs index 07c52b9b720..e3470f03f4b 100644 --- a/validator_manager/src/validator/mod.rs +++ b/validator_manager/src/validator/mod.rs @@ -1,5 +1,7 @@ pub mod common; pub mod create; +pub mod create_validators; +pub mod import_validators; use clap::{App, ArgMatches}; use environment::Environment; From fec2969cd22017766809ccbee882d32d3ad48a32 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 11:53:41 +1000 Subject: [PATCH 010/138] Remove old command --- validator_manager/src/lib.rs | 6 +- validator_manager/src/validator/create.rs | 542 ------------------ .../{validator => validators}/common/mod.rs | 3 +- .../create_validators.rs | 41 +- .../import_validators.rs | 204 ++----- .../src/{validator => validators}/mod.rs | 11 +- 6 files changed, 79 insertions(+), 728 deletions(-) delete mode 100644 validator_manager/src/validator/create.rs rename validator_manager/src/{validator => validators}/common/mod.rs (95%) rename validator_manager/src/{validator => validators}/create_validators.rs (92%) rename validator_manager/src/{validator => validators}/import_validators.rs (53%) rename validator_manager/src/{validator => validators}/mod.rs (62%) diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index c5d41937b82..5507b5aaa03 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -3,7 +3,7 @@ use clap::ArgMatches; use environment::Environment; use types::EthSpec; -mod validator; +mod validators; pub const CMD: &str = "validator_manager"; @@ -11,7 +11,7 @@ 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()) + .subcommand(validators::cli_app()) } /// Run the account manager, returning an error if the operation did not succeed. @@ -20,7 +20,7 @@ pub async fn run<'a, T: EthSpec>( env: Environment, ) -> Result<(), String> { match matches.subcommand() { - (validator::CMD, Some(matches)) => validator::cli_run(matches, env).await?, + (validators::CMD, Some(matches)) => validators::cli_run(matches, env).await?, (unknown, _) => { return Err(format!( "{} is not a valid {} command. See --help.", diff --git a/validator_manager/src/validator/create.rs b/validator_manager/src/validator/create.rs deleted file mode 100644 index 40725e9c4ed..00000000000 --- a/validator_manager/src/validator/create.rs +++ /dev/null @@ -1,542 +0,0 @@ -use super::common::*; -use account_utils::{ - random_password_string, read_mnemonic_from_cli, read_password_from_user, ZeroizeString, -}; -use clap::{App, Arg, ArgMatches}; -use environment::Environment; -use eth2::{ - lighthouse_vc::{ - http_client::ValidatorClientHttpClient, - std_types::{ImportKeystoresRequest, KeystoreJsonStr}, - types::UpdateFeeRecipientRequest, - }, - SensitiveUrl, -}; -use eth2_keystore::Keystore; -use eth2_wallet::{ - bip39::{Language, Mnemonic}, - WalletBuilder, -}; -use serde::Serialize; -use std::fs; -use std::path::PathBuf; -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 SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; -pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address"; -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 GAS_LIMIT_FLAG: &str = "gas-limit"; -pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; -pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; - -struct ValidatorKeystore { - voting_keystore: Keystore, - voting_keystore_password: ZeroizeString, - voting_pubkey_bytes: PublicKeyBytes, - fee_recipient: Option
, - gas_limit: Option, - builder_proposals: Option, - enabled: Option, -} - -struct ValidatorsAndDeposits { - validators: Vec, - deposits: Option>, -} - -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(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) - .long(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) - .value_name("STRING") - .takes_value(true) - .help( - "If present, the user will be prompted to enter the voting keystore \ - password that will be used to encrypt the voting keystores. If this \ - flag is not provided, a random password will be used. It is not \ - necessary to keep backups of voting keystore passwords if the \ - mnemonic is safely backed up.", - ), - ) - .arg( - Arg::with_name(ETH1_WITHDRAWAL_ADDRESS_FLAG) - .long(ETH1_WITHDRAWAL_ADDRESS_FLAG) - .value_name("ETH1_ADDRESS") - .help( - "If this field is set, the given eth1 address will be used to create the \ - withdrawal credentials. Otherwise, it will generate withdrawal credentials \ - with the mnemonic-derived withdrawal public key in EIP-2334 format.", - ) - .takes_value(true), - ) - .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. \ - If this value is not supplied then a 'dry run' will be conducted where \ - no changes are made to the validator client.", - ) - .requires(VALIDATOR_CLIENT_TOKEN_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.") - .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(GAS_LIMIT_FLAG) - .long(GAS_LIMIT_FLAG) - .value_name("UINT64") - .help( - "All created validators will use this gas limit. It is recommended \ - to leave this as the default value by not specifying this flag.", - ) - .required(false) - .takes_value(true), - ) - .arg( - Arg::with_name(FEE_RECIPIENT_FLAG) - .long(FEE_RECIPIENT_FLAG) - .value_name("ETH1_ADDRESS") - .help( - "All created validators will use this value for the suggested \ - fee recipient. Omit this flag to use the default value from the VC.", - ) - .required(false) - .takes_value(true), - ) - .arg( - Arg::with_name(BUILDER_PROPOSALS_FLAG) - .long(BUILDER_PROPOSALS_FLAG) - .help( - "When provided, all created validators will attempt to create \ - blocks via builder rather than the local EL.", - ) - .required(false) - .required(false), - ) -} - -pub async fn cli_run<'a, T: EthSpec>( - matches: &'a ArgMatches<'a>, - mut env: Environment, -) -> Result<(), String> { - let spec = &env.core_context().eth2_config.spec; - - let create_spec = build_validator_spec_from_cli(matches, spec)?; - enact_spec(create_spec, spec).await -} - -pub fn build_validator_spec_from_cli<'a>( - matches: &'a ArgMatches<'a>, - spec: &ChainSpec, -) -> Result { - 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 = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; - let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); - let json_deposit_data_path: Option = - clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; - let specify_voting_keystore_password = - matches.is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG); - let eth1_withdrawal_address: Option
= - clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; - let vc_url: Option = - clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?; - let vc_token_path: Option = - clap_utils::parse_optional(matches, VALIDATOR_CLIENT_TOKEN_FLAG)?; - let ignore_duplicates = matches.is_present(IGNORE_DUPLICATES_FLAG); - let builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG); - let fee_recipient: Option
= clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?; - let gas_limit: Option = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?; - - let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; - let voting_keystore_password = if specify_voting_keystore_password { - eprintln!("Please enter a voting keystore password when prompted."); - Some(read_password_from_user(stdin_inputs)?) - } else { - None - }; - - /* - * Generate a wallet to be used for HD key generation. - */ - - // A random password is always appropriate for the wallet since it is ephemeral. - let wallet_password = random_password_string(); - // A random password is always appropriate for the withdrawal keystore since we don't ever store - // it anywhere. - let withdrawal_keystore_password = random_password_string(); - let mut wallet = - WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) - .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? - .build() - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; - - /* - * Start deriving individual validators. - */ - - let mut validators = Vec::with_capacity(count as usize); - let mut deposits = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); - - for (i, derivation_index) in (first_index..first_index + count).enumerate() { - let voting_keystore_password = - voting_keystore_password.unwrap_or_else(|| random_password_string()); - - wallet - .set_nextaccount(derivation_index) - .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; - - let keystores = wallet - .next_validator( - wallet_password.as_ref(), - voting_keystore_password.as_ref(), - withdrawal_keystore_password.as_ref(), - ) - .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; - let voting_keystore = keystores.voting; - let voting_keypair = voting_keystore - .decrypt_keypair(voting_keystore_password.as_ref()) - .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; - - if let Some(deposits) = &mut deposits { - 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_ref()) - .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; - WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec) - }; - - let json_deposit = StandardDepositDataJson::new( - &voting_keypair, - withdrawal_credentials.into(), - deposit_gwei, - &spec, - )?; - - deposits.push(json_deposit); - } - - let validator = ValidatorSpecification { - voting_keystore: KeystoreJsonStr(voting_keystore), - voting_keystore_password: voting_keystore_password.clone(), - fee_recipient, - gas_limit, - builder_proposals: Some(builder_proposals), - enabled: Some(true), - }; - validators.push(validator); - } - - Ok(ValidatorsAndDeposits { - validators, - deposits, - }) -} - -pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result<(), String> { - let CreateSpec { - mnemonic, - validator_client_url, - validator_client_token_path, - json_deposit_data_path, - ignore_duplicates, - validators, - } = create_spec; - - let count = validators.len(); - - let mnemonic = Mnemonic::from_phrase(&mnemonic, Language::English) - .map_err(|e| format!("Failed to parse mnemonic from create spec: {:?}", e))?; - - let http_client = match (validator_client_url, validator_client_token_path) { - (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) - } - (None, None) => None, - _ => { - return Err(format!( - "Inconsistent use of {} and {}", - VALIDATOR_CLIENT_URL_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG - )) - } - }; - - // A random password is always appropriate for the wallet since it is ephemeral. - let wallet_password = random_password_string(); - // A random password is always appropriate for the withdrawal keystore since we don't ever store - // it anywhere. - let withdrawal_keystore_password = random_password_string(); - - let mut wallet = - WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) - .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? - .build() - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; - - let mut validator_keystores = Vec::with_capacity(count); - - eprintln!("Starting key generation. Each validator may take several seconds."); - - for (i, validator) in validators.into_iter().enumerate() { - let CreateValidatorSpec { - voting_keystore, - voting_keystore_password, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - } = validator; - - let voting_keystore = voting_keystore.0; - - let voting_keypair = voting_keystore - .decrypt_keypair(voting_keystore_password.as_ref()) - .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 - )); - } - } - } - - eprintln!( - "{}/{}: {:?}", - i.saturating_add(1), - count, - &voting_keypair.pk - ); - - validator_keystores.push(ValidatorKeystore { - voting_keystore, - voting_keystore_password, - voting_pubkey_bytes, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - }); - } - - if let Some(http_client) = http_client { - eprintln!( - "Generated {} keystores. Starting to submit keystores to VC, \ - each keystore may take several seconds", - count - ); - - for (i, validator_keystore) in validator_keystores.into_iter().enumerate() { - let ValidatorKeystore { - voting_keystore, - voting_keystore_password, - voting_pubkey_bytes, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - } = validator_keystore; - - let request = ImportKeystoresRequest { - keystores: vec![KeystoreJsonStr(voting_keystore)], - passwords: vec![voting_keystore_password], - // New validators have no slashing protection history. - slashing_protection: None, - }; - - if let Err(e) = http_client.post_keystores(&request).await { - eprintln!( - "Failed to upload batch {}. Some keys were imported whilst \ - others may not have been imported. A potential solution is to use the \ - --{} flag, however care should be taken to ensure that there are no \ - duplicate deposits submitted.", - i, IGNORE_DUPLICATES_FLAG - ); - // Return here *without* writing the deposit JSON file. This might help prevent - // users from submitting duplicate deposits or deposits for validators that weren't - // initialized on a VC. - // - // Next the the user runs with the --ignore-duplicates flag there should be a new, - // complete deposit JSON file created. - return Err(format!("Key upload failed: {:?}", e)); - } - - if let Some(fee_recipient) = fee_recipient { - http_client - .post_fee_recipient( - &voting_pubkey_bytes, - &UpdateFeeRecipientRequest { - ethaddress: fee_recipient, - }, - ) - .await - .map_err(|e| format!("Failed to update fee recipient on VC: {:?}", e))?; - } - - if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { - http_client - .patch_lighthouse_validators( - &voting_pubkey_bytes, - enabled, - gas_limit, - builder_proposals, - ) - .await - .map_err(|e| format!("Failed to update lighthouse validator on VC: {:?}", e))?; - } - - eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count); - } - } - - // If configured, create a single JSON file which contains deposit data information for all - // validators. - if let Some(json_deposit_data_path) = json_deposit_data_path { - let json_deposits = json_deposits.ok_or("Internal error: JSON deposit data is None")?; - - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&json_deposit_data_path) - .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; - - serde_json::to_writer(&mut file, &json_deposits) - .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; - } - - Ok(()) -} diff --git a/validator_manager/src/validator/common/mod.rs b/validator_manager/src/validators/common/mod.rs similarity index 95% rename from validator_manager/src/validator/common/mod.rs rename to validator_manager/src/validators/common/mod.rs index d0431248e17..67bee65b32a 100644 --- a/validator_manager/src/validator/common/mod.rs +++ b/validator_manager/src/validators/common/mod.rs @@ -1,5 +1,5 @@ use account_utils::ZeroizeString; -use eth2::lighthouse_vc::std_types::KeystoreJsonStr; +use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::SensitiveUrl; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -10,6 +10,7 @@ use types::*; pub struct ValidatorSpecification { pub voting_keystore: KeystoreJsonStr, pub voting_keystore_password: ZeroizeString, + pub slashing_protection: Option, pub fee_recipient: Option
, pub gas_limit: Option, pub builder_proposals: Option, diff --git a/validator_manager/src/validator/create_validators.rs b/validator_manager/src/validators/create_validators.rs similarity index 92% rename from validator_manager/src/validator/create_validators.rs rename to validator_manager/src/validators/create_validators.rs index 3c66c09481d..161392c8540 100644 --- a/validator_manager/src/validator/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -1,11 +1,8 @@ use super::common::*; -use account_utils::{ - random_password_string, read_mnemonic_from_cli, read_password_from_user, ZeroizeString, -}; +use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; use clap::{App, Arg, ArgMatches}; use environment::Environment; use eth2::lighthouse_vc::std_types::KeystoreJsonStr; -use eth2_keystore::Keystore; use eth2_wallet::WalletBuilder; use serde::Serialize; use std::fs; @@ -22,7 +19,6 @@ pub const FIRST_INDEX_FLAG: &str = "first-index"; pub const MNEMONIC_FLAG: &str = "mnemonic-path"; pub const SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address"; -pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; @@ -30,16 +26,6 @@ pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; pub const VALIDATORS_FILENAME: &str = "validators.json"; pub const DEPOSITS_FILENAME: &str = "deposits.json"; -struct ValidatorKeystore { - voting_keystore: Keystore, - voting_keystore_password: ZeroizeString, - voting_pubkey_bytes: PublicKeyBytes, - fee_recipient: Option
, - gas_limit: Option, - builder_proposals: Option, - enabled: Option, -} - struct ValidatorsAndDeposits { validators: Vec, deposits: Option>, @@ -47,7 +33,13 @@ struct ValidatorsAndDeposits { pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) - .about("Creates new validators from BIP-39 mnemonic.") + .about( + "Creates new validators from BIP-39 mnemonic. A JSON file will be created which \ + contains all the validator keystores and other validator data. This file can then \ + be imported to a validator client using the \"import-validators\" command. \ + Another, optional JSON file is created which contains a list of validator \ + deposits in the same format as the \"ethereum/staking-deposit-cli\" tool.", + ) .arg( Arg::with_name(OUTPUT_PATH_FLAG) .long(OUTPUT_PATH_FLAG) @@ -176,6 +168,7 @@ pub async fn cli_run<'a, T: EthSpec>( mut env: Environment, ) -> Result<(), String> { let spec = &env.core_context().eth2_config.spec; + let output_path: PathBuf = clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?; if !output_path.exists() { @@ -204,8 +197,8 @@ pub async fn cli_run<'a, T: EthSpec>( write_to_json_file(&validators_path, &validators_and_deposits.validators)?; - if let Some(deposits) = validators_and_deposits.deposits { - write_to_json_file(&deposits_path, &validators_and_deposits.validators)?; + if let Some(deposits) = &validators_and_deposits.deposits { + write_to_json_file(&deposits_path, deposits)?; } Ok(()) @@ -215,13 +208,13 @@ fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Re let mut file = fs::OpenOptions::new() .write(true) .create_new(true) - .open(path) + .open(&path) .map_err(|e| format!("Failed to open {:?}: {:?}", path.as_ref(), e))?; serde_json::to_writer(&mut file, contents) .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) } -pub fn build_validator_spec_from_cli<'a>( +fn build_validator_spec_from_cli<'a>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, ) -> Result { @@ -236,7 +229,6 @@ pub fn build_validator_spec_from_cli<'a>( matches.is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG); let eth1_withdrawal_address: Option
= clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; - let ignore_duplicates = matches.is_present(IGNORE_DUPLICATES_FLAG); let builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG); let fee_recipient: Option
= clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?; let gas_limit: Option = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?; @@ -274,8 +266,9 @@ pub fn build_validator_spec_from_cli<'a>( for (i, derivation_index) in (first_index..first_index + count).enumerate() { // If the voting keystore password was not provided by the user then use a unique random // string for each validator. - let voting_keystore_password = - voting_keystore_password.unwrap_or_else(|| random_password_string()); + let voting_keystore_password = voting_keystore_password + .clone() + .unwrap_or_else(|| random_password_string()); // Set the wallet to the appropriate derivation index. wallet @@ -329,6 +322,8 @@ pub fn build_validator_spec_from_cli<'a>( let validator = ValidatorSpecification { voting_keystore: KeystoreJsonStr(voting_keystore), voting_keystore_password: voting_keystore_password.clone(), + // New validators have no slashing protection history. + slashing_protection: None, fee_recipient, gas_limit, builder_proposals: Some(builder_proposals), diff --git a/validator_manager/src/validator/import_validators.rs b/validator_manager/src/validators/import_validators.rs similarity index 53% rename from validator_manager/src/validator/import_validators.rs rename to validator_manager/src/validators/import_validators.rs index 4c82229174e..ffdb88bf879 100644 --- a/validator_manager/src/validator/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -1,26 +1,14 @@ use super::common::*; -use account_utils::{ - random_password_string, read_mnemonic_from_cli, read_password_from_user, ZeroizeString, -}; use clap::{App, Arg, ArgMatches}; -use environment::Environment; use eth2::{ lighthouse_vc::{ - http_client::ValidatorClientHttpClient, - std_types::{ImportKeystoresRequest, KeystoreJsonStr}, + http_client::ValidatorClientHttpClient, std_types::ImportKeystoresRequest, types::UpdateFeeRecipientRequest, }, SensitiveUrl, }; -use eth2_keystore::Keystore; -use eth2_wallet::{ - bip39::{Language, Mnemonic}, - WalletBuilder, -}; -use serde::Serialize; use std::fs; use std::path::PathBuf; -use types::*; pub const CMD: &str = "import"; pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; @@ -28,24 +16,13 @@ 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"; -struct ValidatorKeystore { - voting_keystore: Keystore, - voting_keystore_password: ZeroizeString, - voting_pubkey_bytes: PublicKeyBytes, - fee_recipient: Option
, - gas_limit: Option, - builder_proposals: Option, - enabled: Option, -} - -struct ValidatorsAndDeposits { - validators: Vec, - deposits: Option>, -} - pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) - .about("Uploads validators to a validator client.") + .about( + "Uploads validators to a validator client using the HTTP API. The validators \ + are defined in a JSON file which can be generated using the \"create-validators\" \ + command.", + ) .arg( Arg::with_name(VALIDATORS_FILE_FLAG) .long(VALIDATORS_FILE_FLAG) @@ -93,32 +70,39 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub async fn cli_run<'a, T: EthSpec>( - matches: &'a ArgMatches<'a>, - mut env: Environment, -) -> Result<(), String> { - let spec = &env.core_context().eth2_config.spec; +pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>) -> Result<(), String> { + let validators_file_path: PathBuf = clap_utils::parse_required(matches, VALIDATORS_FILE_FLAG)?; + if !validators_file_path.exists() { + return Err(format!("Unable to find file at {:?}", validators_file_path)); + } - let create_spec = build_validator_spec_from_cli(matches, spec)?; - enact_spec(create_spec, spec).await -} + let validators_file = fs::OpenOptions::new() + .read(true) + .create(false) + .open(&validators_file_path) + .map_err(|e| format!("Unable to open {:?}: {:?}", validators_file_path, e))?; + let validators = serde_json::from_reader(&validators_file).map_err(|e| { + format!( + "Unable to parse JSON in {:?}: {:?}", + validators_file_path, e + ) + })?; -pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result<(), String> { - let CreateSpec { - mnemonic, - validator_client_url, - validator_client_token_path, - json_deposit_data_path, - ignore_duplicates, - validators, - } = create_spec; + import_validators(matches, validators).await +} +pub async fn import_validators<'a>( + matches: &'a ArgMatches<'a>, + validators: Vec, +) -> Result<(), String> { let count = validators.len(); - let mnemonic = Mnemonic::from_phrase(&mnemonic, Language::English) - .map_err(|e| format!("Failed to parse mnemonic from create spec: {:?}", e))?; + let vc_url: Option = + clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?; + let vc_token_path: Option = + clap_utils::parse_optional(matches, VALIDATOR_CLIENT_TOKEN_FLAG)?; - let http_client = match (validator_client_url, validator_client_token_path) { + let http_client = match (vc_url, vc_token_path) { (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))?; @@ -145,7 +129,6 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result Some(http_client) } - (None, None) => None, _ => { return Err(format!( "Inconsistent use of {} and {}", @@ -154,108 +137,34 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result } }; - // A random password is always appropriate for the wallet since it is ephemeral. - let wallet_password = random_password_string(); - // A random password is always appropriate for the withdrawal keystore since we don't ever store - // it anywhere. - let withdrawal_keystore_password = random_password_string(); - - let mut wallet = - WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) - .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? - .build() - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; - - let mut validator_keystores = Vec::with_capacity(count); - - eprintln!("Starting key generation. Each validator may take several seconds."); - - for (i, validator) in validators.into_iter().enumerate() { - let CreateValidatorSpec { - voting_keystore, - voting_keystore_password, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - } = validator; - - let voting_keystore = voting_keystore.0; - - let voting_keypair = voting_keystore - .decrypt_keypair(voting_keystore_password.as_ref()) - .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 - )); - } - } - } - - eprintln!( - "{}/{}: {:?}", - i.saturating_add(1), - count, - &voting_keypair.pk - ); - - validator_keystores.push(ValidatorKeystore { - voting_keystore, - voting_keystore_password, - voting_pubkey_bytes, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - }); - } - if let Some(http_client) = http_client { eprintln!( - "Generated {} keystores. Starting to submit keystores to VC, \ - each keystore may take several seconds", + "Starting to submit validators {} to VC, each validator may take several seconds", count ); - for (i, validator_keystore) in validator_keystores.into_iter().enumerate() { - let ValidatorKeystore { + for (i, validator) in validators.into_iter().enumerate() { + let ValidatorSpecification { voting_keystore, voting_keystore_password, - voting_pubkey_bytes, + slashing_protection, fee_recipient, gas_limit, builder_proposals, enabled, - } = validator_keystore; + } = validator; + + let voting_public_key = voting_keystore + .public_key() + .ok_or_else(|| { + format!("Validator keystore at index {} is missing a public key", i) + })? + .into(); let request = ImportKeystoresRequest { - keystores: vec![KeystoreJsonStr(voting_keystore)], + keystores: vec![voting_keystore], passwords: vec![voting_keystore_password], - // New validators have no slashing protection history. - slashing_protection: None, + slashing_protection, }; if let Err(e) = http_client.post_keystores(&request).await { @@ -278,7 +187,7 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result if let Some(fee_recipient) = fee_recipient { http_client .post_fee_recipient( - &voting_pubkey_bytes, + &voting_public_key, &UpdateFeeRecipientRequest { ethaddress: fee_recipient, }, @@ -290,7 +199,7 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { http_client .patch_lighthouse_validators( - &voting_pubkey_bytes, + &voting_public_key, enabled, gas_limit, builder_proposals, @@ -303,20 +212,5 @@ pub async fn enact_spec<'a>(create_spec: CreateSpec, spec: &ChainSpec) -> Result } } - // If configured, create a single JSON file which contains deposit data information for all - // validators. - if let Some(json_deposit_data_path) = json_deposit_data_path { - let json_deposits = json_deposits.ok_or("Internal error: JSON deposit data is None")?; - - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&json_deposit_data_path) - .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; - - serde_json::to_writer(&mut file, &json_deposits) - .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; - } - Ok(()) } diff --git a/validator_manager/src/validator/mod.rs b/validator_manager/src/validators/mod.rs similarity index 62% rename from validator_manager/src/validator/mod.rs rename to validator_manager/src/validators/mod.rs index e3470f03f4b..9142ae1c992 100644 --- a/validator_manager/src/validator/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -1,5 +1,4 @@ pub mod common; -pub mod create; pub mod create_validators; pub mod import_validators; @@ -7,12 +6,13 @@ use clap::{App, ArgMatches}; use environment::Environment; use types::EthSpec; -pub const CMD: &str = "validator"; +pub const CMD: &str = "validators"; 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()) + .subcommand(create_validators::cli_app()) + .subcommand(import_validators::cli_app()) } pub async fn cli_run<'a, T: EthSpec>( @@ -20,7 +20,10 @@ pub async fn cli_run<'a, T: EthSpec>( env: Environment, ) -> Result<(), String> { match matches.subcommand() { - (create::CMD, Some(matches)) => create::cli_run::(matches, env).await, + (create_validators::CMD, Some(matches)) => { + create_validators::cli_run::(matches, env).await + } + (import_validators::CMD, Some(matches)) => import_validators::cli_run(matches).await, (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown From 85d084385759e3d4028deaf218dde50a87f9ee6c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 12:13:35 +1000 Subject: [PATCH 011/138] Add function to check against beacon node --- .../src/validators/create_validators.rs | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 161392c8540..54320961fdf 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -2,11 +2,16 @@ use super::common::*; use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; use clap::{App, Arg, ArgMatches}; use environment::Environment; -use eth2::lighthouse_vc::std_types::KeystoreJsonStr; +use eth2::{ + lighthouse_vc::std_types::KeystoreJsonStr, + types::{StateId, ValidatorId}, + BeaconNodeHttpClient, SensitiveUrl, Timeouts, +}; use eth2_wallet::WalletBuilder; use serde::Serialize; use std::fs; use std::path::{Path, PathBuf}; +use std::time::Duration; use types::*; pub const CMD: &str = "create"; @@ -22,10 +27,13 @@ pub const ETH1_WITHDRAWAL_ADDRESS_FLAG: &str = "eth1-withdrawal-address"; pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; +pub const BEACON_NODE_FLAG: &str = "beacon-node"; pub const VALIDATORS_FILENAME: &str = "validators.json"; pub const DEPOSITS_FILENAME: &str = "deposits.json"; +const BEACON_NODE_HTTP_TIMEOUT: Duration = Duration::from_secs(2); + struct ValidatorsAndDeposits { validators: Vec, deposits: Option>, @@ -161,6 +169,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .required(false), ) + .arg( + Arg::with_name(BEACON_NODE_FLAG) + .long(BEACON_NODE_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a beacon node using the beacon-API. \ + If this value is provided, an error will be raised if any validator \ + key here is already known as a validator by that beacon node. This helps \ + prevent the same validator being created twice and therefore slashable \ + conditions.", + ) + .takes_value(true), + ) } pub async fn cli_run<'a, T: EthSpec>( @@ -193,7 +214,7 @@ pub async fn cli_run<'a, T: EthSpec>( )); } - let validators_and_deposits = build_validator_spec_from_cli(matches, spec)?; + let validators_and_deposits = build_validator_spec_from_cli(matches, spec).await?; write_to_json_file(&validators_path, &validators_and_deposits.validators)?; @@ -214,7 +235,7 @@ fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Re .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) } -fn build_validator_spec_from_cli<'a>( +async fn build_validator_spec_from_cli<'a>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, ) -> Result { @@ -232,6 +253,25 @@ fn build_validator_spec_from_cli<'a>( let builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG); let fee_recipient: Option
= clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?; let gas_limit: Option = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?; + let bn_url: Option = clap_utils::parse_optional(matches, BEACON_NODE_FLAG)?; + + let bn_http_client = if let Some(bn_url) = bn_url { + let bn_http_client = + BeaconNodeHttpClient::new(bn_url, Timeouts::set_all(BEACON_NODE_HTTP_TIMEOUT)); + + let version = bn_http_client + .get_node_version() + .await + .map_err(|e| format!("Failed to test connection to beacon node: {:?}", e))? + .data + .version; + + eprintln!("Connected to beacon node running version {}", version); + + Some(bn_http_client) + } else { + None + }; let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; let voting_keystore_password = if specify_voting_keystore_password { @@ -285,6 +325,44 @@ fn build_validator_spec_from_cli<'a>( .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; let voting_keystore = keystores.voting; + // If the user has provided a beacon node URL, check that the validator doesn't already + // exist in the beacon chain. + if let Some(bn_http_client) = &bn_http_client { + let voting_public_key = voting_keystore + .public_key() + .ok_or_else(|| { + format!("Validator keystore at index {} is missing a public key", i) + })? + .into(); + + match bn_http_client + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(voting_public_key), + ) + .await + { + Ok(Some(_)) => { + return Err(format!( + "Validator {:?} at derivation index {} already exists in the beacon chain. \ + This indicates a slashing risk, be sure to never run the same validator on two \ + different validator clients", + voting_public_key, derivation_index + ))? + } + Ok(None) => eprintln!( + "Validator {:?} was not found in the beacon chain", + voting_public_key + ), + Err(e) => { + return Err(format!( + "Error checking if validator exists in beacon chain: {:?}", + e + )) + } + } + } + if let Some(deposits) = &mut deposits { // Decrypt the voting keystore so a deposit message can be signed. let voting_keypair = voting_keystore From ce7f29d0d964132b1c95eac5b822d14b0aad8e83 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 12:18:50 +1000 Subject: [PATCH 012/138] Appease clippy --- validator_manager/src/validators/create_validators.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 54320961fdf..166c815ecda 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -301,14 +301,14 @@ async fn build_validator_spec_from_cli<'a>( */ let mut validators = Vec::with_capacity(count as usize); - let mut deposits = disable_deposits.then(|| vec![]); + let mut deposits = disable_deposits.then(Vec::new); for (i, derivation_index) in (first_index..first_index + count).enumerate() { // If the voting keystore password was not provided by the user then use a unique random // string for each validator. let voting_keystore_password = voting_keystore_password .clone() - .unwrap_or_else(|| random_password_string()); + .unwrap_or_else(random_password_string); // Set the wallet to the appropriate derivation index. wallet @@ -372,7 +372,7 @@ async fn build_validator_spec_from_cli<'a>( let withdrawal_credentials = if let Some(eth1_withdrawal_address) = eth1_withdrawal_address { - WithdrawalCredentials::eth1(eth1_withdrawal_address, &spec) + WithdrawalCredentials::eth1(eth1_withdrawal_address, spec) } else { // Decrypt the withdrawal keystore so withdrawal credentials can be created. It's // not strictly necessary to decrypt the keystore since we can read the pubkey @@ -382,7 +382,7 @@ async fn build_validator_spec_from_cli<'a>( .withdrawal .decrypt_keypair(withdrawal_keystore_password.as_ref()) .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; - WithdrawalCredentials::bls(&withdrawal_keypair.pk, &spec) + WithdrawalCredentials::bls(&withdrawal_keypair.pk, spec) }; // Create a JSON structure equivalent to the one generated by @@ -391,7 +391,7 @@ async fn build_validator_spec_from_cli<'a>( &voting_keypair, withdrawal_credentials.into(), deposit_gwei, - &spec, + spec, )?; deposits.push(json_deposit); From 709598f09615d1cb3a467f37b33da059a5eca853 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 12:34:08 +1000 Subject: [PATCH 013/138] Link validator_manager into Lighthouse binary --- Cargo.lock | 1 + lighthouse/Cargo.toml | 1 + lighthouse/src/main.rs | 11 +++++++++++ validator_manager/src/lib.rs | 33 +++++++++++++++++++++------------ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3941aa7ec3f..3c0863bd3d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3648,6 +3648,7 @@ dependencies = [ "unused_port", "validator_client", "validator_dir", + "validator_manager", ] [[package]] diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index b74e1516f45..3b0416cf0f1 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -54,6 +54,7 @@ directory = { path = "../common/directory" } unused_port = { path = "../common/unused_port" } database_manager = { path = "../database_manager" } slasher = { path = "../slasher" } +validator_manager = { path = "../validator_manager" } [dev-dependencies] tempfile = "3.1.0" diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 7897494cc45..89928f1edd4 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -283,6 +283,7 @@ fn main() { .subcommand(validator_client::cli_app()) .subcommand(account_manager::cli_app()) .subcommand(database_manager::cli_app()) + .subcommand(validator_manager::cli_app()) .get_matches(); // Configure the allocator early in the process, before it has the chance to use the default values for @@ -498,6 +499,16 @@ fn run( return Ok(()); } + if let Some(sub_matches) = matches.subcommand_matches(account_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)?; + + // Exit as soon as account manager returns control. + return Ok(()); + } + if let Some(sub_matches) = matches.subcommand_matches(database_manager::CMD) { info!(log, "Running database manager for {} network", network_name); // Pass the entire `environment` to the database manager so it can run blocking operations. diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 5507b5aaa03..add88991aab 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -15,19 +15,28 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } /// Run the account manager, returning an error if the operation did not succeed. -pub async fn run<'a, T: EthSpec>( +pub fn run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, - env: Environment, + mut env: Environment, ) -> Result<(), String> { - match matches.subcommand() { - (validators::CMD, Some(matches)) => validators::cli_run(matches, env).await?, - (unknown, _) => { - return Err(format!( - "{} is not a valid {} command. See --help.", - unknown, CMD - )); - } - } + let context = env.core_context(); - Ok(()) + context + .executor + // This `block_on_dangerous` call reasonable since it is at the very highest level of the + // application, the rest of which is all async. All other functions below this should be + // async and should never call `block_on_dangerous` themselves. + .block_on_dangerous( + async { + match matches.subcommand() { + (validators::CMD, Some(matches)) => validators::cli_run(matches, env).await, + (unknown, _) => Err(format!( + "{} is not a valid {} command. See --help.", + unknown, CMD + )), + } + }, + "validator_manager", + ) + .ok_or("Shutting down")? } From 0cc72bc76838148d48d43fc76ab3be185791e9be Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 12:41:58 +1000 Subject: [PATCH 014/138] Fix mistake in command name --- lighthouse/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 89928f1edd4..5d7a523a7c3 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -499,7 +499,7 @@ fn run( return Ok(()); } - if let Some(sub_matches) = matches.subcommand_matches(account_manager::CMD) { + 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. From b025185438d1edfb2cdcaca9368cb07f2dc89f78 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 13:26:17 +1000 Subject: [PATCH 015/138] Avoid pulling env into blocking context --- validator_manager/src/lib.rs | 3 ++- .../src/validators/create_validators.rs | 8 +------- validator_manager/src/validators/mod.rs | 12 +++--------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index add88991aab..71314da0ba1 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -20,6 +20,7 @@ pub fn run<'a, T: EthSpec>( mut env: Environment, ) -> Result<(), String> { let context = env.core_context(); + let spec = context.eth2_config.spec.clone(); context .executor @@ -29,7 +30,7 @@ pub fn run<'a, T: EthSpec>( .block_on_dangerous( async { match matches.subcommand() { - (validators::CMD, Some(matches)) => validators::cli_run(matches, env).await, + (validators::CMD, Some(matches)) => validators::cli_run(matches, &spec).await, (unknown, _) => Err(format!( "{} is not a valid {} command. See --help.", unknown, CMD diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 166c815ecda..7df8789fffa 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -1,7 +1,6 @@ use super::common::*; use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; use clap::{App, Arg, ArgMatches}; -use environment::Environment; use eth2::{ lighthouse_vc::std_types::KeystoreJsonStr, types::{StateId, ValidatorId}, @@ -184,12 +183,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub async fn cli_run<'a, T: EthSpec>( - matches: &'a ArgMatches<'a>, - mut env: Environment, -) -> Result<(), String> { - let spec = &env.core_context().eth2_config.spec; - +pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Result<(), String> { let output_path: PathBuf = clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?; if !output_path.exists() { diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs index 9142ae1c992..0c9fa7476e5 100644 --- a/validator_manager/src/validators/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -3,8 +3,7 @@ pub mod create_validators; pub mod import_validators; use clap::{App, ArgMatches}; -use environment::Environment; -use types::EthSpec; +use types::ChainSpec; pub const CMD: &str = "validators"; @@ -15,14 +14,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(import_validators::cli_app()) } -pub async fn cli_run<'a, T: EthSpec>( - matches: &'a ArgMatches<'a>, - env: Environment, -) -> Result<(), String> { +pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Result<(), String> { match matches.subcommand() { - (create_validators::CMD, Some(matches)) => { - create_validators::cli_run::(matches, env).await - } + (create_validators::CMD, Some(matches)) => create_validators::cli_run(matches, spec).await, (import_validators::CMD, Some(matches)) => import_validators::cli_run(matches).await, (unknown, _) => Err(format!( "{} does not have a {} command. See --help", From 3b324fc6c53dd3769348f9fe36806eceff1c52e1 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 13:46:07 +1000 Subject: [PATCH 016/138] Tidy logging --- .../src/validators/create_validators.rs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 7df8789fffa..c4aba9a7854 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -210,6 +210,8 @@ pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Resul let validators_and_deposits = build_validator_spec_from_cli(matches, spec).await?; + eprintln!("Keystore generation complete"); + write_to_json_file(&validators_path, &validators_and_deposits.validators)?; if let Some(deposits) = &validators_and_deposits.deposits { @@ -220,6 +222,7 @@ pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Resul } fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Result<(), String> { + eprintln!("Writing {:?}", path.as_ref()); let mut file = fs::OpenOptions::new() .write(true) .create_new(true) @@ -294,6 +297,11 @@ async fn build_validator_spec_from_cli<'a>( * Start deriving individual validators. */ + eprintln!( + "Starting derivation of {} keystores. Each keystore may take several seconds.", + count + ); + let mut validators = Vec::with_capacity(count as usize); let mut deposits = disable_deposits.then(Vec::new); @@ -318,17 +326,14 @@ async fn build_validator_spec_from_cli<'a>( ) .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; let voting_keystore = keystores.voting; + let voting_public_key = voting_keystore + .public_key() + .ok_or_else(|| format!("Validator keystore at index {} is missing a public key", i))? + .into(); // If the user has provided a beacon node URL, check that the validator doesn't already // exist in the beacon chain. if let Some(bn_http_client) = &bn_http_client { - let voting_public_key = voting_keystore - .public_key() - .ok_or_else(|| { - format!("Validator keystore at index {} is missing a public key", i) - })? - .into(); - match bn_http_client .get_beacon_states_validator_id( StateId::Head, @@ -363,6 +368,15 @@ async fn build_validator_spec_from_cli<'a>( .decrypt_keypair(voting_keystore_password.as_ref()) .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; + // Sanity check to ensure the keystore is reporting the correct public key. + if PublicKeyBytes::from(voting_keypair.pk.clone()) != voting_public_key { + return Err(format!( + "Mismatch for keystore public key and derived public key \ + for derivation index {}", + derivation_index + )); + } + let withdrawal_credentials = if let Some(eth1_withdrawal_address) = eth1_withdrawal_address { @@ -401,6 +415,9 @@ async fn build_validator_spec_from_cli<'a>( builder_proposals: Some(builder_proposals), enabled: Some(true), }; + + eprintln!("{}/{}: {:?}", i.saturating_add(1), count, voting_public_key); + validators.push(validator); } From 7b620645d4606c5e9e566e7d2665f138b36835f8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 13:46:29 +1000 Subject: [PATCH 017/138] Fix deposit log creation --- validator_manager/src/validators/create_validators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index c4aba9a7854..f8b731ac479 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -303,7 +303,7 @@ async fn build_validator_spec_from_cli<'a>( ); let mut validators = Vec::with_capacity(count as usize); - let mut deposits = disable_deposits.then(Vec::new); + let mut deposits = (!disable_deposits).then(Vec::new); for (i, derivation_index) in (first_index..first_index + count).enumerate() { // If the voting keystore password was not provided by the user then use a unique random From c27d3dd8308bad310bd2c9817109e47c420147c2 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 14:11:48 +1000 Subject: [PATCH 018/138] Refactor to use `Config` struct --- lighthouse/src/main.rs | 2 +- validator_manager/src/lib.rs | 4 +- .../src/validators/create_validators.rs | 486 ++++++++++-------- validator_manager/src/validators/mod.rs | 11 +- 4 files changed, 295 insertions(+), 208 deletions(-) diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 5d7a523a7c3..c2372fac4d1 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -503,7 +503,7 @@ fn run( 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)?; + validator_manager::run::(sub_matches, environment)?; // Exit as soon as account manager returns control. return Ok(()); diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 71314da0ba1..88bdbd96dd5 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -30,7 +30,9 @@ pub fn run<'a, T: EthSpec>( .block_on_dangerous( async { match matches.subcommand() { - (validators::CMD, Some(matches)) => validators::cli_run(matches, &spec).await, + (validators::CMD, Some(matches)) => { + validators::cli_run::(matches, &spec).await + } (unknown, _) => Err(format!( "{} is not a valid {} command. See --help.", unknown, CMD diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index f8b731ac479..67a738394ee 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -33,11 +33,6 @@ pub const DEPOSITS_FILENAME: &str = "deposits.json"; const BEACON_NODE_HTTP_TIMEOUT: Duration = Duration::from_secs(2); -struct ValidatorsAndDeposits { - validators: Vec, - deposits: Option>, -} - pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about( @@ -183,8 +178,288 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Result<(), String> { - let output_path: PathBuf = clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?; +struct Config { + output_path: PathBuf, + first_index: u32, + count: u32, + deposit_gwei: u64, + mnemonic_path: Option, + stdin_inputs: bool, + disable_deposits: bool, + specify_voting_keystore_password: bool, + eth1_withdrawal_address: Option
, + builder_proposals: bool, + fee_recipient: Option
, + gas_limit: Option, + bn_url: Option, +} + +impl Config { + fn from_cli(matches: &ArgMatches, spec: &ChainSpec) -> Result { + Ok(Self { + output_path: clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?, + deposit_gwei: clap_utils::parse_optional(matches, DEPOSIT_GWEI_FLAG)? + .unwrap_or(spec.max_effective_balance), + first_index: clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?, + count: clap_utils::parse_required(matches, COUNT_FLAG)?, + mnemonic_path: clap_utils::parse_optional(matches, MNEMONIC_FLAG)?, + stdin_inputs: cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG), + disable_deposits: matches.is_present(DISABLE_DEPOSITS_FLAG), + specify_voting_keystore_password: matches + .is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG), + eth1_withdrawal_address: clap_utils::parse_optional( + matches, + ETH1_WITHDRAWAL_ADDRESS_FLAG, + )?, + builder_proposals: matches.is_present(BUILDER_PROPOSALS_FLAG), + fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, + gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, + bn_url: clap_utils::parse_optional(matches, BEACON_NODE_FLAG)?, + }) + } +} + +struct ValidatorsAndDeposits { + validators: Vec, + deposits: Option>, +} + +impl ValidatorsAndDeposits { + async fn new<'a, T: EthSpec>(config: Config, spec: &ChainSpec) -> Result { + let Config { + // The output path is handled upstream. + output_path: _, + first_index, + count, + deposit_gwei, + mnemonic_path, + stdin_inputs, + disable_deposits, + specify_voting_keystore_password, + eth1_withdrawal_address, + builder_proposals, + fee_recipient, + gas_limit, + bn_url, + } = config; + + let bn_http_client = if let Some(bn_url) = bn_url { + let bn_http_client = + BeaconNodeHttpClient::new(bn_url, Timeouts::set_all(BEACON_NODE_HTTP_TIMEOUT)); + + /* + * Print the version of the remote beacon node. + */ + let version = bn_http_client + .get_node_version() + .await + .map_err(|e| format!("Failed to test connection to beacon node: {:?}", e))? + .data + .version; + eprintln!("Connected to beacon node running version {}", version); + + /* + * Attempt to ensure that the beacon node is on the same network. + */ + let bn_config = bn_http_client + .get_config_spec::() + .await + .map_err(|e| format!("Failed to get spec from beacon node: {:?}", e))? + .data; + if let Some(config_name) = &bn_config.config_name { + eprintln!("Beacon node is on {} network", config_name) + } + let bn_spec = bn_config + .apply_to_chain_spec::(&T::default_spec()) + .ok_or("Beacon node appears to be on an incorrect network")?; + if bn_spec.genesis_fork_version != spec.genesis_fork_version { + if let Some(config_name) = bn_spec.config_name { + eprintln!("Beacon node is on {} network", config_name) + } + return Err("Beacon node appears to be on the wrong network".to_string()); + } + + Some(bn_http_client) + } else { + None + }; + + let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; + let voting_keystore_password = if specify_voting_keystore_password { + eprintln!("Please enter a voting keystore password when prompted."); + Some(read_password_from_user(stdin_inputs)?) + } else { + None + }; + + /* + * Generate a wallet to be used for HD key generation. + */ + + // A random password is always appropriate for the wallet since it is ephemeral. + let wallet_password = random_password_string(); + // A random password is always appropriate for the withdrawal keystore since we don't ever store + // it anywhere. + let withdrawal_keystore_password = random_password_string(); + let mut wallet = + WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) + .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? + .build() + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + + /* + * Start deriving individual validators. + */ + + eprintln!( + "Starting derivation of {} keystores. Each keystore may take several seconds.", + count + ); + + let mut validators = Vec::with_capacity(count as usize); + let mut deposits = (!disable_deposits).then(Vec::new); + + for (i, derivation_index) in (first_index..first_index + count).enumerate() { + // If the voting keystore password was not provided by the user then use a unique random + // string for each validator. + let voting_keystore_password = voting_keystore_password + .clone() + .unwrap_or_else(random_password_string); + + // Set the wallet to the appropriate derivation index. + wallet + .set_nextaccount(derivation_index) + .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; + + // Derive the keystore from the HD wallet. + let keystores = wallet + .next_validator( + wallet_password.as_ref(), + voting_keystore_password.as_ref(), + withdrawal_keystore_password.as_ref(), + ) + .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; + let voting_keystore = keystores.voting; + let voting_public_key = voting_keystore + .public_key() + .ok_or_else(|| { + format!("Validator keystore at index {} is missing a public key", i) + })? + .into(); + + // If the user has provided a beacon node URL, check that the validator doesn't already + // exist in the beacon chain. + if let Some(bn_http_client) = &bn_http_client { + match bn_http_client + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(voting_public_key), + ) + .await + { + Ok(Some(_)) => { + return Err(format!( + "Validator {:?} at derivation index {} already exists in the beacon chain. \ + This indicates a slashing risk, be sure to never run the same validator on two \ + different validator clients", + voting_public_key, derivation_index + ))? + } + Ok(None) => eprintln!( + "{:?} was not found in the beacon chain", + voting_public_key + ), + Err(e) => { + return Err(format!( + "Error checking if validator exists in beacon chain: {:?}", + e + )) + } + } + } + + if let Some(deposits) = &mut deposits { + // Decrypt the voting keystore so a deposit message can be signed. + let voting_keypair = voting_keystore + .decrypt_keypair(voting_keystore_password.as_ref()) + .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; + + // Sanity check to ensure the keystore is reporting the correct public key. + if PublicKeyBytes::from(voting_keypair.pk.clone()) != voting_public_key { + return Err(format!( + "Mismatch for keystore public key and derived public key \ + for derivation index {}", + derivation_index + )); + } + + let withdrawal_credentials = + if let Some(eth1_withdrawal_address) = eth1_withdrawal_address { + WithdrawalCredentials::eth1(eth1_withdrawal_address, spec) + } else { + // Decrypt the withdrawal keystore so withdrawal credentials can be created. It's + // not strictly necessary to decrypt the keystore since we can read the pubkey + // directly from the keystore. However we decrypt the keystore to be more certain + // that we have access to the withdrawal keys. + let withdrawal_keypair = keystores + .withdrawal + .decrypt_keypair(withdrawal_keystore_password.as_ref()) + .map_err(|e| { + format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e) + })?; + WithdrawalCredentials::bls(&withdrawal_keypair.pk, spec) + }; + + // Create a JSON structure equivalent to the one generated by + // `ethereum/staking-deposit-cli`. + let json_deposit = StandardDepositDataJson::new( + &voting_keypair, + withdrawal_credentials.into(), + deposit_gwei, + spec, + )?; + + deposits.push(json_deposit); + } + + let validator = ValidatorSpecification { + voting_keystore: KeystoreJsonStr(voting_keystore), + voting_keystore_password: voting_keystore_password.clone(), + // New validators have no slashing protection history. + slashing_protection: None, + fee_recipient, + gas_limit, + builder_proposals: Some(builder_proposals), + enabled: Some(true), + }; + + eprintln!( + "Completed {}/{}: {:?}", + i.saturating_add(1), + count, + voting_public_key + ); + + validators.push(validator); + } + + Ok(Self { + validators, + deposits, + }) + } +} + +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + spec: &ChainSpec, +) -> Result<(), String> { + let config = Config::from_cli(matches, spec)?; + run::(config, spec).await +} + +async fn run<'a, T: EthSpec>(config: Config, spec: &ChainSpec) -> Result<(), String> { + let output_path = config.output_path.clone(); if !output_path.exists() { fs::create_dir(&output_path) @@ -208,7 +483,7 @@ pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Resul )); } - let validators_and_deposits = build_validator_spec_from_cli(matches, spec).await?; + let validators_and_deposits = ValidatorsAndDeposits::new::(config, spec).await?; eprintln!("Keystore generation complete"); @@ -231,198 +506,3 @@ fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Re serde_json::to_writer(&mut file, contents) .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) } - -async fn build_validator_spec_from_cli<'a>( - matches: &'a ArgMatches<'a>, - spec: &ChainSpec, -) -> Result { - 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 = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; - let stdin_inputs = cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG); - let disable_deposits = matches.is_present(DISABLE_DEPOSITS_FLAG); - let specify_voting_keystore_password = - matches.is_present(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG); - let eth1_withdrawal_address: Option
= - clap_utils::parse_optional(matches, ETH1_WITHDRAWAL_ADDRESS_FLAG)?; - let builder_proposals = matches.is_present(BUILDER_PROPOSALS_FLAG); - let fee_recipient: Option
= clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?; - let gas_limit: Option = clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?; - let bn_url: Option = clap_utils::parse_optional(matches, BEACON_NODE_FLAG)?; - - let bn_http_client = if let Some(bn_url) = bn_url { - let bn_http_client = - BeaconNodeHttpClient::new(bn_url, Timeouts::set_all(BEACON_NODE_HTTP_TIMEOUT)); - - let version = bn_http_client - .get_node_version() - .await - .map_err(|e| format!("Failed to test connection to beacon node: {:?}", e))? - .data - .version; - - eprintln!("Connected to beacon node running version {}", version); - - Some(bn_http_client) - } else { - None - }; - - let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; - let voting_keystore_password = if specify_voting_keystore_password { - eprintln!("Please enter a voting keystore password when prompted."); - Some(read_password_from_user(stdin_inputs)?) - } else { - None - }; - - /* - * Generate a wallet to be used for HD key generation. - */ - - // A random password is always appropriate for the wallet since it is ephemeral. - let wallet_password = random_password_string(); - // A random password is always appropriate for the withdrawal keystore since we don't ever store - // it anywhere. - let withdrawal_keystore_password = random_password_string(); - let mut wallet = - WalletBuilder::from_mnemonic(&mnemonic, wallet_password.as_ref(), "".to_string()) - .map_err(|e| format!("Unable create seed from mnemonic: {:?}", e))? - .build() - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; - - /* - * Start deriving individual validators. - */ - - eprintln!( - "Starting derivation of {} keystores. Each keystore may take several seconds.", - count - ); - - let mut validators = Vec::with_capacity(count as usize); - let mut deposits = (!disable_deposits).then(Vec::new); - - for (i, derivation_index) in (first_index..first_index + count).enumerate() { - // If the voting keystore password was not provided by the user then use a unique random - // string for each validator. - let voting_keystore_password = voting_keystore_password - .clone() - .unwrap_or_else(random_password_string); - - // Set the wallet to the appropriate derivation index. - wallet - .set_nextaccount(derivation_index) - .map_err(|e| format!("Failure to set validator derivation index: {:?}", e))?; - - // Derive the keystore from the HD wallet. - let keystores = wallet - .next_validator( - wallet_password.as_ref(), - voting_keystore_password.as_ref(), - withdrawal_keystore_password.as_ref(), - ) - .map_err(|e| format!("Failed to derive keystore {}: {:?}", i, e))?; - let voting_keystore = keystores.voting; - let voting_public_key = voting_keystore - .public_key() - .ok_or_else(|| format!("Validator keystore at index {} is missing a public key", i))? - .into(); - - // If the user has provided a beacon node URL, check that the validator doesn't already - // exist in the beacon chain. - if let Some(bn_http_client) = &bn_http_client { - match bn_http_client - .get_beacon_states_validator_id( - StateId::Head, - &ValidatorId::PublicKey(voting_public_key), - ) - .await - { - Ok(Some(_)) => { - return Err(format!( - "Validator {:?} at derivation index {} already exists in the beacon chain. \ - This indicates a slashing risk, be sure to never run the same validator on two \ - different validator clients", - voting_public_key, derivation_index - ))? - } - Ok(None) => eprintln!( - "Validator {:?} was not found in the beacon chain", - voting_public_key - ), - Err(e) => { - return Err(format!( - "Error checking if validator exists in beacon chain: {:?}", - e - )) - } - } - } - - if let Some(deposits) = &mut deposits { - // Decrypt the voting keystore so a deposit message can be signed. - let voting_keypair = voting_keystore - .decrypt_keypair(voting_keystore_password.as_ref()) - .map_err(|e| format!("Failed to decrypt voting keystore {}: {:?}", i, e))?; - - // Sanity check to ensure the keystore is reporting the correct public key. - if PublicKeyBytes::from(voting_keypair.pk.clone()) != voting_public_key { - return Err(format!( - "Mismatch for keystore public key and derived public key \ - for derivation index {}", - derivation_index - )); - } - - let withdrawal_credentials = if let Some(eth1_withdrawal_address) = - eth1_withdrawal_address - { - WithdrawalCredentials::eth1(eth1_withdrawal_address, spec) - } else { - // Decrypt the withdrawal keystore so withdrawal credentials can be created. It's - // not strictly necessary to decrypt the keystore since we can read the pubkey - // directly from the keystore. However we decrypt the keystore to be more certain - // that we have access to the withdrawal keys. - let withdrawal_keypair = keystores - .withdrawal - .decrypt_keypair(withdrawal_keystore_password.as_ref()) - .map_err(|e| format!("Failed to decrypt withdrawal keystore {}: {:?}", i, e))?; - WithdrawalCredentials::bls(&withdrawal_keypair.pk, spec) - }; - - // Create a JSON structure equivalent to the one generated by - // `ethereum/staking-deposit-cli`. - let json_deposit = StandardDepositDataJson::new( - &voting_keypair, - withdrawal_credentials.into(), - deposit_gwei, - spec, - )?; - - deposits.push(json_deposit); - } - - let validator = ValidatorSpecification { - voting_keystore: KeystoreJsonStr(voting_keystore), - voting_keystore_password: voting_keystore_password.clone(), - // New validators have no slashing protection history. - slashing_protection: None, - fee_recipient, - gas_limit, - builder_proposals: Some(builder_proposals), - enabled: Some(true), - }; - - eprintln!("{}/{}: {:?}", i.saturating_add(1), count, voting_public_key); - - validators.push(validator); - } - - Ok(ValidatorsAndDeposits { - validators, - deposits, - }) -} diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs index 0c9fa7476e5..04ff692401a 100644 --- a/validator_manager/src/validators/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -3,7 +3,7 @@ pub mod create_validators; pub mod import_validators; use clap::{App, ArgMatches}; -use types::ChainSpec; +use types::{ChainSpec, EthSpec}; pub const CMD: &str = "validators"; @@ -14,9 +14,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(import_validators::cli_app()) } -pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>, spec: &ChainSpec) -> Result<(), String> { +pub async fn cli_run<'a, T: EthSpec>( + matches: &'a ArgMatches<'a>, + spec: &ChainSpec, +) -> Result<(), String> { match matches.subcommand() { - (create_validators::CMD, Some(matches)) => create_validators::cli_run(matches, spec).await, + (create_validators::CMD, Some(matches)) => { + create_validators::cli_run::(matches, spec).await + } (import_validators::CMD, Some(matches)) => import_validators::cli_run(matches).await, (unknown, _) => Err(format!( "{} does not have a {} command. See --help", From 61e0347326ea38fd1cb08a7581b80b570ea5e01f Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 15:30:18 +1000 Subject: [PATCH 019/138] Add testing for create command --- Cargo.lock | 2 + validator_manager/Cargo.toml | 4 + .../src/validators/common/mod.rs | 2 +- .../src/validators/create_validators.rs | 273 +++++++++++++++++- 4 files changed, 274 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c0863bd3d5..21263063283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7372,6 +7372,8 @@ dependencies = [ "eth2_wallet", "serde", "serde_json", + "tempfile", + "tokio", "tree_hash", "types", ] diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 041d548bea8..d5dca7d53ee 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -20,3 +20,7 @@ serde_json = "1.0.58" eth2_serde_utils = "0.1.1" tree_hash = "0.4.1" eth2 = { path = "../common/eth2", features = ["lighthouse"]} + +[dev-dependencies] +tempfile = "3.1.0" +tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } diff --git a/validator_manager/src/validators/common/mod.rs b/validator_manager/src/validators/common/mod.rs index 67bee65b32a..a910d46e052 100644 --- a/validator_manager/src/validators/common/mod.rs +++ b/validator_manager/src/validators/common/mod.rs @@ -33,7 +33,7 @@ pub struct CreateSpec { /// 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)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct StandardDepositDataJson { pub pubkey: PublicKeyBytes, pub withdrawal_credentials: Hash256, diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 67a738394ee..c4dbea7df5a 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -178,7 +178,10 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -struct Config { +/// The CLI arguments are parsed into this struct before running the application. This step of +/// indirection allows for testing the underlying logic without needing to parse CLI arguments. +#[derive(Clone)] +struct CreateConfig { output_path: PathBuf, first_index: u32, count: u32, @@ -194,7 +197,7 @@ struct Config { bn_url: Option, } -impl Config { +impl CreateConfig { fn from_cli(matches: &ArgMatches, spec: &ChainSpec) -> Result { Ok(Self { output_path: clap_utils::parse_required(matches, OUTPUT_PATH_FLAG)?, @@ -225,8 +228,8 @@ struct ValidatorsAndDeposits { } impl ValidatorsAndDeposits { - async fn new<'a, T: EthSpec>(config: Config, spec: &ChainSpec) -> Result { - let Config { + async fn new<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result { + let CreateConfig { // The output path is handled upstream. output_path: _, first_index, @@ -243,6 +246,10 @@ impl ValidatorsAndDeposits { bn_url, } = config; + if count == 0 { + return Err(format!("--{} cannot be 0", COUNT_FLAG)); + } + let bn_http_client = if let Some(bn_url) = bn_url { let bn_http_client = BeaconNodeHttpClient::new(bn_url, Timeouts::set_all(BEACON_NODE_HTTP_TIMEOUT)); @@ -454,11 +461,11 @@ pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, ) -> Result<(), String> { - let config = Config::from_cli(matches, spec)?; + let config = CreateConfig::from_cli(matches, spec)?; run::(config, spec).await } -async fn run<'a, T: EthSpec>(config: Config, spec: &ChainSpec) -> Result<(), String> { +async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { let output_path = config.output_path.clone(); if !output_path.exists() { @@ -506,3 +513,257 @@ fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Re serde_json::to_writer(&mut file, contents) .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) } + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + use tempfile::{tempdir, TempDir}; + use tree_hash::TreeHash; + + type E = MainnetEthSpec; + + struct TestBuilder { + spec: ChainSpec, + output_dir: TempDir, + mnemonic_dir: TempDir, + config: CreateConfig, + } + + impl Default for TestBuilder { + fn default() -> Self { + let spec = E::default_spec(); + let output_dir = tempdir().unwrap(); + let mnemonic_dir = tempdir().unwrap(); + let mnemonic_path = mnemonic_dir.path().join("mnemonic"); + fs::write( + &mnemonic_path, + "test test test test test test test test test test test waste", + ) + .unwrap(); + + let config = CreateConfig { + output_path: output_dir.path().into(), + first_index: 0, + count: 1, + deposit_gwei: spec.max_effective_balance, + mnemonic_path: Some(mnemonic_path), + stdin_inputs: false, + disable_deposits: false, + specify_voting_keystore_password: false, + eth1_withdrawal_address: None, + builder_proposals: false, + fee_recipient: None, + gas_limit: None, + bn_url: None, + }; + + Self { + spec, + output_dir, + mnemonic_dir, + config, + } + } + } + + impl TestBuilder { + fn mutate_config(mut self, func: F) -> Self { + func(&mut self.config); + self + } + + async fn run_test(self) -> TestResult { + let Self { + spec, + output_dir, + mnemonic_dir, + config, + } = self; + + let result = run::(config.clone(), &spec).await; + + if result.is_ok() { + let validators_file_contents = + fs::read_to_string(output_dir.path().join(VALIDATORS_FILENAME)).unwrap(); + let validators: Vec = + serde_json::from_str(&validators_file_contents).unwrap(); + + assert_eq!(validators.len(), config.count as usize); + + for (i, validator) in validators.iter().enumerate() { + let voting_keystore = &validator.voting_keystore.0; + let keypair = voting_keystore + .decrypt_keypair(validator.voting_keystore_password.as_ref()) + .unwrap(); + assert_eq!(keypair.pk, voting_keystore.public_key().unwrap()); + assert_eq!( + voting_keystore.path().unwrap(), + format!("m/12381/3600/{}/0/0", config.first_index as usize + i) + ); + assert!(validator.slashing_protection.is_none()); + assert_eq!(validator.fee_recipient, config.fee_recipient); + assert_eq!(validator.gas_limit, config.gas_limit); + assert_eq!(validator.builder_proposals, Some(config.builder_proposals)); + assert_eq!(validator.enabled, Some(true)); + } + + let deposits_path = output_dir.path().join(DEPOSITS_FILENAME); + if config.disable_deposits { + assert!(!deposits_path.exists()); + } else { + let deposits_file_contents = fs::read_to_string(&deposits_path).unwrap(); + let deposits: Vec = + serde_json::from_str(&deposits_file_contents).unwrap(); + + assert_eq!(deposits.len(), config.count as usize); + + for (validator, deposit) in validators.iter().zip(deposits.iter()) { + let validator_pubkey = validator.voting_keystore.0.public_key().unwrap(); + assert_eq!(deposit.pubkey, validator_pubkey.clone().into()); + if let Some(address) = config.eth1_withdrawal_address { + assert_eq!( + deposit.withdrawal_credentials.as_bytes()[0], + spec.eth1_address_withdrawal_prefix_byte + ); + assert_eq!( + &deposit.withdrawal_credentials.as_bytes()[12..], + address.as_bytes() + ); + } else { + assert_eq!( + deposit.withdrawal_credentials.as_bytes()[0], + spec.bls_withdrawal_prefix_byte + ); + } + assert_eq!(deposit.amount, config.deposit_gwei); + let deposit_message_root = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: SignatureBytes::empty(), + } + .as_deposit_message() + .signing_root(spec.get_deposit_domain()); + assert!(deposit + .signature + .decompress() + .unwrap() + .verify(&validator_pubkey, deposit_message_root)); + assert_eq!(deposit.fork_version, spec.genesis_fork_version); + assert_eq!( + &deposit.eth2_network_name, + spec.config_name.as_ref().unwrap() + ); + assert_eq!(deposit.deposit_message_root, deposit_message_root); + assert_eq!( + deposit.deposit_data_root, + DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone() + } + .tree_hash_root() + ); + } + } + // + } + + // The directory containing the mnemonic can now be removed. + drop(mnemonic_dir); + + TestResult { result, output_dir } + } + } + + #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. + struct TestResult { + result: Result<(), String>, + output_dir: TempDir, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + + fn assert_err(self) { + assert!(self.result.is_err()) + } + } + + #[tokio::test] + async fn default_test_values() { + TestBuilder::default().run_test().await.assert_ok(); + } + + #[tokio::test] + async fn default_test_values_deposits_disabled() { + TestBuilder::default() + .mutate_config(|config| config.disable_deposits = true) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn count_is_zero() { + TestBuilder::default() + .mutate_config(|config| config.count = 0) + .run_test() + .await + .assert_err(); + } + + #[tokio::test] + async fn eth1_withdrawal_addresses() { + TestBuilder::default() + .mutate_config(|config| { + config.count = 2; + config.eth1_withdrawal_address = + Some(Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap()); + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn non_zero_first_index() { + TestBuilder::default() + .mutate_config(|config| { + config.first_index = 2; + config.count = 2; + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn misc_modifications() { + TestBuilder::default() + .mutate_config(|config| { + config.deposit_gwei = 42; + config.builder_proposals = true; + config.gas_limit = Some(1337); + }) + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn bogus_bn_url() { + TestBuilder::default() + .mutate_config(|config| { + config.bn_url = + Some(SensitiveUrl::from_str("http://sdjfvwfhsdhfschwkeyfwhwlga.com").unwrap()); + }) + .run_test() + .await + .assert_err(); + } +} From 5edcfb7c2bc9c828a22602ca296abc93fba6d5cf Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 17 Aug 2022 16:11:38 +1000 Subject: [PATCH 020/138] Start working on test vectors --- validator_manager/test_vectors/.gitignore | 1 + validator_manager/test_vectors/generate.py | 86 ++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 validator_manager/test_vectors/.gitignore create mode 100644 validator_manager/test_vectors/generate.py diff --git a/validator_manager/test_vectors/.gitignore b/validator_manager/test_vectors/.gitignore new file mode 100644 index 00000000000..3fec32c8427 --- /dev/null +++ b/validator_manager/test_vectors/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/validator_manager/test_vectors/generate.py b/validator_manager/test_vectors/generate.py new file mode 100644 index 00000000000..bb8090f1aeb --- /dev/null +++ b/validator_manager/test_vectors/generate.py @@ -0,0 +1,86 @@ +import os +import sys +import shutil +import subprocess +from subprocess import Popen, PIPE, STDOUT + +NUM_VALIDATORS=3 +TEST_MNEMONIC = "test test test test test test test test test test test waste" +WALLET_NAME="test_wallet" + +tmp_dir = os.path.join(".", "tmp") +mnemonic_path = os.path.join(tmp_dir, "mnemonic.txt") +sdc_dir = os.path.join(tmp_dir, "sdc") +sdc_git_dir = os.path.join(sdc_dir, "staking-deposit-cli") + + +def setup(): + if os.path.exists(tmp_dir): + cleanup() + + os.mkdir(tmp_dir) + os.mkdir(sdc_dir) + + setup_sdc() + with open(mnemonic_path, "x") as file: + file.write(TEST_MNEMONIC) + + +def cleanup(): + shutil.rmtree(tmp_dir) + + +def setup_sdc(): + result = subprocess.run([ + "git", + "clone", + "--single-branch", + "https://github.com/ethereum/staking-deposit-cli.git", + str(sdc_git_dir) + ]) + assert(result.returncode == 0) + result = subprocess.run([ + "pip", + "install", + "-r", + "requirements.txt", + ], cwd=sdc_git_dir) + assert(result.returncode == 0) + result = subprocess.run([ + "python", + "setup.py", + "install", + ], cwd=sdc_git_dir) + assert(result.returncode == 0) + + +def sdc_generate(network, first_index, count): + process = Popen([ + '/bin/sh', + 'deposit.sh', + 'existing-mnemonic', + ], stdout=PIPE, stdin=PIPE, stderr=STDOUT, cwd=sdc_git_dir, text=True) + process.stdin.write('3\n') # Select "3. English" as the mnemonic language. + process.stdin.write(TEST_MNEMONIC + '\n') + process.stdin.write(str(first_index) + '\n') + process.stdin.write(str(first_index) + '\n') + process.stdin.write(str(count) + '\n') + process.stdin.write(network + '\n') + process.stdin.write('junk_password\n') + process.stdin.write('junk_password\n') + process.wait() + # process.wait() + # Select "3. English" as the mnemonic language. + # p.communicate(input='3'.encode('utf-8')) + # Input the mnemonic. + # p.communicate(input=TEST_MNEMONIC.encode('utf-8')) + + + +def test(network): + setup() + sdc_generate(network, 0, 2) + # cleanup() + + +test("mainnet") From 79b5c6f7142ec57984bc043db3ec45b111fa06bb Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 14:05:34 +1000 Subject: [PATCH 021/138] Add working test vector generator --- validator_manager/test_vectors/generate.py | 56 +++++++++++++--------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/validator_manager/test_vectors/generate.py b/validator_manager/test_vectors/generate.py index bb8090f1aeb..f9dfa5ed847 100644 --- a/validator_manager/test_vectors/generate.py +++ b/validator_manager/test_vectors/generate.py @@ -12,14 +12,18 @@ mnemonic_path = os.path.join(tmp_dir, "mnemonic.txt") sdc_dir = os.path.join(tmp_dir, "sdc") sdc_git_dir = os.path.join(sdc_dir, "staking-deposit-cli") +vectors_dir = os.path.join(".", "vectors") def setup(): - if os.path.exists(tmp_dir): - cleanup() + cleanup() + + if os.path.exists(vectors_dir): + shutil.rmtree(vectors_dir) os.mkdir(tmp_dir) os.mkdir(sdc_dir) + os.mkdir(vectors_dir) setup_sdc() with open(mnemonic_path, "x") as file: @@ -27,7 +31,8 @@ def setup(): def cleanup(): - shutil.rmtree(tmp_dir) + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir) def setup_sdc(): @@ -53,34 +58,39 @@ def setup_sdc(): ], cwd=sdc_git_dir) assert(result.returncode == 0) - def sdc_generate(network, first_index, count): + test_name = "{}_first_{}_count_{}".format(network, first_index, count) + output_dir = os.path.join(vectors_dir, test_name) + os.mkdir(output_dir) + + print("Running " + test_name) process = Popen([ '/bin/sh', 'deposit.sh', + '--language', 'english', + '--non_interactive', 'existing-mnemonic', - ], stdout=PIPE, stdin=PIPE, stderr=STDOUT, cwd=sdc_git_dir, text=True) - process.stdin.write('3\n') # Select "3. English" as the mnemonic language. - process.stdin.write(TEST_MNEMONIC + '\n') - process.stdin.write(str(first_index) + '\n') - process.stdin.write(str(first_index) + '\n') - process.stdin.write(str(count) + '\n') - process.stdin.write(network + '\n') - process.stdin.write('junk_password\n') - process.stdin.write('junk_password\n') + '--validator_start_index', str(first_index), + '--num_validators', str(count), + '--mnemonic', TEST_MNEMONIC, + '--chain', 'mainnet', + '--keystore_password', 'MyPassword', + '--folder', os.path.abspath(output_dir), + ], cwd=sdc_git_dir, text=True, stdin = PIPE) process.wait() - # process.wait() - # Select "3. English" as the mnemonic language. - # p.communicate(input='3'.encode('utf-8')) - # Input the mnemonic. - # p.communicate(input=TEST_MNEMONIC.encode('utf-8')) -def test(network): - setup() - sdc_generate(network, 0, 2) - # cleanup() +def test_network(network): + sdc_generate(network, first_index=0, count=1) + sdc_generate(network, first_index=0, count=2) + sdc_generate(network, first_index=0, count=3) + sdc_generate(network, first_index=12, count=1) + sdc_generate(network, first_index=99, count=2) + sdc_generate(network, first_index=1024, count=3) -test("mainnet") +setup() +test_network("mainnet") +test_network("prater") +cleanup() From 3bab10bb68eb1b86f734bf969adfc277395950c6 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 16:14:22 +1000 Subject: [PATCH 022/138] Pass test vectors --- Cargo.lock | 2 + validator_manager/Cargo.toml | 3 + .../src/validators/common/mod.rs | 108 +++++++++++++- .../src/validators/create_validators.rs | 136 ++++++++++++++++-- validator_manager/test_vectors/generate.py | 25 +++- 5 files changed, 247 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21263063283..899dcb09d8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7370,6 +7370,8 @@ dependencies = [ "eth2_network_config", "eth2_serde_utils", "eth2_wallet", + "hex", + "regex", "serde", "serde_json", "tempfile", diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index d5dca7d53ee..38cbbae6698 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -20,7 +20,10 @@ serde_json = "1.0.58" eth2_serde_utils = "0.1.1" tree_hash = "0.4.1" eth2 = { path = "../common/eth2", features = ["lighthouse"]} +hex = "0.4.2" [dev-dependencies] tempfile = "3.1.0" tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } +regex = "1.6.0" +eth2_network_config = { path = "../common/eth2_network_config" } diff --git a/validator_manager/src/validators/common/mod.rs b/validator_manager/src/validators/common/mod.rs index a910d46e052..0b4743aaf5d 100644 --- a/validator_manager/src/validators/common/mod.rs +++ b/validator_manager/src/validators/common/mod.rs @@ -6,6 +6,15 @@ use std::path::PathBuf; use tree_hash::TreeHash; use types::*; +/// When the `ethereum/staking-deposit-cli` tool generates deposit data JSON, it adds a +/// `deposit_cli_version` to protect the web-based "Launchpad" tool against a breaking change that +/// was introduced in `ethereum/staking-deposit-cli`. Lighthouse don't really have a version that it +/// can use here, so we choose a static string that is: +/// +/// 1. High enough that it's accepted by Launchpad. +/// 2. Weird enough to identify Lighthouse. +const LIGHTHOUSE_DEPOSIT_CLI_VERSION: &str = "20.18.20"; + #[derive(Serialize, Deserialize)] pub struct ValidatorSpecification { pub voting_keystore: KeystoreJsonStr, @@ -35,16 +44,22 @@ pub struct CreateSpec { /// https://github.com/ethereum/staking-deposit-cli/blob/76ed78224fdfe3daca788d12442b3d1a37978296/staking_deposit/credentials.py#L131-L144 #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct StandardDepositDataJson { + #[serde(with = "public_key_bytes_without_0x_prefix")] pub pubkey: PublicKeyBytes, + #[serde(with = "hash256_without_0x_prefix")] pub withdrawal_credentials: Hash256, #[serde(with = "eth2_serde_utils::quoted_u64")] pub amount: u64, + #[serde(with = "signature_bytes_without_0x_prefix")] pub signature: SignatureBytes, - #[serde(with = "eth2_serde_utils::bytes_4_hex")] + #[serde(with = "bytes_4_without_0x_prefix")] pub fork_version: [u8; 4], - pub eth2_network_name: String, + pub network_name: String, + #[serde(with = "hash256_without_0x_prefix")] pub deposit_message_root: Hash256, + #[serde(with = "hash256_without_0x_prefix")] pub deposit_data_root: Hash256, + pub deposit_cli_version: String, } impl StandardDepositDataJson { @@ -65,8 +80,7 @@ impl StandardDepositDataJson { deposit_data }; - let domain = spec.get_deposit_domain(); - let deposit_message_root = deposit_data.as_deposit_message().signing_root(domain); + let deposit_message_root = deposit_data.as_deposit_message().tree_hash_root(); let deposit_data_root = deposit_data.tree_hash_root(); let DepositData { @@ -82,12 +96,96 @@ impl StandardDepositDataJson { amount, signature, fork_version: spec.genesis_fork_version, - eth2_network_name: spec + network_name: spec .config_name .clone() .ok_or("The network specification does not have a CONFIG_NAME set")?, deposit_message_root, deposit_data_root, + deposit_cli_version: LIGHTHOUSE_DEPOSIT_CLI_VERSION.to_string(), }) } } + +macro_rules! without_0x_prefix { + ($mod_name: ident, $type: ty) => { + pub mod $mod_name { + use super::*; + use std::str::FromStr; + + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = $type; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("ascii hex without a 0x prefix") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + <$type>::from_str(&format!("0x{}", v)).map_err(serde::de::Error::custom) + } + } + + /// Serialize with quotes. + pub fn serialize(value: &$type, serializer: S) -> Result + where + S: serde::Serializer, + { + let with_prefix = format!("{:?}", value); + let without_prefix = with_prefix + .strip_prefix("0x") + .ok_or_else(|| serde::ser::Error::custom("serialization is missing 0x"))?; + serializer.serialize_str(&without_prefix) + } + + /// Deserialize with quotes. + pub fn deserialize<'de, D>(deserializer: D) -> Result<$type, D::Error> + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(Visitor) + } + } + }; +} + +without_0x_prefix!(hash256_without_0x_prefix, Hash256); +without_0x_prefix!(signature_bytes_without_0x_prefix, SignatureBytes); +without_0x_prefix!(public_key_bytes_without_0x_prefix, PublicKeyBytes); + +mod bytes_4_without_0x_prefix { + use serde::de::Error; + + const BYTES_LEN: usize = 4; + + pub fn serialize(bytes: &[u8; BYTES_LEN], serializer: S) -> Result + where + S: serde::Serializer, + { + let hex_string = &hex::encode(&bytes); + serializer.serialize_str(&hex_string) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; BYTES_LEN], D::Error> + where + D: serde::Deserializer<'de>, + { + let decoded = deserializer.deserialize_str(eth2_serde_utils::hex::HexVisitor)?; + + if decoded.len() != BYTES_LEN { + return Err(D::Error::custom(format!( + "expected {} bytes for array, got {}", + BYTES_LEN, + decoded.len() + ))); + } + + let mut array = [0; BYTES_LEN]; + array.copy_from_slice(&decoded); + Ok(array) + } +} diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index c4dbea7df5a..1ef870ac5e0 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -517,12 +517,16 @@ fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Re #[cfg(test)] mod tests { use super::*; + use eth2_network_config::Eth2NetworkConfig; + use regex::Regex; use std::str::FromStr; use tempfile::{tempdir, TempDir}; use tree_hash::TreeHash; type E = MainnetEthSpec; + const TEST_VECTOR_DEPOSIT_CLI_VERSION: &str = "2.3.0"; + struct TestBuilder { spec: ChainSpec, output_dir: TempDir, @@ -532,7 +536,12 @@ mod tests { impl Default for TestBuilder { fn default() -> Self { - let spec = E::default_spec(); + Self::new(E::default_spec()) + } + } + + impl TestBuilder { + fn new(spec: ChainSpec) -> Self { let output_dir = tempdir().unwrap(); let mnemonic_dir = tempdir().unwrap(); let mnemonic_path = mnemonic_dir.path().join("mnemonic"); @@ -565,9 +574,7 @@ mod tests { config, } } - } - impl TestBuilder { fn mutate_config(mut self, func: F) -> Self { func(&mut self.config); self @@ -637,25 +644,23 @@ mod tests { ); } assert_eq!(deposit.amount, config.deposit_gwei); - let deposit_message_root = DepositData { + let deposit_message = DepositData { pubkey: deposit.pubkey, withdrawal_credentials: deposit.withdrawal_credentials, amount: deposit.amount, signature: SignatureBytes::empty(), } - .as_deposit_message() - .signing_root(spec.get_deposit_domain()); - assert!(deposit - .signature - .decompress() - .unwrap() - .verify(&validator_pubkey, deposit_message_root)); + .as_deposit_message(); + assert!(deposit.signature.decompress().unwrap().verify( + &validator_pubkey, + deposit_message.signing_root(spec.get_deposit_domain()) + )); assert_eq!(deposit.fork_version, spec.genesis_fork_version); + assert_eq!(&deposit.network_name, spec.config_name.as_ref().unwrap()); assert_eq!( - &deposit.eth2_network_name, - spec.config_name.as_ref().unwrap() + deposit.deposit_message_root, + deposit_message.tree_hash_root() ); - assert_eq!(deposit.deposit_message_root, deposit_message_root); assert_eq!( deposit.deposit_data_root, DepositData { @@ -668,7 +673,6 @@ mod tests { ); } } - // } // The directory containing the mnemonic can now be removed. @@ -766,4 +770,106 @@ mod tests { .await .assert_err(); } + + #[tokio::test] + async fn staking_deposit_cli_vectors() { + let vectors_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("test_vectors") + .join("vectors"); + for entry in fs::read_dir(vectors_dir).unwrap() { + let entry = entry.unwrap(); + let file_name = entry.file_name(); + let vector_name = file_name.to_str().unwrap(); + let path = entry.path(); + // Leave this `println!` so we can tell which test fails. + println!("Running test {}", vector_name); + run_test_vector(vector_name, &path).await; + } + } + + async fn run_test_vector>(name: &str, vectors_path: P) { + /* + * Parse the test vector name into a set of test parameters. + */ + let re = Regex::new(r"(.*)_(.*)_(.*)_(.*)_(.*)_(.*)_(.*)").unwrap(); + let capture = re.captures_iter(name).next().unwrap(); + let network = capture.get(1).unwrap().as_str(); + let first = u32::from_str(capture.get(3).unwrap().as_str()).unwrap(); + let count = u32::from_str(capture.get(5).unwrap().as_str()).unwrap(); + let uses_eth1 = bool::from_str(capture.get(7).unwrap().as_str()).unwrap(); + + /* + * Use the test parameters to generate equivalent files "locally" (i.e., with our code). + */ + + let spec = Eth2NetworkConfig::constant(network) + .unwrap() + .unwrap() + .chain_spec::() + .unwrap(); + + let test_result = TestBuilder::new(spec) + .mutate_config(|config| { + config.first_index = first; + config.count = count; + if uses_eth1 { + config.eth1_withdrawal_address = Some( + Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap(), + ); + } + }) + .run_test() + .await; + let TestResult { result, output_dir } = test_result; + result.expect("local generation should succeed"); + + /* + * Ensure the deposit data is identical when parsed as JSON. + */ + + let local_deposits = { + let path = output_dir.path().join(DEPOSITS_FILENAME); + let contents = fs::read_to_string(&path).unwrap(); + let mut deposits: Vec = + serde_json::from_str(&contents).unwrap(); + for deposit in &mut deposits { + // Ensures we can match test vectors. + deposit.deposit_cli_version = TEST_VECTOR_DEPOSIT_CLI_VERSION.to_string(); + + // We use "prater" and the vectors use "goerli" now. The two names refer to the same + // network so there should be no issue here. + if deposit.network_name == "prater" { + deposit.network_name = "goerli".to_string(); + } + } + deposits + }; + let vector_deposits: Vec = { + let path = fs::read_dir(vectors_path.as_ref().join("validator_keys")) + .unwrap() + .find_map(|entry| { + let entry = entry.unwrap(); + let file_name = entry.file_name(); + if file_name.to_str().unwrap().starts_with("deposit_data") { + Some(entry.path()) + } else { + None + } + }) + .unwrap(); + let contents = fs::read_to_string(&path).unwrap(); + serde_json::from_str(&contents).unwrap() + }; + + assert_eq!(local_deposits, vector_deposits); + + /* + * Note: we don't check the keystores generated by the deposit-cli since there is little + * value in this. + * + * If we check the deposits then we are verifying the signature across the deposit message. + * This implicitly verifies that the keypair generated by the deposit-cli is identical to + * the one created by Lighthouse. + */ + } } diff --git a/validator_manager/test_vectors/generate.py b/validator_manager/test_vectors/generate.py index f9dfa5ed847..45f73fb7b46 100644 --- a/validator_manager/test_vectors/generate.py +++ b/validator_manager/test_vectors/generate.py @@ -58,13 +58,20 @@ def setup_sdc(): ], cwd=sdc_git_dir) assert(result.returncode == 0) -def sdc_generate(network, first_index, count): - test_name = "{}_first_{}_count_{}".format(network, first_index, count) +def sdc_generate(network, first_index, count, eth1_withdrawal_address=None): + if eth1_withdrawal_address is not None: + eth1_flags = ['--eth1_withdrawal_address', eth1_withdrawal_address] + uses_eth1 = True + else: + eth1_flags = [] + uses_eth1 = False + + test_name = "{}_first_{}_count_{}_eth1_{}".format(network, first_index, count, + str(uses_eth1).lower()) output_dir = os.path.join(vectors_dir, test_name) os.mkdir(output_dir) - print("Running " + test_name) - process = Popen([ + command = [ '/bin/sh', 'deposit.sh', '--language', 'english', @@ -73,10 +80,13 @@ def sdc_generate(network, first_index, count): '--validator_start_index', str(first_index), '--num_validators', str(count), '--mnemonic', TEST_MNEMONIC, - '--chain', 'mainnet', + '--chain', network, '--keystore_password', 'MyPassword', '--folder', os.path.abspath(output_dir), - ], cwd=sdc_git_dir, text=True, stdin = PIPE) + ] + eth1_flags + + print("Running " + test_name) + process = Popen(command, cwd=sdc_git_dir, text=True, stdin = PIPE) process.wait() @@ -84,10 +94,11 @@ def sdc_generate(network, first_index, count): def test_network(network): sdc_generate(network, first_index=0, count=1) sdc_generate(network, first_index=0, count=2) - sdc_generate(network, first_index=0, count=3) sdc_generate(network, first_index=12, count=1) sdc_generate(network, first_index=99, count=2) sdc_generate(network, first_index=1024, count=3) + sdc_generate(network, first_index=0, count=2, + eth1_withdrawal_address="0x0f51bb10119727a7e5ea3538074fb341f56b09ad") setup() From ed0f3544883780fc8056f47b401ca1a41de5721b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 16:21:59 +1000 Subject: [PATCH 023/138] Remove keystore json --- validator_manager/test_vectors/generate.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/validator_manager/test_vectors/generate.py b/validator_manager/test_vectors/generate.py index 45f73fb7b46..722414de733 100644 --- a/validator_manager/test_vectors/generate.py +++ b/validator_manager/test_vectors/generate.py @@ -1,13 +1,22 @@ +# This script uses the `ethereum/staking-deposit-cli` tool to generate +# deposit data files which are then used for testing by Lighthouse. +# +# To generate vectors, simply run this Python script: +# +# `python generate.py` +# import os import sys import shutil import subprocess from subprocess import Popen, PIPE, STDOUT + NUM_VALIDATORS=3 TEST_MNEMONIC = "test test test test test test test test test test test waste" WALLET_NAME="test_wallet" + tmp_dir = os.path.join(".", "tmp") mnemonic_path = os.path.join(tmp_dir, "mnemonic.txt") sdc_dir = os.path.join(tmp_dir, "sdc") @@ -34,6 +43,13 @@ def cleanup(): if os.path.exists(tmp_dir): shutil.rmtree(tmp_dir) + # Remove all the keystores since we don't use them in testing. + if os.path.exists(vectors_dir): + for root, dirs, files in os.walk(vectors_dir): + for file in files: + if file.startswith("keystore"): + os.remove(os.path.join(root, file)) + def setup_sdc(): result = subprocess.run([ @@ -58,6 +74,7 @@ def setup_sdc(): ], cwd=sdc_git_dir) assert(result.returncode == 0) + def sdc_generate(network, first_index, count, eth1_withdrawal_address=None): if eth1_withdrawal_address is not None: eth1_flags = ['--eth1_withdrawal_address', eth1_withdrawal_address] @@ -90,7 +107,6 @@ def sdc_generate(network, first_index, count, eth1_withdrawal_address=None): process.wait() - def test_network(network): sdc_generate(network, first_index=0, count=1) sdc_generate(network, first_index=0, count=2) From 516e4ee71ad7193472b6547ec1708fa2edaf192d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 16:22:37 +1000 Subject: [PATCH 024/138] Add test vectors --- .../validator_keys/deposit_data-1660803666.json | 1 + .../validator_keys/deposit_data-1660803669.json | 1 + .../validator_keys/deposit_data-1660803684.json | 1 + .../validator_keys/deposit_data-1660803679.json | 1 + .../validator_keys/deposit_data-1660803672.json | 1 + .../validator_keys/deposit_data-1660803675.json | 1 + .../validator_keys/deposit_data-1660803687.json | 1 + .../validator_keys/deposit_data-1660803690.json | 1 + .../validator_keys/deposit_data-1660803705.json | 1 + .../validator_keys/deposit_data-1660803701.json | 1 + .../validator_keys/deposit_data-1660803693.json | 1 + .../validator_keys/deposit_data-1660803696.json | 1 + 12 files changed, 12 insertions(+) create mode 100644 validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json create mode 100644 validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803669.json create mode 100644 validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803684.json create mode 100644 validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803679.json create mode 100644 validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803672.json create mode 100644 validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803675.json create mode 100644 validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json create mode 100644 validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json create mode 100644 validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json create mode 100644 validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json create mode 100644 validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json create mode 100644 validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json new file mode 100644 index 00000000000..31c00c57f24 --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803666.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "8ac88247c1b431a2d1eb2c5f00e7b8467bc21d6dc267f1af9ef727a12e32b4299e3b289ae5734a328b3202478dd746a80bf9e15a2217240dca1fc1b91a6b7ff7a0f5830d9a2610c1c30f19912346271357c21bd9af35a74097ebbdda2ddaf491", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "807a20b2801eabfd9065c1b74ed6ae3e991a1ab770e4eaf268f30b37cfd2cbd7", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803669.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803669.json new file mode 100644 index 00000000000..2880b7724cf --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803669.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "8ac88247c1b431a2d1eb2c5f00e7b8467bc21d6dc267f1af9ef727a12e32b4299e3b289ae5734a328b3202478dd746a80bf9e15a2217240dca1fc1b91a6b7ff7a0f5830d9a2610c1c30f19912346271357c21bd9af35a74097ebbdda2ddaf491", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "807a20b2801eabfd9065c1b74ed6ae3e991a1ab770e4eaf268f30b37cfd2cbd7", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "84b9fc8f260a1488c4c9a438f875edfa2bac964d651b2bc886d8442829b13f89752e807c8ca9bae9d50b1b506d3a64730015dd7f91e271ff9c1757d1996dcf6082fe5205cf6329fa2b6be303c21b66d75be608757a123da6ee4a4f14c01716d7", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "cd991ea8ff32e6b3940aed43b476c720fc1abd3040893b77a8a3efb306320d4c", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803684.json b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803684.json new file mode 100644 index 00000000000..da92a1d0d94 --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803684.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "a8461b58a5a5a0573c4af37da6ee4ba63e35894cffad6797d4a2c80f8f2c79d2c30c0de0299d8edde76e0c3f3e6d4f1e03cc377969f56d8760717d6e86f9316da9375573ce7bb87a8520daedb13c49284377f7a4f64a70aa2ca44b1581d47e20", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "d26d642a880ff8a109260fe69681840f6e1868c8c1cd2163a1db5a094e8db03a", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "93a398c09143203beb94c9223c7e18f36e5ea36090875284b222c2fcb16982e6f2e26f27ca9d30e3c6f6b5ad44857fc50f531925f4736810712f68a9d7a9c0eb664a851180f3b7d2e44a35717d43b3d3e4fd555354fa1dfa92f451870f36084d", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "7c7617a2c11870ec49e975b3691b9f822d63938df38555161e23aa245b150c66", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803679.json b/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803679.json new file mode 100644 index 00000000000..9cc01dc0df7 --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803679.json @@ -0,0 +1 @@ +[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "a0a96851892b257c032284928641021e58e0bcd277c3da5a2c41bcce6633d144781e4761261138277b5a8cf0ead59cce073e5a3bbc4704a37abf8cd1e290dc52e56cb0c334303945ebbb79be453c8177937e44e08f980679f1a2997fe58d2d86", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "2bedaf48f8315d8631defc97c1c4c05a8152e2dc3fe779fc8e800dd67bd839a2", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "b469179ad8ba9d6ad71b99a3c7ae662d9b77cca3ee53b20ab2eb20beee31874ad47224e94e75578fa6ecd30c1d40a0b300053817f934169d84425691edf13216445fbc6dd9b0953ad3af20c834fba63c1f50c0b0f92dd8bf383cd2cc8e0431f1", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "69862477671957ab0b3f1167c5cd550c107132a0079eb70eaa4bc5c5fe06b5a0", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "a8b05626657ce5b1801e0824aaeb21de2e1a11bc16cad6100ac911bcb873aaf7e7282f1f8465df4aaea998a1a4e1645f075e7e65f8c6b8688b0162f86be2128541f91fc9feb628bcab3b4afec1f7aeccaba04aaa54dc17c738233d360f94b97e", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "34ef32901d793cd9a0a3d93e7ee40e7be9abe6fb26f0b49a86b8ff29dc649930", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803672.json b/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803672.json new file mode 100644 index 00000000000..3a971d0959a --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803672.json @@ -0,0 +1 @@ +[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "a57299cde3c2ea8dc17ad3ce5a38a5f6de69d198599150dc4df02624ba1d8672440d02c0d27c3dc3b8c9f86c679571ab14c798426acd9b059895f1f5887bdee805fb4e31bd8f93ec9e78403c23d7924f23eae6af056154f35fee03bf9ffe0e98", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "246619823b45d80f53a30404542ec4be447d4e268cc0afcdf480e6a846d58411", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803675.json b/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803675.json new file mode 100644 index 00000000000..2efa5c4ec8c --- /dev/null +++ b/validator_manager/test_vectors/vectors/mainnet_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803675.json @@ -0,0 +1 @@ +[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "8ca8a6f30b4346d7b9912e3dcd820652bc472511f89d91fd102acfb0c8df1cfc7a2629f44170727e126e88f2847fe5c9081b13fb0838a2b2343a95cabf16f57708fc0cf846bc5307209ae976c34500cc826ff48ab64169d8bebec99dded5dd1d", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "c0c6cd40b43ea0fe7fcc284de9acd9c1bd001bb88c059c155393af22a6c85d46", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "8c0784645c611b4f514a6519b737f2d02df3eba0e04cd30efebffcca769af8cc599ce28e4421cefe665ec31d3c34e44c174e0cca4891d8196796085e712459b45e411efecd07cf3258f1d6309a07a6dd52a0ae186e6184d37bf11cee36ec84e8", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "c57790b77ef97318d4ec7b97ea07ea458d08209ba372bfe76171e2ece22d6130", "fork_version": "00000000", "network_name": "mainnet", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json b/validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json new file mode 100644 index 00000000000..c736d75b7e9 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_0_count_1_eth1_false/validator_keys/deposit_data-1660803687.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "a940e0142ad9b56a1310326137347d1ada275b31b3748af4accc63bd189573376615be8e8ae047766c6d10864e54b2e7098177598edf3a043eb560bbdf1a1c12588375a054d1323a0900e2286d0993cde9675e5b74523e6e8e03715cc96b3ce5", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "28484efb20c961a1354689a556d4c352fe9deb24684efdb32d22e1af17e2a45d", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json new file mode 100644 index 00000000000..e86500d14f2 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_false/validator_keys/deposit_data-1660803690.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0049b6188ed20314309f617dd4030b8ddfac3c6e65759a03c226a13b2fe4cc72", "amount": 32000000000, "signature": "a940e0142ad9b56a1310326137347d1ada275b31b3748af4accc63bd189573376615be8e8ae047766c6d10864e54b2e7098177598edf3a043eb560bbdf1a1c12588375a054d1323a0900e2286d0993cde9675e5b74523e6e8e03715cc96b3ce5", "deposit_message_root": "a9bc1d21cc009d9b10782a07213e37592c0d235463ed0117dec755758da90d51", "deposit_data_root": "28484efb20c961a1354689a556d4c352fe9deb24684efdb32d22e1af17e2a45d", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "00ad3748cbd1adc855c2bdab431f7e755a21663f4f6447ac888e5855c588af5a", "amount": 32000000000, "signature": "87b4b4e9c923aa9e1687219e9df0e838956ee6e15b7ab18142467430d00940dc7aa243c9996e85125dfe72d9dbdb00a30a36e16a2003ee0c86f29c9f5d74f12bfe5b7f62693dbf5187a093555ae8d6b48acd075788549c4b6a249b397af24cd0", "deposit_message_root": "c5271aba974c802ff5b02b11fa33b545d7f430ff3b85c0f9eeef4cd59d83abf3", "deposit_data_root": "ea80b639356a03f6f58e4acbe881fabefc9d8b93375a6aa7e530c77d7e45d3e4", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json new file mode 100644 index 00000000000..c79ae5a4fc0 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_0_count_2_eth1_true/validator_keys/deposit_data-1660803705.json @@ -0,0 +1 @@ +[{"pubkey": "88b6b3a9b391fa5593e8bce8d06102df1a56248368086929709fbb4a8570dc6a560febeef8159b19789e9c1fd13572f0", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "ab32595d8201c2b4e8173aece9151fdc15f4d2ad36008462d0416598ddbf0f37ed0877f06d284a9669e73dbc0885bd2207fe64385e95a4488dc2bcb2c324d5c20da3248a6244463583dfbba8db20805765421e59cb56b0bc3ee6d24a9218216d", "deposit_message_root": "62967565d11471da4af7769911926cd1826124048036b25616216f99bc320f13", "deposit_data_root": "b4df3a3a26dd5f6eb32999d8a7051a7d1a8573a16553d4b45ee706a0d59c1066", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "a33ab9d93fb53c4f027944aaa11a13be0c150b7cc2e379d85d1ed4db38d178b4e4ebeae05832158b8c746c1961da00ce", "withdrawal_credentials": "0100000000000000000000000f51bb10119727a7e5ea3538074fb341f56b09ad", "amount": 32000000000, "signature": "9655e195eda5517efe6f36bcebd45250c889a4177d7bf5fcd59598d2d03f37f038b5ee2ec079a30a8382ea42f351943f08a6f006bab9c2130db2742bd7315c8ad5aa1f03a0801c26d4c9efdef71c4c59c449c7f9b21fa62600ab8f5f1e2b938a", "deposit_message_root": "ce110433298ffb78d827d67dcc13655344a139cb7e3ce10b341937c0a76b25b7", "deposit_data_root": "7661474fba11bfb453274f62df022cab3c0b6f4a58af4400f6bce83c9cb5fcb8", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json b/validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json new file mode 100644 index 00000000000..136dc38554c --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_1024_count_3_eth1_false/validator_keys/deposit_data-1660803701.json @@ -0,0 +1 @@ +[{"pubkey": "92ca8dddba4ae7ada6584c377fc53fb978ad9d5ee8db585b18e226c27682b326b3c68e10f5d99a453e233268c144e0ef", "withdrawal_credentials": "00dd4f8bfd1a48be288c2af8bb7315f6198900b5b3f56df010420d5328e682cb", "amount": 32000000000, "signature": "b5dae79ce8f3d7326b46f93182981c5f3d64257a457f038caa78ec8e5cc25a9fdac52c7beb221ab2a3205404131366ad18e1e13801393b3d486819e8cca96128bf1244884a91d05dced092c74bc1e7259788f30dd3432df15f3d2f629645f345", "deposit_message_root": "5421d9177b4d035e6525506509ab702c5f458c53458dad437097b37cb8209b43", "deposit_data_root": "94213d76aba9e6a434589d4939dd3764e0832df78f66d30db22a760c14ba1b89", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "86474cd2874663445ef0ee02aca81b2b942a383fd4c7085fa675388e26c67afc0fef44a8666d46f571723e349ae4a0cb", "withdrawal_credentials": "001c31aa161ed1d3c481c1ee8f3ad1853217296a15877917fe3c2f680580ac01", "amount": 32000000000, "signature": "816f38a321c4f84ad5187eda58f6d9c1fd1e81c860ed1722bdb76b920fdd430a1e814b9bb893837ae3b38ad738684fbf1795fa687f617c52121472b1ac8d2e34e5c1127186233a8833ffb54c509d9e52cb7242c6c6a65b5e496296b3caa90d89", "deposit_message_root": "279271f7065c83868c37021c32c014516b21e6188fb2cee4e8543c5d38427698", "deposit_data_root": "7ad1d059d69794680a1deef5e72c33827f0c449a5f0917095821c0343572789d", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "997e27aa262238beb01464434694a466321b5270297bdfdb944b65a3b6617b6ce2613628ac35a8f4cf2e9b4b55c46ef8", "withdrawal_credentials": "0097fffee9cf9fd91a6fa89af90e73f1cb8b8a043e742afaeb2e57b83b0845fe", "amount": 32000000000, "signature": "95d20c35484dea6b2a0bd7c2da2d2e810d7829e14c03657b2524adfc2111aa5ed95908ecb975ff75ff742c68ce8df417016c048959b0f807675430f6d981478e26d48e594e0830a0406da9817f8a1ecb94bd8be1f9281eeb5e952a82173c72bb", "deposit_message_root": "187e177721bfdd8ea13cb52c8de2dead29164a0e093efb640457a0e6ac918191", "deposit_data_root": "83abfb2a166f7af708526a9bdd2767c4be3cd231c9bc4e2f047a80df88a2860c", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json b/validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json new file mode 100644 index 00000000000..ccd2ece0699 --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_12_count_1_eth1_false/validator_keys/deposit_data-1660803693.json @@ -0,0 +1 @@ +[{"pubkey": "8b181759a027c09a409ef24f6b35db213982c2474e2017f3851d76b1c4e560a4238072f67a0c22cb667f940da4ea9ec9", "withdrawal_credentials": "00cbec90e8570679f565bd4645f73a078981067a705564283e61c93c81707842", "amount": 32000000000, "signature": "8f75836ceb390dd4fc8c16bc4be52ca09b9c5aa0ab5bc16dcfdb344787b29ddfd76d877b0a2330bc8e904b233397c6bd124845d1b868e4951cb6daacea023c986bdf0c6ac28d73f65681d941ea96623bc23acc7c84dcfc1304686240d9171cfc", "deposit_message_root": "fcdf3d94740766299a95b3e477e64abadff6ab8978400578f241c93eb367b938", "deposit_data_root": "3011f5cac32f13e86ecc061e89ed6675c27a46ab6ecb1ec6f6e5f133ae1d0287", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file diff --git a/validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json b/validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json new file mode 100644 index 00000000000..2ab5908307b --- /dev/null +++ b/validator_manager/test_vectors/vectors/prater_first_99_count_2_eth1_false/validator_keys/deposit_data-1660803696.json @@ -0,0 +1 @@ +[{"pubkey": "a57a4ed429e415b862cc758e75c93936e3f6339640d0763b969ba133a82c03717827fbdd8ec42fc862ed50e3b5b528dc", "withdrawal_credentials": "00864081ef2f5aec1aa667872615e25027f1fdc256a4948b6318cf75a8d635a3", "amount": 32000000000, "signature": "a7706e102bfb0b986a5c8050044f7e221919463149771a92c3ca46ff7d4564867db48eaf89b5237fed8db2cdb9c9c057099d0982bbdb3fbfcbe0ab7259ad3f31f7713692b78ee25e6251982e7081d049804632b70b8a24d8c3e59b624a0bd221", "deposit_message_root": "c08d0ecd085bc0f50c35f1b34d8b8937b2b9c8a172a9808de70f8d448c526f07", "deposit_data_root": "8a26fbee0c3a99fe090af1fce68afc525b4e7efa70df72abaa91f29148b2f672", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}, {"pubkey": "a2801622bc391724989004b5de78cb85746f85a303572691ecc945d9f5c61ec512127e58482e0dfcb4de77be3294ab01", "withdrawal_credentials": "00edff674c66a7f58285554e700183aeee5e740691de8087f7ce4d81f3597108", "amount": 32000000000, "signature": "8b7aa5b0e97d15ec8c2281b919fde9e064f6ac064b163445ea99441ab063f9d10534bfde861b5606021ae46614ff075e0c2305ce5a6cbcc9f0bc8e7df1a177c4d969a5ed4ac062b0ea959bdac963fe206b73565a1a3937adcca736c6117c15f0", "deposit_message_root": "f5a530bee9698c2447961ecd210184fbb130bbb8e8916988d802d47e3b147842", "deposit_data_root": "d38575167a94b516455c5b7e36d24310a612fa0f4580446c5f9d45e4e94f0642", "fork_version": "00001020", "network_name": "goerli", "deposit_cli_version": "2.3.0"}] \ No newline at end of file From 2873cb858c29238787aa290dc99255762d5b474c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 16:26:55 +1000 Subject: [PATCH 025/138] Tidy directory structure --- validator_manager/src/validators/{common/mod.rs => common.rs} | 0 validator_manager/src/validators/create_validators.rs | 3 +++ 2 files changed, 3 insertions(+) rename validator_manager/src/validators/{common/mod.rs => common.rs} (100%) diff --git a/validator_manager/src/validators/common/mod.rs b/validator_manager/src/validators/common.rs similarity index 100% rename from validator_manager/src/validators/common/mod.rs rename to validator_manager/src/validators/common.rs diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 1ef870ac5e0..2a7b050d143 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -503,6 +503,9 @@ async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<( Ok(()) } +/// Write some object to a file as JSON. +/// +/// The file must be created new, it must not already exist. fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Result<(), String> { eprintln!("Writing {:?}", path.as_ref()); let mut file = fs::OpenOptions::new() From 5e31754caa3ded8fe00423c6caf52082697b4918 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 16:54:44 +1000 Subject: [PATCH 026/138] Expose VC `ApiTester` via `test_utils` --- validator_client/Cargo.toml | 2 +- validator_client/src/http_api/mod.rs | 2 + validator_client/src/http_api/test_utils.rs | 598 ++++++++++++++++++++ validator_client/src/http_api/tests.rs | 597 +------------------ 4 files changed, 606 insertions(+), 593 deletions(-) create mode 100644 validator_client/src/http_api/test_utils.rs diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 8a3c8303a95..edfc330ce95 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -10,7 +10,6 @@ path = "src/lib.rs" [dev-dependencies] tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } -logging = { path = "../common/logging" } [dependencies] tree_hash = "0.4.1" @@ -59,3 +58,4 @@ task_executor = { path = "../common/task_executor" } reqwest = { version = "0.11.0", features = ["json","stream"] } url = "2.2.2" malloc_utils = { path = "../common/malloc_utils" } +logging = { path = "../common/logging" } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index e9c7bf69d45..4bb8f33a06c 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -4,6 +4,8 @@ mod keystores; mod remotekeys; mod tests; +pub mod test_utils; + use crate::ValidatorStore; use account_utils::{ mnemonic_from_phrase, diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs new file mode 100644 index 00000000000..52d86b6531f --- /dev/null +++ b/validator_client/src/http_api/test_utils.rs @@ -0,0 +1,598 @@ +use crate::doppelganger_service::DoppelgangerService; +use crate::{ + http_api::{ApiSecret, Config as HttpConfig, Context}, + initialized_validators::InitializedValidators, + Config, ValidatorDefinitions, ValidatorStore, +}; +use account_utils::{ + eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, + ZeroizeString, +}; +use deposit_contract::decode_eth1_tx_data; +use eth2::{ + lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, + types::ErrorMessage as ApiErrorMessage, + Error as ApiError, +}; +use eth2_keystore::KeystoreBuilder; +use logging::test_logger; +use parking_lot::RwLock; +use sensitive_url::SensitiveUrl; +use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; +use slot_clock::{SlotClock, TestingSlotClock}; +use std::future::Future; +use std::marker::PhantomData; +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::Arc; +use std::time::Duration; +use task_executor::TaskExecutor; +use tempfile::{tempdir, TempDir}; +use tokio::runtime::Runtime; +use tokio::sync::oneshot; + +pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; +pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); + +type E = MainnetEthSpec; + +pub struct HdValidatorScenario { + count: usize, + specify_mnemonic: bool, + key_derivation_path_offset: u32, + disabled: Vec, +} + +pub struct KeystoreValidatorScenario { + enabled: bool, + correct_password: bool, +} + +pub struct Web3SignerValidatorScenario { + count: usize, + enabled: bool, +} + +pub struct ApiTester { + client: ValidatorClientHttpClient, + initialized_validators: Arc>, + validator_store: Arc>, + url: SensitiveUrl, + _server_shutdown: oneshot::Sender<()>, + _validator_dir: TempDir, + _runtime_shutdown: exit_future::Signal, +} + +// Builds a runtime to be used in the testing configuration. +pub fn build_runtime() -> Arc { + Arc::new( + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Should be able to build a testing runtime"), + ) +} + +impl ApiTester { + pub async fn new(runtime: std::sync::Weak) -> Self { + let log = test_logger(); + + let validator_dir = tempdir().unwrap(); + let secrets_dir = tempdir().unwrap(); + + let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); + + let initialized_validators = InitializedValidators::from_definitions( + validator_defs, + validator_dir.path().into(), + log.clone(), + ) + .await + .unwrap(); + + let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); + let api_pubkey = api_secret.api_token(); + + let mut config = Config::default(); + config.validator_dir = validator_dir.path().into(); + config.secrets_dir = secrets_dir.path().into(); + config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT); + + let spec = E::default_spec(); + + let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); + let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); + + let slot_clock = + TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + + let (runtime_shutdown, exit) = exit_future::signal(); + let (shutdown_tx, _) = futures::channel::mpsc::channel(1); + let executor = TaskExecutor::new(runtime.clone(), exit, log.clone(), shutdown_tx); + + let validator_store = Arc::new(ValidatorStore::<_, E>::new( + initialized_validators, + slashing_protection, + Hash256::repeat_byte(42), + spec, + Some(Arc::new(DoppelgangerService::new(log.clone()))), + slot_clock, + &config, + executor.clone(), + log.clone(), + )); + + validator_store + .register_all_in_doppelganger_protection_if_enabled() + .expect("Should attach doppelganger service"); + + let initialized_validators = validator_store.initialized_validators(); + + let context = Arc::new(Context { + task_executor: executor, + api_secret, + validator_dir: Some(validator_dir.path().into()), + validator_store: Some(validator_store.clone()), + spec: E::default_spec(), + config: HttpConfig { + enabled: true, + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + listen_port: 0, + allow_origin: None, + }, + log, + _phantom: PhantomData, + }); + let ctx = context.clone(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_shutdown = async { + // It's not really interesting why this triggered, just that it happened. + let _ = shutdown_rx.await; + }; + let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); + + tokio::spawn(async { server.await }); + + let url = SensitiveUrl::parse(&format!( + "http://{}:{}", + listening_socket.ip(), + listening_socket.port() + )) + .unwrap(); + + let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey).unwrap(); + + Self { + client, + initialized_validators, + validator_store, + url, + _server_shutdown: shutdown_tx, + _validator_dir: validator_dir, + _runtime_shutdown: runtime_shutdown, + } + } + + pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { + let tmp = tempdir().unwrap(); + let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); + let invalid_pubkey = api_secret.api_token(); + ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey.clone()).unwrap() + } + + pub async fn test_with_invalid_auth(self, func: F) -> Self + where + F: Fn(ValidatorClientHttpClient) -> A, + A: Future>, + { + /* + * Test with an invalid Authorization header. + */ + match func(self.invalid_token_client()).await { + Err(ApiError::ServerMessage(ApiErrorMessage { code: 403, .. })) => (), + Err(other) => panic!("expected authorized error, got {:?}", other), + Ok(_) => panic!("expected authorized error, got Ok"), + } + + /* + * Test with a missing Authorization header. + */ + let mut missing_token_client = self.client.clone(); + missing_token_client.send_authorization_header(false); + match func(missing_token_client).await { + Err(ApiError::ServerMessage(ApiErrorMessage { + code: 401, message, .. + })) if message.contains("missing Authorization header") => (), + Err(other) => panic!("expected missing header error, got {:?}", other), + Ok(_) => panic!("expected missing header error, got Ok"), + } + + self + } + + pub fn invalidate_api_token(mut self) -> Self { + self.client = self.invalid_token_client(); + self + } + + pub async fn test_get_lighthouse_version_invalid(self) -> Self { + self.client.get_lighthouse_version().await.unwrap_err(); + self + } + + pub async fn test_get_lighthouse_spec(self) -> Self { + let result = self + .client + .get_lighthouse_spec::() + .await + .map(|res| ConfigAndPreset::Bellatrix(res.data)) + .unwrap(); + let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); + + assert_eq!(result, expected); + + self + } + + pub async fn test_get_lighthouse_version(self) -> Self { + let result = self.client.get_lighthouse_version().await.unwrap().data; + + let expected = VersionData { + version: lighthouse_version::version_with_platform(), + }; + + assert_eq!(result, expected); + + self + } + + #[cfg(target_os = "linux")] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap(); + + self + } + + #[cfg(not(target_os = "linux"))] + pub async fn test_get_lighthouse_health(self) -> Self { + self.client.get_lighthouse_health().await.unwrap_err(); + + self + } + pub fn vals_total(&self) -> usize { + self.initialized_validators.read().num_total() + } + + pub fn vals_enabled(&self) -> usize { + self.initialized_validators.read().num_enabled() + } + + pub fn assert_enabled_validators_count(self, count: usize) -> Self { + assert_eq!(self.vals_enabled(), count); + self + } + + pub fn assert_validators_count(self, count: usize) -> Self { + assert_eq!(self.vals_total(), count); + self + } + + pub async fn create_hd_validators(self, s: HdValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let validators = (0..s.count) + .map(|i| ValidatorRequest { + enable: !s.disabled.contains(&i), + description: format!("boi #{}", i), + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + deposit_gwei: E::default_spec().max_effective_balance, + }) + .collect::>(); + + let (response, mnemonic) = if s.specify_mnemonic { + let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); + let request = CreateValidatorsMnemonicRequest { + mnemonic: mnemonic.clone(), + key_derivation_path_offset: s.key_derivation_path_offset, + validators: validators.clone(), + }; + let response = self + .client + .post_lighthouse_validators_mnemonic(&request) + .await + .unwrap() + .data; + + (response, mnemonic) + } else { + assert_eq!( + s.key_derivation_path_offset, 0, + "cannot use a derivation offset without specifying a mnemonic" + ); + let response = self + .client + .post_lighthouse_validators(validators.clone()) + .await + .unwrap() + .data; + (response.validators.clone(), response.mnemonic.clone()) + }; + + assert_eq!(response.len(), s.count); + assert_eq!(self.vals_total(), initial_vals + s.count); + assert_eq!( + self.vals_enabled(), + initial_enabled_vals + s.count - s.disabled.len() + ); + + let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; + + assert_eq!(server_vals.len(), self.vals_total()); + + // Ensure the server lists all of these newly created validators. + for validator in &response { + assert!(server_vals + .iter() + .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey)); + } + + /* + * Verify that we can regenerate all the keys from the mnemonic. + */ + + let mnemonic = mnemonic_from_phrase(mnemonic.as_str()).unwrap(); + let mut wallet = WalletBuilder::from_mnemonic(&mnemonic, PASSWORD_BYTES, "".to_string()) + .unwrap() + .build() + .unwrap(); + + wallet + .set_nextaccount(s.key_derivation_path_offset) + .unwrap(); + + for i in 0..s.count { + let keypairs = wallet + .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) + .unwrap(); + let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); + + assert_eq!( + response[i].voting_pubkey, + voting_keypair.pk.clone().into(), + "the locally generated voting pk should match the server response" + ); + + let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); + + let deposit_bytes = + eth2_serde_utils::hex::decode(&response[i].eth1_deposit_tx_data).unwrap(); + + let (deposit_data, _) = + decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) + .unwrap(); + + assert_eq!( + deposit_data.pubkey, + voting_keypair.pk.clone().into(), + "the locally generated voting pk should match the deposit data" + ); + + assert_eq!( + deposit_data.withdrawal_credentials, + Hash256::from_slice(&bls::get_withdrawal_credentials( + &withdrawal_keypair.pk, + E::default_spec().bls_withdrawal_prefix_byte + )), + "the locally generated withdrawal creds should match the deposit data" + ); + + assert_eq!( + deposit_data.signature, + deposit_data.create_signature(&voting_keypair.sk, &E::default_spec()), + "the locally-generated deposit sig should create the same deposit sig" + ); + } + + self + } + + pub async fn create_keystore_validators(self, s: KeystoreValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let password = random_password(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) + .unwrap() + .build() + .unwrap(); + + if !s.correct_password { + let request = KeystoreValidatorsPostRequest { + enable: s.enabled, + password: String::from_utf8(random_password().as_ref().to_vec()) + .unwrap() + .into(), + keystore, + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + }; + + self.client + .post_lighthouse_validators_keystore(&request) + .await + .unwrap_err(); + + return self; + } + + let request = KeystoreValidatorsPostRequest { + enable: s.enabled, + password: String::from_utf8(password.as_ref().to_vec()) + .unwrap() + .into(), + keystore, + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + }; + + let response = self + .client + .post_lighthouse_validators_keystore(&request) + .await + .unwrap() + .data; + + let num_enabled = s.enabled as usize; + + assert_eq!(self.vals_total(), initial_vals + 1); + assert_eq!(self.vals_enabled(), initial_enabled_vals + num_enabled); + + let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; + + assert_eq!(server_vals.len(), self.vals_total()); + + assert_eq!(response.voting_pubkey, keypair.pk.into()); + assert_eq!(response.enabled, s.enabled); + + self + } + + pub async fn create_web3signer_validators(self, s: Web3SignerValidatorScenario) -> Self { + let initial_vals = self.vals_total(); + let initial_enabled_vals = self.vals_enabled(); + + let request: Vec<_> = (0..s.count) + .map(|i| { + let kp = Keypair::random(); + Web3SignerValidatorRequest { + enable: s.enabled, + description: format!("{}", i), + graffiti: None, + suggested_fee_recipient: None, + gas_limit: None, + builder_proposals: None, + voting_public_key: kp.pk, + url: format!("http://signer_{}.com/", i), + root_certificate_path: None, + request_timeout_ms: None, + client_identity_path: None, + client_identity_password: None, + } + }) + .collect(); + + self.client + .post_lighthouse_validators_web3signer(&request) + .await + .unwrap(); + + assert_eq!(self.vals_total(), initial_vals + s.count); + if s.enabled { + assert_eq!(self.vals_enabled(), initial_enabled_vals + s.count); + } else { + assert_eq!(self.vals_enabled(), initial_enabled_vals); + }; + + self + } + + pub async fn set_validator_enabled(self, index: usize, enabled: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) + .await + .unwrap(); + + assert_eq!( + self.initialized_validators + .read() + .is_enabled(&validator.voting_pubkey.decompress().unwrap()) + .unwrap(), + enabled + ); + + assert!(self + .client + .get_lighthouse_validators() + .await + .unwrap() + .data + .into_iter() + .find(|v| v.voting_pubkey == validator.voting_pubkey) + .map(|v| v.enabled == enabled) + .unwrap()); + + // Check the server via an individual request. + assert_eq!( + self.client + .get_lighthouse_validators_pubkey(&validator.voting_pubkey) + .await + .unwrap() + .unwrap() + .data + .enabled, + enabled + ); + + self + } + + pub async fn set_gas_limit(self, index: usize, gas_limit: u64) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) + .await + .unwrap(); + + self + } + + pub async fn assert_gas_limit(self, index: usize, gas_limit: u64) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + assert_eq!( + self.validator_store.get_gas_limit(&validator.voting_pubkey), + gas_limit + ); + + self + } + + pub async fn set_builder_proposals(self, index: usize, builder_proposals: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + self.client + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + None, + Some(builder_proposals), + ) + .await + .unwrap(); + + self + } + + pub async fn assert_builder_proposals(self, index: usize, builder_proposals: bool) -> Self { + let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; + + assert_eq!( + self.validator_store + .get_builder_proposals(&validator.voting_pubkey), + builder_proposals + ); + + self + } +} diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index b121dda5b1a..2fa46622265 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -3,605 +3,18 @@ mod keystores; -use crate::doppelganger_service::DoppelgangerService; -use crate::{ - http_api::{ApiSecret, Config as HttpConfig, Context}, - initialized_validators::InitializedValidators, - Config, ValidatorDefinitions, ValidatorStore, -}; -use account_utils::{ - eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, - random_password_string, ZeroizeString, -}; -use deposit_contract::decode_eth1_tx_data; -use eth2::{ - lighthouse_vc::{http_client::ValidatorClientHttpClient, types::*}, - types::ErrorMessage as ApiErrorMessage, - Error as ApiError, +use crate::http_api::test_utils::{ + build_runtime, ApiTester, HdValidatorScenario, KeystoreValidatorScenario, + Web3SignerValidatorScenario, TEST_DEFAULT_FEE_RECIPIENT, }; +use account_utils::{random_password, random_password_string, ZeroizeString}; +use eth2::lighthouse_vc::types::*; use eth2_keystore::KeystoreBuilder; -use logging::test_logger; -use parking_lot::RwLock; -use sensitive_url::SensitiveUrl; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; -use slot_clock::{SlotClock, TestingSlotClock}; use std::future::Future; -use std::marker::PhantomData; -use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; -use std::time::Duration; -use task_executor::TaskExecutor; -use tempfile::{tempdir, TempDir}; -use tokio::runtime::Runtime; -use tokio::sync::oneshot; - -const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; -pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); type E = MainnetEthSpec; -struct ApiTester { - client: ValidatorClientHttpClient, - initialized_validators: Arc>, - validator_store: Arc>, - url: SensitiveUrl, - _server_shutdown: oneshot::Sender<()>, - _validator_dir: TempDir, - _runtime_shutdown: exit_future::Signal, -} - -// Builds a runtime to be used in the testing configuration. -fn build_runtime() -> Arc { - Arc::new( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("Should be able to build a testing runtime"), - ) -} - -impl ApiTester { - pub async fn new(runtime: std::sync::Weak) -> Self { - let log = test_logger(); - - let validator_dir = tempdir().unwrap(); - let secrets_dir = tempdir().unwrap(); - - let validator_defs = ValidatorDefinitions::open_or_create(validator_dir.path()).unwrap(); - - let initialized_validators = InitializedValidators::from_definitions( - validator_defs, - validator_dir.path().into(), - log.clone(), - ) - .await - .unwrap(); - - let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); - let api_pubkey = api_secret.api_token(); - - let mut config = Config::default(); - config.validator_dir = validator_dir.path().into(); - config.secrets_dir = secrets_dir.path().into(); - config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT); - - let spec = E::default_spec(); - - let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); - let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); - - let slot_clock = - TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - - let (runtime_shutdown, exit) = exit_future::signal(); - let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = TaskExecutor::new(runtime.clone(), exit, log.clone(), shutdown_tx); - - let validator_store = Arc::new(ValidatorStore::<_, E>::new( - initialized_validators, - slashing_protection, - Hash256::repeat_byte(42), - spec, - Some(Arc::new(DoppelgangerService::new(log.clone()))), - slot_clock, - &config, - executor.clone(), - log.clone(), - )); - - validator_store - .register_all_in_doppelganger_protection_if_enabled() - .expect("Should attach doppelganger service"); - - let initialized_validators = validator_store.initialized_validators(); - - let context = Arc::new(Context { - task_executor: executor, - api_secret, - validator_dir: Some(validator_dir.path().into()), - validator_store: Some(validator_store.clone()), - spec: E::default_spec(), - config: HttpConfig { - enabled: true, - listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - listen_port: 0, - allow_origin: None, - }, - log, - _phantom: PhantomData, - }); - let ctx = context.clone(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_shutdown = async { - // It's not really interesting why this triggered, just that it happened. - let _ = shutdown_rx.await; - }; - let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); - - tokio::spawn(async { server.await }); - - let url = SensitiveUrl::parse(&format!( - "http://{}:{}", - listening_socket.ip(), - listening_socket.port() - )) - .unwrap(); - - let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey).unwrap(); - - Self { - client, - initialized_validators, - validator_store, - url, - _server_shutdown: shutdown_tx, - _validator_dir: validator_dir, - _runtime_shutdown: runtime_shutdown, - } - } - - pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { - let tmp = tempdir().unwrap(); - let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); - let invalid_pubkey = api_secret.api_token(); - ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey.clone()).unwrap() - } - - pub async fn test_with_invalid_auth(self, func: F) -> Self - where - F: Fn(ValidatorClientHttpClient) -> A, - A: Future>, - { - /* - * Test with an invalid Authorization header. - */ - match func(self.invalid_token_client()).await { - Err(ApiError::ServerMessage(ApiErrorMessage { code: 403, .. })) => (), - Err(other) => panic!("expected authorized error, got {:?}", other), - Ok(_) => panic!("expected authorized error, got Ok"), - } - - /* - * Test with a missing Authorization header. - */ - let mut missing_token_client = self.client.clone(); - missing_token_client.send_authorization_header(false); - match func(missing_token_client).await { - Err(ApiError::ServerMessage(ApiErrorMessage { - code: 401, message, .. - })) if message.contains("missing Authorization header") => (), - Err(other) => panic!("expected missing header error, got {:?}", other), - Ok(_) => panic!("expected missing header error, got Ok"), - } - - self - } - - pub fn invalidate_api_token(mut self) -> Self { - self.client = self.invalid_token_client(); - self - } - - pub async fn test_get_lighthouse_version_invalid(self) -> Self { - self.client.get_lighthouse_version().await.unwrap_err(); - self - } - - pub async fn test_get_lighthouse_spec(self) -> Self { - let result = self - .client - .get_lighthouse_spec::() - .await - .map(|res| ConfigAndPreset::Bellatrix(res.data)) - .unwrap(); - let expected = ConfigAndPreset::from_chain_spec::(&E::default_spec(), None); - - assert_eq!(result, expected); - - self - } - - pub async fn test_get_lighthouse_version(self) -> Self { - let result = self.client.get_lighthouse_version().await.unwrap().data; - - let expected = VersionData { - version: lighthouse_version::version_with_platform(), - }; - - assert_eq!(result, expected); - - self - } - - #[cfg(target_os = "linux")] - pub async fn test_get_lighthouse_health(self) -> Self { - self.client.get_lighthouse_health().await.unwrap(); - - self - } - - #[cfg(not(target_os = "linux"))] - pub async fn test_get_lighthouse_health(self) -> Self { - self.client.get_lighthouse_health().await.unwrap_err(); - - self - } - pub fn vals_total(&self) -> usize { - self.initialized_validators.read().num_total() - } - - pub fn vals_enabled(&self) -> usize { - self.initialized_validators.read().num_enabled() - } - - pub fn assert_enabled_validators_count(self, count: usize) -> Self { - assert_eq!(self.vals_enabled(), count); - self - } - - pub fn assert_validators_count(self, count: usize) -> Self { - assert_eq!(self.vals_total(), count); - self - } - - pub async fn create_hd_validators(self, s: HdValidatorScenario) -> Self { - let initial_vals = self.vals_total(); - let initial_enabled_vals = self.vals_enabled(); - - let validators = (0..s.count) - .map(|i| ValidatorRequest { - enable: !s.disabled.contains(&i), - description: format!("boi #{}", i), - graffiti: None, - suggested_fee_recipient: None, - gas_limit: None, - builder_proposals: None, - deposit_gwei: E::default_spec().max_effective_balance, - }) - .collect::>(); - - let (response, mnemonic) = if s.specify_mnemonic { - let mnemonic = ZeroizeString::from(random_mnemonic().phrase().to_string()); - let request = CreateValidatorsMnemonicRequest { - mnemonic: mnemonic.clone(), - key_derivation_path_offset: s.key_derivation_path_offset, - validators: validators.clone(), - }; - let response = self - .client - .post_lighthouse_validators_mnemonic(&request) - .await - .unwrap() - .data; - - (response, mnemonic) - } else { - assert_eq!( - s.key_derivation_path_offset, 0, - "cannot use a derivation offset without specifying a mnemonic" - ); - let response = self - .client - .post_lighthouse_validators(validators.clone()) - .await - .unwrap() - .data; - (response.validators.clone(), response.mnemonic.clone()) - }; - - assert_eq!(response.len(), s.count); - assert_eq!(self.vals_total(), initial_vals + s.count); - assert_eq!( - self.vals_enabled(), - initial_enabled_vals + s.count - s.disabled.len() - ); - - let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; - - assert_eq!(server_vals.len(), self.vals_total()); - - // Ensure the server lists all of these newly created validators. - for validator in &response { - assert!(server_vals - .iter() - .any(|server_val| server_val.voting_pubkey == validator.voting_pubkey)); - } - - /* - * Verify that we can regenerate all the keys from the mnemonic. - */ - - let mnemonic = mnemonic_from_phrase(mnemonic.as_str()).unwrap(); - let mut wallet = WalletBuilder::from_mnemonic(&mnemonic, PASSWORD_BYTES, "".to_string()) - .unwrap() - .build() - .unwrap(); - - wallet - .set_nextaccount(s.key_derivation_path_offset) - .unwrap(); - - for i in 0..s.count { - let keypairs = wallet - .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) - .unwrap(); - let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); - - assert_eq!( - response[i].voting_pubkey, - voting_keypair.pk.clone().into(), - "the locally generated voting pk should match the server response" - ); - - let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); - - let deposit_bytes = - eth2_serde_utils::hex::decode(&response[i].eth1_deposit_tx_data).unwrap(); - - let (deposit_data, _) = - decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) - .unwrap(); - - assert_eq!( - deposit_data.pubkey, - voting_keypair.pk.clone().into(), - "the locally generated voting pk should match the deposit data" - ); - - assert_eq!( - deposit_data.withdrawal_credentials, - Hash256::from_slice(&bls::get_withdrawal_credentials( - &withdrawal_keypair.pk, - E::default_spec().bls_withdrawal_prefix_byte - )), - "the locally generated withdrawal creds should match the deposit data" - ); - - assert_eq!( - deposit_data.signature, - deposit_data.create_signature(&voting_keypair.sk, &E::default_spec()), - "the locally-generated deposit sig should create the same deposit sig" - ); - } - - self - } - - pub async fn create_keystore_validators(self, s: KeystoreValidatorScenario) -> Self { - let initial_vals = self.vals_total(); - let initial_enabled_vals = self.vals_enabled(); - - let password = random_password(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) - .unwrap() - .build() - .unwrap(); - - if !s.correct_password { - let request = KeystoreValidatorsPostRequest { - enable: s.enabled, - password: String::from_utf8(random_password().as_ref().to_vec()) - .unwrap() - .into(), - keystore, - graffiti: None, - suggested_fee_recipient: None, - gas_limit: None, - builder_proposals: None, - }; - - self.client - .post_lighthouse_validators_keystore(&request) - .await - .unwrap_err(); - - return self; - } - - let request = KeystoreValidatorsPostRequest { - enable: s.enabled, - password: String::from_utf8(password.as_ref().to_vec()) - .unwrap() - .into(), - keystore, - graffiti: None, - suggested_fee_recipient: None, - gas_limit: None, - builder_proposals: None, - }; - - let response = self - .client - .post_lighthouse_validators_keystore(&request) - .await - .unwrap() - .data; - - let num_enabled = s.enabled as usize; - - assert_eq!(self.vals_total(), initial_vals + 1); - assert_eq!(self.vals_enabled(), initial_enabled_vals + num_enabled); - - let server_vals = self.client.get_lighthouse_validators().await.unwrap().data; - - assert_eq!(server_vals.len(), self.vals_total()); - - assert_eq!(response.voting_pubkey, keypair.pk.into()); - assert_eq!(response.enabled, s.enabled); - - self - } - - pub async fn create_web3signer_validators(self, s: Web3SignerValidatorScenario) -> Self { - let initial_vals = self.vals_total(); - let initial_enabled_vals = self.vals_enabled(); - - let request: Vec<_> = (0..s.count) - .map(|i| { - let kp = Keypair::random(); - Web3SignerValidatorRequest { - enable: s.enabled, - description: format!("{}", i), - graffiti: None, - suggested_fee_recipient: None, - gas_limit: None, - builder_proposals: None, - voting_public_key: kp.pk, - url: format!("http://signer_{}.com/", i), - root_certificate_path: None, - request_timeout_ms: None, - client_identity_path: None, - client_identity_password: None, - } - }) - .collect(); - - self.client - .post_lighthouse_validators_web3signer(&request) - .await - .unwrap(); - - assert_eq!(self.vals_total(), initial_vals + s.count); - if s.enabled { - assert_eq!(self.vals_enabled(), initial_enabled_vals + s.count); - } else { - assert_eq!(self.vals_enabled(), initial_enabled_vals); - }; - - self - } - - pub async fn set_validator_enabled(self, index: usize, enabled: bool) -> Self { - let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; - - self.client - .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) - .await - .unwrap(); - - assert_eq!( - self.initialized_validators - .read() - .is_enabled(&validator.voting_pubkey.decompress().unwrap()) - .unwrap(), - enabled - ); - - assert!(self - .client - .get_lighthouse_validators() - .await - .unwrap() - .data - .into_iter() - .find(|v| v.voting_pubkey == validator.voting_pubkey) - .map(|v| v.enabled == enabled) - .unwrap()); - - // Check the server via an individual request. - assert_eq!( - self.client - .get_lighthouse_validators_pubkey(&validator.voting_pubkey) - .await - .unwrap() - .unwrap() - .data - .enabled, - enabled - ); - - self - } - - pub async fn set_gas_limit(self, index: usize, gas_limit: u64) -> Self { - let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; - - self.client - .patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) - .await - .unwrap(); - - self - } - - pub async fn assert_gas_limit(self, index: usize, gas_limit: u64) -> Self { - let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; - - assert_eq!( - self.validator_store.get_gas_limit(&validator.voting_pubkey), - gas_limit - ); - - self - } - - pub async fn set_builder_proposals(self, index: usize, builder_proposals: bool) -> Self { - let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; - - self.client - .patch_lighthouse_validators( - &validator.voting_pubkey, - None, - None, - Some(builder_proposals), - ) - .await - .unwrap(); - - self - } - - pub async fn assert_builder_proposals(self, index: usize, builder_proposals: bool) -> Self { - let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; - - assert_eq!( - self.validator_store - .get_builder_proposals(&validator.voting_pubkey), - builder_proposals - ); - - self - } -} - -struct HdValidatorScenario { - count: usize, - specify_mnemonic: bool, - key_derivation_path_offset: u32, - disabled: Vec, -} - -struct KeystoreValidatorScenario { - enabled: bool, - correct_password: bool, -} - -struct Web3SignerValidatorScenario { - count: usize, - enabled: bool, -} - #[test] fn invalid_pubkey() { let runtime = build_runtime(); From 2677443586c667373dc9caf71beb39736d6649b4 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 18 Aug 2022 16:59:27 +1000 Subject: [PATCH 027/138] Fix visibility --- validator_client/src/http_api/test_utils.rs | 30 ++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 52d86b6531f..45282c8fd28 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -36,30 +36,30 @@ pub const TEST_DEFAULT_FEE_RECIPIENT: Address = Address::repeat_byte(42); type E = MainnetEthSpec; pub struct HdValidatorScenario { - count: usize, - specify_mnemonic: bool, - key_derivation_path_offset: u32, - disabled: Vec, + pub count: usize, + pub specify_mnemonic: bool, + pub key_derivation_path_offset: u32, + pub disabled: Vec, } pub struct KeystoreValidatorScenario { - enabled: bool, - correct_password: bool, + pub enabled: bool, + pub correct_password: bool, } pub struct Web3SignerValidatorScenario { - count: usize, - enabled: bool, + pub count: usize, + pub enabled: bool, } pub struct ApiTester { - client: ValidatorClientHttpClient, - initialized_validators: Arc>, - validator_store: Arc>, - url: SensitiveUrl, - _server_shutdown: oneshot::Sender<()>, - _validator_dir: TempDir, - _runtime_shutdown: exit_future::Signal, + pub client: ValidatorClientHttpClient, + pub initialized_validators: Arc>, + pub validator_store: Arc>, + pub url: SensitiveUrl, + pub _server_shutdown: oneshot::Sender<()>, + pub _validator_dir: TempDir, + pub _runtime_shutdown: exit_future::Signal, } // Builds a runtime to be used in the testing configuration. From 391ece953f7c424eda0142ed6cdbfb94a7c29f5d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sun, 21 Aug 2022 18:01:21 +1000 Subject: [PATCH 028/138] Migrate VC tests to tokio::test --- validator_client/src/http_api/test_utils.rs | 27 +- validator_client/src/http_api/tests.rs | 575 ++++++++---------- .../src/http_api/tests/keystores.rs | 396 ++++++------ 3 files changed, 485 insertions(+), 513 deletions(-) diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 45282c8fd28..3e670c49142 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -25,9 +25,8 @@ use std::marker::PhantomData; use std::net::{IpAddr, Ipv4Addr}; use std::sync::Arc; use std::time::Duration; -use task_executor::TaskExecutor; +use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; -use tokio::runtime::Runtime; use tokio::sync::oneshot; pub const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; @@ -57,23 +56,13 @@ pub struct ApiTester { pub initialized_validators: Arc>, pub validator_store: Arc>, pub url: SensitiveUrl, + pub test_runtime: TestRuntime, pub _server_shutdown: oneshot::Sender<()>, pub _validator_dir: TempDir, - pub _runtime_shutdown: exit_future::Signal, -} - -// Builds a runtime to be used in the testing configuration. -pub fn build_runtime() -> Arc { - Arc::new( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("Should be able to build a testing runtime"), - ) } impl ApiTester { - pub async fn new(runtime: std::sync::Weak) -> Self { + pub async fn new() -> Self { let log = test_logger(); let validator_dir = tempdir().unwrap(); @@ -105,9 +94,7 @@ impl ApiTester { let slot_clock = TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); - let (runtime_shutdown, exit) = exit_future::signal(); - let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = TaskExecutor::new(runtime.clone(), exit, log.clone(), shutdown_tx); + let test_runtime = TestRuntime::default(); let validator_store = Arc::new(ValidatorStore::<_, E>::new( initialized_validators, @@ -117,7 +104,7 @@ impl ApiTester { Some(Arc::new(DoppelgangerService::new(log.clone()))), slot_clock, &config, - executor.clone(), + test_runtime.task_executor.clone(), log.clone(), )); @@ -128,7 +115,7 @@ impl ApiTester { let initialized_validators = validator_store.initialized_validators(); let context = Arc::new(Context { - task_executor: executor, + task_executor: test_runtime.task_executor.clone(), api_secret, validator_dir: Some(validator_dir.path().into()), validator_store: Some(validator_store.clone()), @@ -166,9 +153,9 @@ impl ApiTester { initialized_validators, validator_store, url, + test_runtime, _server_shutdown: shutdown_tx, _validator_dir: validator_dir, - _runtime_shutdown: runtime_shutdown, } } diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 2fa46622265..6de088c37cc 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -1,337 +1,298 @@ #![cfg(test)] -#![cfg(not(debug_assertions))] +// #![cfg(not(debug_assertions))] mod keystores; use crate::http_api::test_utils::{ - build_runtime, ApiTester, HdValidatorScenario, KeystoreValidatorScenario, - Web3SignerValidatorScenario, TEST_DEFAULT_FEE_RECIPIENT, + ApiTester, HdValidatorScenario, KeystoreValidatorScenario, Web3SignerValidatorScenario, + TEST_DEFAULT_FEE_RECIPIENT, }; use account_utils::{random_password, random_password_string, ZeroizeString}; use eth2::lighthouse_vc::types::*; use eth2_keystore::KeystoreBuilder; use std::future::Future; -use std::sync::Arc; type E = MainnetEthSpec; -#[test] -fn invalid_pubkey() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .invalidate_api_token() - .test_get_lighthouse_version_invalid() - .await; - }); +#[tokio::test] +async fn invalid_pubkey() { + ApiTester::new() + .await + .invalidate_api_token() + .test_get_lighthouse_version_invalid() + .await; } -#[test] -fn routes_with_invalid_auth() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .test_with_invalid_auth(|client| async move { client.get_lighthouse_version().await }) - .await - .test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await }) - .await - .test_with_invalid_auth(|client| async move { - client.get_lighthouse_spec::().await - }) - .await - .test_with_invalid_auth( - |client| async move { client.get_lighthouse_validators().await }, - ) - .await - .test_with_invalid_auth(|client| async move { - client - .get_lighthouse_validators_pubkey(&PublicKeyBytes::empty()) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .post_lighthouse_validators(vec![ValidatorRequest { - enable: <_>::default(), - description: <_>::default(), - graffiti: <_>::default(), - suggested_fee_recipient: <_>::default(), - gas_limit: <_>::default(), - builder_proposals: <_>::default(), - deposit_gwei: <_>::default(), - }]) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .post_lighthouse_validators_mnemonic(&CreateValidatorsMnemonicRequest { - mnemonic: String::default().into(), - key_derivation_path_offset: <_>::default(), - validators: <_>::default(), - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - let password = random_password(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) - .unwrap() - .build() - .unwrap(); - client - .post_lighthouse_validators_keystore(&KeystoreValidatorsPostRequest { - password: String::default().into(), - enable: <_>::default(), - keystore, - graffiti: <_>::default(), - suggested_fee_recipient: <_>::default(), - gas_limit: <_>::default(), - builder_proposals: <_>::default(), - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None) - .await - }) - .await - .test_with_invalid_auth(|client| async move { client.get_keystores().await }) - .await - .test_with_invalid_auth(|client| async move { - let password = random_password_string(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) - .unwrap() - .build() - .map(KeystoreJsonStr) - .unwrap(); - client - .post_keystores(&ImportKeystoresRequest { - keystores: vec![keystore], - passwords: vec![password], - slashing_protection: None, - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - let keypair = Keypair::random(); - client - .delete_keystores(&DeleteKeystoresRequest { - pubkeys: vec![keypair.pk.compress()], - }) - .await - }) - .await - }); +#[tokio::test] +async fn routes_with_invalid_auth() { + ApiTester::new() + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_version().await }) + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await }) + .await + .test_with_invalid_auth(|client| async move { + client.get_lighthouse_spec::().await + }) + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_validators().await }) + .await + .test_with_invalid_auth(|client| async move { + client + .get_lighthouse_validators_pubkey(&PublicKeyBytes::empty()) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .post_lighthouse_validators(vec![ValidatorRequest { + enable: <_>::default(), + description: <_>::default(), + graffiti: <_>::default(), + suggested_fee_recipient: <_>::default(), + gas_limit: <_>::default(), + builder_proposals: <_>::default(), + deposit_gwei: <_>::default(), + }]) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .post_lighthouse_validators_mnemonic(&CreateValidatorsMnemonicRequest { + mnemonic: String::default().into(), + key_derivation_path_offset: <_>::default(), + validators: <_>::default(), + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) + .unwrap() + .build() + .unwrap(); + client + .post_lighthouse_validators_keystore(&KeystoreValidatorsPostRequest { + password: String::default().into(), + enable: <_>::default(), + keystore, + graffiti: <_>::default(), + suggested_fee_recipient: <_>::default(), + gas_limit: <_>::default(), + builder_proposals: <_>::default(), + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None) + .await + }) + .await + .test_with_invalid_auth(|client| async move { client.get_keystores().await }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password_string(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) + .unwrap() + .build() + .map(KeystoreJsonStr) + .unwrap(); + client + .post_keystores(&ImportKeystoresRequest { + keystores: vec![keystore], + passwords: vec![password], + slashing_protection: None, + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let keypair = Keypair::random(); + client + .delete_keystores(&DeleteKeystoresRequest { + pubkeys: vec![keypair.pk.compress()], + }) + .await + }) + .await; } -#[test] -fn simple_getters() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .test_get_lighthouse_version() - .await - .test_get_lighthouse_health() - .await - .test_get_lighthouse_spec() - .await; - }); +#[tokio::test] +async fn simple_getters() { + ApiTester::new() + .await + .test_get_lighthouse_version() + .await + .test_get_lighthouse_health() + .await + .test_get_lighthouse_spec() + .await; } -#[test] -fn hd_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: true, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .create_hd_validators(HdValidatorScenario { - count: 1, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![0], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(3) - .create_hd_validators(HdValidatorScenario { - count: 0, - specify_mnemonic: true, - key_derivation_path_offset: 4, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(3); - }); +#[tokio::test] +async fn hd_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: true, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .create_hd_validators(HdValidatorScenario { + count: 1, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![0], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3) + .create_hd_validators(HdValidatorScenario { + count: 0, + specify_mnemonic: true, + key_derivation_path_offset: 4, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3); } -#[test] -fn validator_enabling() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2); - }); +#[tokio::test] +async fn validator_enabling() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2); } -#[test] -fn validator_gas_limit() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_gas_limit(0, 500) - .await - .assert_gas_limit(0, 500) - .await - // Update gas limit while validator is disabled. - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_gas_limit(0, 1000) - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_gas_limit(0, 1000) - .await - }); +#[tokio::test] +async fn validator_gas_limit() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_gas_limit(0, 500) + .await + .assert_gas_limit(0, 500) + .await + // Update gas limit while validator is disabled. + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_gas_limit(0, 1000) + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_gas_limit(0, 1000) + .await; } -#[test] -fn validator_builder_proposals() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_builder_proposals(0, true) - .await - // Test setting builder proposals while the validator is disabled - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_builder_proposals(0, false) - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_builder_proposals(0, false) - .await - }); +#[tokio::test] +async fn validator_builder_proposals() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_builder_proposals(0, true) + .await + // Test setting builder proposals while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_builder_proposals(0, false) + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_builder_proposals(0, false) + .await; } -#[test] -fn keystore_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: true, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: false, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: true, - enabled: false, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2); - }); +#[tokio::test] +async fn keystore_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: false, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: false, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2); } -#[test] -fn web3signer_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_web3signer_validators(Web3SignerValidatorScenario { - count: 1, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1); - }); +#[tokio::test] +async fn web3signer_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_web3signer_validators(Web3SignerValidatorScenario { + count: 1, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1); } diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index 769d8a1d49c..6a401575579 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -12,6 +12,7 @@ use itertools::Itertools; use rand::{rngs::SmallRng, Rng, SeedableRng}; use slashing_protection::interchange::{Interchange, InterchangeMetadata}; use std::{collections::HashMap, path::Path}; +use tokio::runtime::Handle; use types::Address; fn new_keystore(password: ZeroizeString) -> Keystore { @@ -64,31 +65,23 @@ fn remotekey_validator_with_pubkey(pubkey: PublicKey) -> SingleImportRemotekeysR } } -fn run_test(f: F) +async fn run_test(f: F) where F: FnOnce(ApiTester) -> V, V: Future, { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - let tester = ApiTester::new(weak_runtime).await; - f(tester).await - }); + let tester = ApiTester::new().await; + f(tester).await } -fn run_dual_vc_test(f: F) +async fn run_dual_vc_test(f: F) where F: FnOnce(ApiTester, ApiTester) -> V, V: Future, { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - let tester1 = ApiTester::new(weak_runtime.clone()).await; - let tester2 = ApiTester::new(weak_runtime).await; - f(tester1, tester2).await - }); + let tester1 = ApiTester::new().await; + let tester2 = ApiTester::new().await; + f(tester1, tester2).await } fn keystore_pubkey(keystore: &Keystore) -> PublicKeyBytes { @@ -199,8 +192,8 @@ fn check_remotekey_delete_response( } } -#[test] -fn get_auth_no_token() { +#[tokio::test] +async fn get_auth_no_token() { run_test(|mut tester| async move { let _ = &tester; tester.client.send_authorization_header(false); @@ -213,19 +206,21 @@ fn get_auth_no_token() { // The token should match the one that the client was originally initialised with. assert!(tester.client.api_token() == Some(&token)); }) + .await; } -#[test] -fn get_empty_keystores() { +#[tokio::test] +async fn get_empty_keystores() { run_test(|tester| async move { let _ = &tester; let res = tester.client.get_keystores().await.unwrap(); assert_eq!(res, ListKeystoresResponse { data: vec![] }); }) + .await; } -#[test] -fn import_new_keystores() { +#[tokio::test] +async fn import_new_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -250,10 +245,11 @@ fn import_new_keystores() { let get_res = tester.client.get_keystores().await.unwrap(); check_keystore_get_response(&get_res, &keystores); }) + .await; } -#[test] -fn import_only_duplicate_keystores() { +#[tokio::test] +async fn import_only_duplicate_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -279,10 +275,11 @@ fn import_only_duplicate_keystores() { let get_res = tester.client.get_keystores().await.unwrap(); check_keystore_get_response(&get_res, &keystores); }) + .await; } -#[test] -fn import_some_duplicate_keystores() { +#[tokio::test] +async fn import_some_duplicate_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -330,10 +327,11 @@ fn import_some_duplicate_keystores() { let import_res = tester.client.post_keystores(&req2).await.unwrap(); check_keystore_import_response(&import_res, expected); }) + .await; } -#[test] -fn import_wrong_number_of_passwords() { +#[tokio::test] +async fn import_wrong_number_of_passwords() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -352,10 +350,11 @@ fn import_wrong_number_of_passwords() { .unwrap_err(); assert_eq!(err.status().unwrap(), 400); }) + .await; } -#[test] -fn get_web3_signer_keystores() { +#[tokio::test] +async fn get_web3_signer_keystores() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -412,10 +411,11 @@ fn get_web3_signer_keystores() { assert!(get_res.data.contains(&response), "{:?}", response); } }) + .await; } -#[test] -fn import_and_delete_conflicting_web3_signer_keystores() { +#[tokio::test] +async fn import_and_delete_conflicting_web3_signer_keystores() { run_test(|tester| async move { let _ = &tester; let num_keystores = 3; @@ -477,10 +477,11 @@ fn import_and_delete_conflicting_web3_signer_keystores() { let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); check_keystore_delete_response(&delete_res, all_delete_error(keystores.len())); }) + .await; } -#[test] -fn import_keystores_wrong_password() { +#[tokio::test] +async fn import_keystores_wrong_password() { run_test(|tester| async move { let _ = &tester; let num_keystores = 4; @@ -551,11 +552,12 @@ fn import_keystores_wrong_password() { &import_res, (0..num_keystores).map(|_| ImportKeystoreStatus::Duplicate), ); - }); + }) + .await; } -#[test] -fn import_invalid_slashing_protection() { +#[tokio::test] +async fn import_invalid_slashing_protection() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -589,10 +591,11 @@ fn import_invalid_slashing_protection() { let get_res = tester.client.get_keystores().await.unwrap(); check_keystore_get_response(&get_res, &[]); }) + .await; } -#[test] -fn check_get_set_fee_recipient() { +#[tokio::test] +async fn check_get_set_fee_recipient() { run_test(|tester: ApiTester| async move { let _ = &tester; let password = random_password_string(); @@ -768,6 +771,7 @@ fn check_get_set_fee_recipient() { ); } }) + .await; } #[test] @@ -949,8 +953,8 @@ fn all_indices(count: usize) -> Vec { (0..count).collect() } -#[test] -fn migrate_all_with_slashing_protection() { +#[tokio::test] +async fn migrate_all_with_slashing_protection() { let n = 3; generic_migration_test( n, @@ -967,11 +971,12 @@ fn migrate_all_with_slashing_protection() { (1, make_attestation(2, 3), false), (2, make_attestation(1, 2), false), ], - ); + ) + .await; } -#[test] -fn migrate_some_with_slashing_protection() { +#[tokio::test] +async fn migrate_some_with_slashing_protection() { let n = 3; generic_migration_test( n, @@ -989,11 +994,12 @@ fn migrate_some_with_slashing_protection() { (0, make_attestation(2, 3), true), (1, make_attestation(3, 4), true), ], - ); + ) + .await; } -#[test] -fn migrate_some_missing_slashing_protection() { +#[tokio::test] +async fn migrate_some_missing_slashing_protection() { let n = 3; generic_migration_test( n, @@ -1010,11 +1016,12 @@ fn migrate_some_missing_slashing_protection() { (1, make_attestation(2, 3), true), (0, make_attestation(2, 3), true), ], - ); + ) + .await; } -#[test] -fn migrate_some_extra_slashing_protection() { +#[tokio::test] +async fn migrate_some_extra_slashing_protection() { let n = 3; generic_migration_test( n, @@ -1033,7 +1040,8 @@ fn migrate_some_extra_slashing_protection() { (1, make_attestation(3, 4), true), (2, make_attestation(2, 3), false), ], - ); + ) + .await; } /// Run a test that creates some validators on one VC, and then migrates them to a second VC. @@ -1051,7 +1059,7 @@ fn migrate_some_extra_slashing_protection() { /// - `import_indices`: validators to transfer. It needn't be a subset of `delete_indices`. /// - `second_vc_attestations`: attestations to sign on the second VC after the transfer. The bool /// indicates whether the signing should be successful. -fn generic_migration_test( +async fn generic_migration_test( num_validators: usize, first_vc_attestations: Vec<(usize, Attestation)>, delete_indices: Vec, @@ -1169,11 +1177,12 @@ fn generic_migration_test( Err(e) => assert!(!should_succeed, "{:?}", e), } } - }); + }) + .await } -#[test] -fn delete_keystores_twice() { +#[tokio::test] +async fn delete_keystores_twice() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -1201,10 +1210,11 @@ fn delete_keystores_twice() { let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); check_keystore_delete_response(&delete_res, all_not_active(keystores.len())); }) + .await } -#[test] -fn delete_nonexistent_keystores() { +#[tokio::test] +async fn delete_nonexistent_keystores() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -1219,6 +1229,7 @@ fn delete_nonexistent_keystores() { let delete_res = tester.client.delete_keystores(&delete_req).await.unwrap(); check_keystore_delete_response(&delete_res, all_not_found(keystores.len())); }) + .await } fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation { @@ -1242,9 +1253,9 @@ fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation { } } -#[test] -fn delete_concurrent_with_signing() { - let runtime = build_runtime(); +#[tokio::test] +async fn delete_concurrent_with_signing() { + let handle = Handle::try_current().unwrap(); let num_keys = 8; let num_signing_threads = 8; let num_attestations = 100; @@ -1257,115 +1268,112 @@ fn delete_concurrent_with_signing() { "num_keys should be divisible by num threads for simplicity" ); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - let tester = ApiTester::new(weak_runtime).await; - - // Generate a lot of keys and import them. - let password = random_password_string(); - let keystores = (0..num_keys) - .map(|_| new_keystore(password.clone())) - .collect::>(); - let all_pubkeys = keystores.iter().map(keystore_pubkey).collect::>(); + let tester = ApiTester::new().await; - let import_res = tester - .client - .post_keystores(&ImportKeystoresRequest { - keystores: keystores.clone(), - passwords: vec![password.clone(); keystores.len()], - slashing_protection: None, - }) - .await - .unwrap(); - check_keystore_import_response(&import_res, all_imported(keystores.len())); + // Generate a lot of keys and import them. + let password = random_password_string(); + let keystores = (0..num_keys) + .map(|_| new_keystore(password.clone())) + .collect::>(); + let all_pubkeys = keystores.iter().map(keystore_pubkey).collect::>(); - // Start several threads signing attestations at sequential epochs. - let mut join_handles = vec![]; - - for thread_index in 0..num_signing_threads { - let keys_per_thread = num_keys / num_signing_threads; - let validator_store = tester.validator_store.clone(); - let thread_pubkeys = all_pubkeys - [thread_index * keys_per_thread..(thread_index + 1) * keys_per_thread] - .to_vec(); - - let handle = runtime.spawn(async move { - for j in 0..num_attestations { - let mut att = make_attestation(j, j + 1); - for (_validator_id, public_key) in thread_pubkeys.iter().enumerate() { - let _ = validator_store - .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) - .await; - } + let import_res = tester + .client + .post_keystores(&ImportKeystoresRequest { + keystores: keystores.clone(), + passwords: vec![password.clone(); keystores.len()], + slashing_protection: None, + }) + .await + .unwrap(); + check_keystore_import_response(&import_res, all_imported(keystores.len())); + + // Start several threads signing attestations at sequential epochs. + let mut join_handles = vec![]; + + for thread_index in 0..num_signing_threads { + let keys_per_thread = num_keys / num_signing_threads; + let validator_store = tester.validator_store.clone(); + let thread_pubkeys = all_pubkeys + [thread_index * keys_per_thread..(thread_index + 1) * keys_per_thread] + .to_vec(); + + let handle = handle.spawn(async move { + for j in 0..num_attestations { + let mut att = make_attestation(j, j + 1); + for (_validator_id, public_key) in thread_pubkeys.iter().enumerate() { + let _ = validator_store + .sign_attestation(*public_key, 0, &mut att, Epoch::new(j + 1)) + .await; } - }); - join_handles.push(handle); - } + } + }); + join_handles.push(handle); + } + + // Concurrently, delete each validator one at a time. Store the slashing protection + // data so we can ensure it doesn't change after a key is exported. + let mut delete_handles = vec![]; + for _ in 0..num_delete_threads { + let client = tester.client.clone(); + let all_pubkeys = all_pubkeys.clone(); + + let handle = handle.spawn(async move { + let mut rng = SmallRng::from_entropy(); + + let mut slashing_protection = vec![]; + for _ in 0..num_delete_attempts { + let to_delete = all_pubkeys + .iter() + .filter(|_| rng.gen_bool(delete_prob)) + .copied() + .collect::>(); + + if !to_delete.is_empty() { + let delete_res = client + .delete_keystores(&DeleteKeystoresRequest { pubkeys: to_delete }) + .await + .unwrap(); - // Concurrently, delete each validator one at a time. Store the slashing protection - // data so we can ensure it doesn't change after a key is exported. - let mut delete_handles = vec![]; - for _ in 0..num_delete_threads { - let client = tester.client.clone(); - let all_pubkeys = all_pubkeys.clone(); - - let handle = runtime.spawn(async move { - let mut rng = SmallRng::from_entropy(); - - let mut slashing_protection = vec![]; - for _ in 0..num_delete_attempts { - let to_delete = all_pubkeys - .iter() - .filter(|_| rng.gen_bool(delete_prob)) - .copied() - .collect::>(); - - if !to_delete.is_empty() { - let delete_res = client - .delete_keystores(&DeleteKeystoresRequest { pubkeys: to_delete }) - .await - .unwrap(); - - for status in delete_res.data.iter() { - assert_ne!(status.status, DeleteKeystoreStatus::Error); - } - - slashing_protection.push(delete_res.slashing_protection); + for status in delete_res.data.iter() { + assert_ne!(status.status, DeleteKeystoreStatus::Error); } + + slashing_protection.push(delete_res.slashing_protection); } - slashing_protection - }); + } + slashing_protection + }); - delete_handles.push(handle); - } + delete_handles.push(handle); + } - // Collect slashing protection. - let mut slashing_protection_map = HashMap::new(); - let collected_slashing_protection = futures::future::join_all(delete_handles).await; - - for interchange in collected_slashing_protection - .into_iter() - .flat_map(Result::unwrap) - { - for validator_data in interchange.data { - slashing_protection_map - .entry(validator_data.pubkey) - .and_modify(|existing| { - assert_eq!( - *existing, validator_data, - "slashing protection data changed after first export" - ) - }) - .or_insert(validator_data); - } + // Collect slashing protection. + let mut slashing_protection_map = HashMap::new(); + let collected_slashing_protection = futures::future::join_all(delete_handles).await; + + for interchange in collected_slashing_protection + .into_iter() + .flat_map(Result::unwrap) + { + for validator_data in interchange.data { + slashing_protection_map + .entry(validator_data.pubkey) + .and_modify(|existing| { + assert_eq!( + *existing, validator_data, + "slashing protection data changed after first export" + ) + }) + .or_insert(validator_data); } + } - futures::future::join_all(join_handles).await - }); + futures::future::join_all(join_handles).await; } -#[test] -fn delete_then_reimport() { +#[tokio::test] +async fn delete_then_reimport() { run_test(|tester| async move { let _ = &tester; let password = random_password_string(); @@ -1396,19 +1404,21 @@ fn delete_then_reimport() { let import_res = tester.client.post_keystores(&import_req).await.unwrap(); check_keystore_import_response(&import_res, all_imported(keystores.len())); }) + .await } -#[test] -fn get_empty_remotekeys() { +#[tokio::test] +async fn get_empty_remotekeys() { run_test(|tester| async move { let _ = &tester; let res = tester.client.get_remotekeys().await.unwrap(); assert_eq!(res, ListRemotekeysResponse { data: vec![] }); }) + .await } -#[test] -fn import_new_remotekeys() { +#[tokio::test] +async fn import_new_remotekeys() { run_test(|tester| async move { let _ = &tester; @@ -1443,10 +1453,11 @@ fn import_new_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_same_remotekey_different_url() { +#[tokio::test] +async fn import_same_remotekey_different_url() { run_test(|tester| async move { let _ = &tester; @@ -1485,10 +1496,11 @@ fn import_same_remotekey_different_url() { }], ); }) + .await } -#[test] -fn delete_remotekey_then_reimport_different_url() { +#[tokio::test] +async fn delete_remotekey_then_reimport_different_url() { run_test(|tester| async move { let _ = &tester; @@ -1534,10 +1546,11 @@ fn delete_remotekey_then_reimport_different_url() { vec![ImportRemotekeyStatus::Imported].into_iter(), ); }) + .await } -#[test] -fn import_only_duplicate_remotekeys() { +#[tokio::test] +async fn import_only_duplicate_remotekeys() { run_test(|tester| async move { let _ = &tester; let remotekeys = (0..3) @@ -1582,10 +1595,11 @@ fn import_only_duplicate_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_some_duplicate_remotekeys() { +#[tokio::test] +async fn import_some_duplicate_remotekeys() { run_test(|tester| async move { let _ = &tester; let num_remotekeys = 5; @@ -1649,10 +1663,11 @@ fn import_some_duplicate_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remote_and_local_keys() { +#[tokio::test] +async fn import_remote_and_local_keys() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -1714,10 +1729,11 @@ fn import_remote_and_local_keys() { assert!(get_res.data.contains(&response), "{:?}", response); } }) + .await } -#[test] -fn import_same_local_and_remote_keys() { +#[tokio::test] +async fn import_same_local_and_remote_keys() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -1782,9 +1798,10 @@ fn import_same_local_and_remote_keys() { assert!(get_res.data.contains(&response), "{:?}", response); } }) + .await } -#[test] -fn import_same_remote_and_local_keys() { +#[tokio::test] +async fn import_same_remote_and_local_keys() { run_test(|tester| async move { let _ = &tester; let num_local = 3; @@ -1847,10 +1864,11 @@ fn import_same_remote_and_local_keys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn delete_remotekeys_twice() { +#[tokio::test] +async fn delete_remotekeys_twice() { run_test(|tester| async move { let _ = &tester; @@ -1893,10 +1911,11 @@ fn delete_remotekeys_twice() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, Vec::new()); }) + .await } -#[test] -fn delete_nonexistent_remotekey() { +#[tokio::test] +async fn delete_nonexistent_remotekey() { run_test(|tester| async move { let _ = &tester; @@ -1919,10 +1938,11 @@ fn delete_nonexistent_remotekey() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, Vec::new()); }) + .await } -#[test] -fn delete_then_reimport_remotekeys() { +#[tokio::test] +async fn delete_then_reimport_remotekeys() { run_test(|tester| async move { let _ = &tester; @@ -1984,10 +2004,11 @@ fn delete_then_reimport_remotekeys() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remotekey_web3signer() { +#[tokio::test] +async fn import_remotekey_web3signer() { run_test(|tester| async move { let _ = &tester; @@ -2043,10 +2064,11 @@ fn import_remotekey_web3signer() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remotekey_web3signer_disabled() { +#[tokio::test] +async fn import_remotekey_web3signer_disabled() { run_test(|tester| async move { let _ = &tester; @@ -2096,10 +2118,11 @@ fn import_remotekey_web3signer_disabled() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } -#[test] -fn import_remotekey_web3signer_enabled() { +#[tokio::test] +async fn import_remotekey_web3signer_enabled() { run_test(|tester| async move { let _ = &tester; @@ -2156,4 +2179,5 @@ fn import_remotekey_web3signer_enabled() { let get_res = tester.client.get_remotekeys().await.unwrap(); check_remotekey_get_response(&get_res, expected_responses); }) + .await } From fa6eba911ef93d81fe7d35f96133c1d22ae1ae5c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 22 Aug 2022 09:17:46 +1000 Subject: [PATCH 029/138] Start adding import validator testing --- Cargo.lock | 1 + validator_client/src/http_api/test_utils.rs | 6 +- validator_manager/Cargo.toml | 1 + .../src/validators/import_validators.rs | 283 ++++++++++-------- 4 files changed, 172 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 899dcb09d8b..5ed823ccd0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7378,6 +7378,7 @@ dependencies = [ "tokio", "tree_hash", "types", + "validator_client", ] [[package]] diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 3e670c49142..25af9be6fe4 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -56,6 +56,7 @@ pub struct ApiTester { pub initialized_validators: Arc>, pub validator_store: Arc>, pub url: SensitiveUrl, + pub api_token: String, pub test_runtime: TestRuntime, pub _server_shutdown: oneshot::Sender<()>, pub _validator_dir: TempDir, @@ -116,7 +117,7 @@ impl ApiTester { let context = Arc::new(Context { task_executor: test_runtime.task_executor.clone(), - api_secret, + api_secret: api_secret, validator_dir: Some(validator_dir.path().into()), validator_store: Some(validator_store.clone()), spec: E::default_spec(), @@ -146,13 +147,14 @@ impl ApiTester { )) .unwrap(); - let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey).unwrap(); + let client = ValidatorClientHttpClient::new(url.clone(), api_pubkey.clone()).unwrap(); Self { client, initialized_validators, validator_store, url, + api_token: api_pubkey, test_runtime, _server_shutdown: shutdown_tx, _validator_dir: validator_dir, diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 38cbbae6698..6e56d523105 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -27,3 +27,4 @@ tempfile = "3.1.0" tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } regex = "1.6.0" eth2_network_config = { path = "../common/eth2_network_config" } +validator_client = { path = "../validator_client" } diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index ffdb88bf879..4ee6b0247a1 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -70,8 +70,38 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } +#[derive(Clone)] +struct ImportConfig { + validators_file_path: PathBuf, + vc_url: SensitiveUrl, + vc_token_path: PathBuf, + ignore_duplicates: bool, +} + +impl ImportConfig { + fn from_cli(matches: &ArgMatches) -> Result { + Ok(Self { + validators_file_path: clap_utils::parse_required(matches, VALIDATORS_FILE_FLAG)?, + vc_url: clap_utils::parse_required(matches, VALIDATOR_CLIENT_URL_FLAG)?, + vc_token_path: clap_utils::parse_required(matches, VALIDATOR_CLIENT_TOKEN_FLAG)?, + ignore_duplicates: matches.is_present(IGNORE_DUPLICATES_FLAG), + }) + } +} + pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>) -> Result<(), String> { - let validators_file_path: PathBuf = clap_utils::parse_required(matches, VALIDATORS_FILE_FLAG)?; + let config = ImportConfig::from_cli(matches)?; + run(config).await +} + +async fn run<'a>(config: ImportConfig) -> Result<(), String> { + let ImportConfig { + validators_file_path, + vc_url, + vc_token_path, + ignore_duplicates, + } = config; + if !validators_file_path.exists() { return Err(format!("Unable to find file at {:?}", validators_file_path)); } @@ -81,136 +111,155 @@ pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>) -> Result<(), String> { .create(false) .open(&validators_file_path) .map_err(|e| format!("Unable to open {:?}: {:?}", validators_file_path, e))?; - let validators = serde_json::from_reader(&validators_file).map_err(|e| { - format!( - "Unable to parse JSON in {:?}: {:?}", - validators_file_path, e - ) - })?; - - import_validators(matches, validators).await -} + let validators: Vec = serde_json::from_reader(&validators_file) + .map_err(|e| { + format!( + "Unable to parse JSON in {:?}: {:?}", + validators_file_path, e + ) + })?; -pub async fn import_validators<'a>( - matches: &'a ArgMatches<'a>, - validators: Vec, -) -> Result<(), String> { let count = validators.len(); - let vc_url: Option = - clap_utils::parse_optional(matches, VALIDATOR_CLIENT_URL_FLAG)?; - let vc_token_path: Option = - clap_utils::parse_optional(matches, VALIDATOR_CLIENT_TOKEN_FLAG)?; - - let http_client = match (vc_url, vc_token_path) { - (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) - } - _ => { - return Err(format!( - "Inconsistent use of {} and {}", - VALIDATOR_CLIENT_URL_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG - )) - } - }; + let http_client = { + 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 + ) + })?; - if let Some(http_client) = http_client { + // 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!( - "Starting to submit validators {} to VC, each validator may take several seconds", - count + "Validator client is reachable at {} and reports {} validators", + vc_url, + remote_keystores.data.len() ); - for (i, validator) in validators.into_iter().enumerate() { - let ValidatorSpecification { - voting_keystore, - voting_keystore_password, - slashing_protection, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - } = validator; - - let voting_public_key = voting_keystore - .public_key() - .ok_or_else(|| { - format!("Validator keystore at index {} is missing a public key", i) - })? - .into(); - - let request = ImportKeystoresRequest { - keystores: vec![voting_keystore], - passwords: vec![voting_keystore_password], - slashing_protection, - }; - - if let Err(e) = http_client.post_keystores(&request).await { - eprintln!( - "Failed to upload batch {}. Some keys were imported whilst \ + http_client + }; + + eprintln!( + "Starting to submit validators {} to VC, each validator may take several seconds", + count + ); + + for (i, validator) in validators.into_iter().enumerate() { + let ValidatorSpecification { + voting_keystore, + voting_keystore_password, + slashing_protection, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = validator; + + let voting_public_key = voting_keystore + .public_key() + .ok_or_else(|| format!("Validator keystore at index {} is missing a public key", i))? + .into(); + + let request = ImportKeystoresRequest { + keystores: vec![voting_keystore], + passwords: vec![voting_keystore_password], + slashing_protection, + }; + + if let Err(e) = http_client.post_keystores(&request).await { + eprintln!( + "Failed to upload batch {}. Some keys were imported whilst \ others may not have been imported. A potential solution is to use the \ --{} flag, however care should be taken to ensure that there are no \ duplicate deposits submitted.", - i, IGNORE_DUPLICATES_FLAG - ); - // Return here *without* writing the deposit JSON file. This might help prevent - // users from submitting duplicate deposits or deposits for validators that weren't - // initialized on a VC. - // - // Next the the user runs with the --ignore-duplicates flag there should be a new, - // complete deposit JSON file created. - return Err(format!("Key upload failed: {:?}", e)); - } - - if let Some(fee_recipient) = fee_recipient { - http_client - .post_fee_recipient( - &voting_public_key, - &UpdateFeeRecipientRequest { - ethaddress: fee_recipient, - }, - ) - .await - .map_err(|e| format!("Failed to update fee recipient on VC: {:?}", e))?; - } + i, IGNORE_DUPLICATES_FLAG + ); + // Return here *without* writing the deposit JSON file. This might help prevent + // users from submitting duplicate deposits or deposits for validators that weren't + // initialized on a VC. + // + // Next the the user runs with the --ignore-duplicates flag there should be a new, + // complete deposit JSON file created. + return Err(format!("Key upload failed: {:?}", e)); + } - if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { - http_client - .patch_lighthouse_validators( - &voting_public_key, - enabled, - gas_limit, - builder_proposals, - ) - .await - .map_err(|e| format!("Failed to update lighthouse validator on VC: {:?}", e))?; - } + if let Some(fee_recipient) = fee_recipient { + http_client + .post_fee_recipient( + &voting_public_key, + &UpdateFeeRecipientRequest { + ethaddress: fee_recipient, + }, + ) + .await + .map_err(|e| format!("Failed to update fee recipient on VC: {:?}", e))?; + } - eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count); + if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { + http_client + .patch_lighthouse_validators( + &voting_public_key, + enabled, + gas_limit, + builder_proposals, + ) + .await + .map_err(|e| format!("Failed to update lighthouse validator on VC: {:?}", e))?; } + + eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count); } Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + use std::fs; + use tempfile::{tempdir, TempDir}; + use validator_client::http_api::test_utils::ApiTester; + + const VALIDATORS_FILE_NAME: &str = "validators.json"; + const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; + + struct TestBuilder { + config: ImportConfig, + vc: ApiTester, + dir: TempDir, + } + + impl TestBuilder { + async fn new() -> Self { + let dir = tempdir().unwrap(); + let vc = ApiTester::new().await; + let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); + fs::write(&vc_token_path, &vc.api_token).unwrap(); + + Self { + config: ImportConfig { + validators_file_path: dir.path().join(VALIDATORS_FILE_NAME), + vc_url: vc.url.clone(), + vc_token_path, + ignore_duplicates: false, + }, + vc, + dir, + } + } + } + + #[tokio::test] + async fn blah() { + TestBuilder::new().await; + } +} From 7ed821bbe145afeef6fb70a3a82df5f413048f50 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 22 Aug 2022 11:35:13 +1000 Subject: [PATCH 030/138] Progress with testing --- .../src/validators/create_validators.rs | 53 ++++---- .../src/validators/import_validators.rs | 123 +++++++++++++++++- 2 files changed, 148 insertions(+), 28 deletions(-) diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 2a7b050d143..0c5869c4a88 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -181,20 +181,20 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /// The CLI arguments are parsed into this struct before running the application. This step of /// indirection allows for testing the underlying logic without needing to parse CLI arguments. #[derive(Clone)] -struct CreateConfig { - output_path: PathBuf, - first_index: u32, - count: u32, - deposit_gwei: u64, - mnemonic_path: Option, - stdin_inputs: bool, - disable_deposits: bool, - specify_voting_keystore_password: bool, - eth1_withdrawal_address: Option
, - builder_proposals: bool, - fee_recipient: Option
, - gas_limit: Option, - bn_url: Option, +pub struct CreateConfig { + pub output_path: PathBuf, + pub first_index: u32, + pub count: u32, + pub deposit_gwei: u64, + pub mnemonic_path: Option, + pub stdin_inputs: bool, + pub disable_deposits: bool, + pub specify_voting_keystore_password: bool, + pub eth1_withdrawal_address: Option
, + pub builder_proposals: bool, + pub fee_recipient: Option
, + pub gas_limit: Option, + pub bn_url: Option, } impl CreateConfig { @@ -518,7 +518,7 @@ fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Re } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use eth2_network_config::Eth2NetworkConfig; use regex::Regex; @@ -530,7 +530,7 @@ mod tests { const TEST_VECTOR_DEPOSIT_CLI_VERSION: &str = "2.3.0"; - struct TestBuilder { + pub struct TestBuilder { spec: ChainSpec, output_dir: TempDir, mnemonic_dir: TempDir, @@ -544,7 +544,7 @@ mod tests { } impl TestBuilder { - fn new(spec: ChainSpec) -> Self { + pub fn new(spec: ChainSpec) -> Self { let output_dir = tempdir().unwrap(); let mnemonic_dir = tempdir().unwrap(); let mnemonic_path = mnemonic_dir.path().join("mnemonic"); @@ -578,12 +578,12 @@ mod tests { } } - fn mutate_config(mut self, func: F) -> Self { + pub fn mutate_config(mut self, func: F) -> Self { func(&mut self.config); self } - async fn run_test(self) -> TestResult { + pub async fn run_test(self) -> TestResult { let Self { spec, output_dir, @@ -686,12 +686,21 @@ mod tests { } #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. - struct TestResult { - result: Result<(), String>, - output_dir: TempDir, + pub struct TestResult { + pub result: Result<(), String>, + pub output_dir: TempDir, } impl TestResult { + pub fn validators_file_path(&self) -> PathBuf { + self.output_dir.path().join(VALIDATORS_FILENAME) + } + + pub fn validators(&self) -> Vec { + let contents = fs::read_to_string(self.validators_file_path()).unwrap(); + serde_json::from_str(&contents).unwrap() + } + fn assert_ok(self) { assert_eq!(self.result, Ok(())) } diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 4ee6b0247a1..9f101891312 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -225,17 +225,20 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { #[cfg(test)] mod test { use super::*; + use crate::validators::create_validators::tests::TestBuilder as CreateTestBuilder; use std::fs; use tempfile::{tempdir, TempDir}; use validator_client::http_api::test_utils::ApiTester; - const VALIDATORS_FILE_NAME: &str = "validators.json"; const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; struct TestBuilder { - config: ImportConfig, + import_config: ImportConfig, vc: ApiTester, dir: TempDir, + /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up + /// before we can read it. + create_dir: Option, } impl TestBuilder { @@ -246,20 +249,128 @@ mod test { fs::write(&vc_token_path, &vc.api_token).unwrap(); Self { - config: ImportConfig { - validators_file_path: dir.path().join(VALIDATORS_FILE_NAME), + import_config: ImportConfig { + // This field will be overwritten later on. + validators_file_path: dir.path().into(), vc_url: vc.url.clone(), vc_token_path, ignore_duplicates: false, }, vc, dir, + create_dir: None, } } + + async fn create_validators(mut self, count: u32, first_index: u32) -> Self { + let create_result = CreateTestBuilder::default() + .mutate_config(|config| { + config.count = count; + config.first_index = first_index; + }) + .run_test() + .await; + assert!( + create_result.result.is_ok(), + "precondition: validators are created" + ); + self.import_config.validators_file_path = create_result.validators_file_path(); + self.create_dir = Some(create_result.output_dir); + self + } + + async fn run_test(self) -> TestResult { + let result = run(self.import_config.clone()).await; + + if result.is_ok() { + let local_validators: Vec = { + let contents = + fs::read_to_string(&self.import_config.validators_file_path).unwrap(); + serde_json::from_str(&contents).unwrap() + }; + let list_keystores_response = self.vc.client.get_keystores().await.unwrap().data; + + assert_eq!( + local_validators.len(), + list_keystores_response.len(), + "vc should have exactly the number of validators imported" + ); + + for local_validator in &local_validators { + let local_keystore = &local_validator.voting_keystore.0; + let local_pubkey = local_keystore.public_key().unwrap().into(); + let remote_validator = list_keystores_response + .iter() + .find(|validator| validator.validating_pubkey == local_pubkey) + .expect("validator must exist on VC"); + assert_eq!(&remote_validator.derivation_path, &local_keystore.path()); + // It's not immediately clear why Lighthouse returns `None` rather than + // `Some(false)` here, I would expect the latter to be the most accurate. + // However, it doesn't seem like a big deal. + assert_eq!(remote_validator.readonly, None); + } + } + + TestResult { result } + } + } + + #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. + struct TestResult { + result: Result<(), String>, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + + fn assert_err(self) { + assert!(self.result.is_err()) + } + } + + #[tokio::test] + async fn create_one_validator() { + TestBuilder::new() + .await + .create_validators(1, 0) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_three_validators() { + TestBuilder::new() + .await + .create_validators(3, 0) + .await + .run_test() + .await + .assert_ok(); } #[tokio::test] - async fn blah() { - TestBuilder::new().await; + async fn create_one_validator_with_offset() { + TestBuilder::new() + .await + .create_validators(1, 42) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_three_validators_with_offset() { + TestBuilder::new() + .await + .create_validators(3, 1337) + .await + .run_test() + .await + .assert_ok(); } } From 52e50f5fdccbeeebdf057eb3c808dde95a09bb81 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 22 Aug 2022 12:07:27 +1000 Subject: [PATCH 031/138] Progress with testing --- .../src/validators/import_validators.rs | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 9f101891312..82f9fbb6370 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -16,6 +16,8 @@ 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 DETECTED_DUPLICATE_MESSAGE: &str = "Duplicate validator detected!"; + pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about( @@ -175,9 +177,45 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { slashing_protection, }; + // Check to see if this validator already exists on the remote validator. + match http_client.get_keystores().await { + Ok(response) => { + if response + .data + .iter() + .find(|validator| validator.validating_pubkey == voting_public_key) + .is_some() + { + if ignore_duplicates { + eprintln!( + "Duplicate validators are ignored, ignoring {:?} which exists \ + on the validator client and in {:?}", + voting_public_key, validators_file_path + ); + } else { + eprintln!( + "{} {:?} exists on the remote validator client and in {:?}", + DETECTED_DUPLICATE_MESSAGE, voting_public_key, validators_file_path + ); + return Err(DETECTED_DUPLICATE_MESSAGE.to_string()); + } + } + } + Err(e) => { + eprintln!( + "Failed to list keystores during batch {}. Some keys may have been imported whilst \ + others may not have been imported. A potential solution is to use the \ + --{} flag, however care should be taken to ensure that there are no \ + duplicate deposits submitted.", + i, IGNORE_DUPLICATES_FLAG + ); + return Err(format!("Failed to list keys: {:?}", e)); + } + }; + if let Err(e) = http_client.post_keystores(&request).await { eprintln!( - "Failed to upload batch {}. Some keys were imported whilst \ + "Failed to upload batch {}. Some keys may have been imported whilst \ others may not have been imported. A potential solution is to use the \ --{} flag, however care should be taken to ensure that there are no \ duplicate deposits submitted.", @@ -262,6 +300,11 @@ mod test { } } + pub fn mutate_import_config(mut self, func: F) -> Self { + func(&mut self.import_config); + self + } + async fn create_validators(mut self, count: u32, first_index: u32) -> Self { let create_result = CreateTestBuilder::default() .mutate_config(|config| { @@ -279,6 +322,13 @@ mod test { self } + /// Imports validators without running the entire test suite in `Self::run_test`. This is + /// useful for simulating duplicate imports. + async fn import_validators_without_checks(self) -> Self { + run(self.import_config.clone()).await.unwrap(); + self + } + async fn run_test(self) -> TestResult { let result = run(self.import_config.clone()).await; @@ -307,6 +357,10 @@ mod test { // It's not immediately clear why Lighthouse returns `None` rather than // `Some(false)` here, I would expect the latter to be the most accurate. // However, it doesn't seem like a big deal. + // + // See: https://github.com/sigp/lighthouse/pull/3490 + // + // If that PR changes we'll need to change this line. assert_eq!(remote_validator.readonly, None); } } @@ -325,8 +379,8 @@ mod test { assert_eq!(self.result, Ok(())) } - fn assert_err(self) { - assert!(self.result.is_err()) + fn assert_err_is(self, msg: String) { + assert_eq!(self.result, Err(msg)) } } @@ -373,4 +427,33 @@ mod test { .await .assert_ok(); } + + #[tokio::test] + async fn import_duplicates_when_disallowed() { + TestBuilder::new() + .await + .create_validators(1, 0) + .await + .import_validators_without_checks() + .await + .run_test() + .await + .assert_err_is(DETECTED_DUPLICATE_MESSAGE.to_string()); + } + + #[tokio::test] + async fn import_duplicates_when_allowed() { + TestBuilder::new() + .await + .mutate_import_config(|config| { + config.ignore_duplicates = true; + }) + .create_validators(1, 0) + .await + .import_validators_without_checks() + .await + .run_test() + .await + .assert_ok(); + } } From 3bdddb3dd2696d88d269ee378e9121bb8f299d58 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 22 Aug 2022 12:08:01 +1000 Subject: [PATCH 032/138] Fix warning --- validator_manager/src/validators/import_validators.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 82f9fbb6370..d1c237ca5a8 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -273,10 +273,10 @@ mod test { struct TestBuilder { import_config: ImportConfig, vc: ApiTester, - dir: TempDir, /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up /// before we can read it. create_dir: Option, + _dir: TempDir, } impl TestBuilder { @@ -295,8 +295,8 @@ mod test { ignore_duplicates: false, }, vc, - dir, create_dir: None, + _dir: dir, } } From 1d4e7ba6b82bc3248e34ffe0e477ca04e16c6c2e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 22 Aug 2022 12:29:59 +1000 Subject: [PATCH 033/138] Add DumpConfigs --- validator_manager/src/lib.rs | 29 ++++++++++++++++++- .../src/validators/create_validators.rs | 17 ++++++++--- .../src/validators/import_validators.rs | 15 ++++++++-- validator_manager/src/validators/mod.rs | 8 +++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 88bdbd96dd5..79d542e6c2d 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -1,12 +1,36 @@ use clap::App; use clap::ArgMatches; use environment::Environment; +use serde::Serialize; +use std::path::PathBuf; use types::EthSpec; +use validators::create_validators::write_to_json_file; mod validators; pub const CMD: &str = "validator_manager"; +/// This flag is on the top-level `lighthouse` binary. +const DUMP_CONFIGS_FLAG: &str = "dump-configs"; + +/// Used only in testing, this allows a command to dump its configuration to a file and then exit +/// successfully. This allows for testing how the CLI arguments translate to some configuration. +pub enum DumpConfigs { + Disabled, + Enabled(PathBuf), +} + +impl DumpConfigs { + /// Returns `Ok(true)` if the configuration was successfully written to a file and the + /// application should exit successfully without doing anything else. + pub fn should_exit_early(&self, config: &T) -> Result { + match self { + DumpConfigs::Disabled => Ok(false), + DumpConfigs::Enabled(dump_path) => write_to_json_file(dump_path, config).map(|()| true), + } + } +} + pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .visible_aliases(&["vm", CMD]) @@ -21,6 +45,9 @@ pub fn run<'a, T: EthSpec>( ) -> Result<(), String> { let context = env.core_context(); let spec = context.eth2_config.spec.clone(); + let dump_configs = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? + .map(DumpConfigs::Enabled) + .unwrap_or_else(|| DumpConfigs::Disabled); context .executor @@ -31,7 +58,7 @@ pub fn run<'a, T: EthSpec>( async { match matches.subcommand() { (validators::CMD, Some(matches)) => { - validators::cli_run::(matches, &spec).await + validators::cli_run::(matches, &spec, dump_configs).await } (unknown, _) => Err(format!( "{} is not a valid {} command. See --help.", diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 0c5869c4a88..e3e00481ee9 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -1,4 +1,5 @@ use super::common::*; +use crate::DumpConfigs; use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; use clap::{App, Arg, ArgMatches}; use eth2::{ @@ -7,7 +8,7 @@ use eth2::{ BeaconNodeHttpClient, SensitiveUrl, Timeouts, }; use eth2_wallet::WalletBuilder; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -180,7 +181,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /// The CLI arguments are parsed into this struct before running the application. This step of /// indirection allows for testing the underlying logic without needing to parse CLI arguments. -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct CreateConfig { pub output_path: PathBuf, pub first_index: u32, @@ -460,9 +461,14 @@ impl ValidatorsAndDeposits { pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, + dump_configs: DumpConfigs, ) -> Result<(), String> { let config = CreateConfig::from_cli(matches, spec)?; - run::(config, spec).await + if dump_configs.should_exit_early(&config)? { + Ok(()) + } else { + run::(config, spec).await + } } async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<(), String> { @@ -506,7 +512,10 @@ async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<( /// Write some object to a file as JSON. /// /// The file must be created new, it must not already exist. -fn write_to_json_file, S: Serialize>(path: P, contents: &S) -> Result<(), String> { +pub fn write_to_json_file, S: Serialize>( + path: P, + contents: &S, +) -> Result<(), String> { eprintln!("Writing {:?}", path.as_ref()); let mut file = fs::OpenOptions::new() .write(true) diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index d1c237ca5a8..418b561b6cc 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -1,4 +1,5 @@ use super::common::*; +use crate::DumpConfigs; use clap::{App, Arg, ArgMatches}; use eth2::{ lighthouse_vc::{ @@ -7,6 +8,7 @@ use eth2::{ }, SensitiveUrl, }; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -72,7 +74,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -#[derive(Clone)] +#[derive(Clone, Serialize, Deserialize)] struct ImportConfig { validators_file_path: PathBuf, vc_url: SensitiveUrl, @@ -91,9 +93,16 @@ impl ImportConfig { } } -pub async fn cli_run<'a>(matches: &'a ArgMatches<'a>) -> Result<(), String> { +pub async fn cli_run<'a>( + matches: &'a ArgMatches<'a>, + dump_configs: DumpConfigs, +) -> Result<(), String> { let config = ImportConfig::from_cli(matches)?; - run(config).await + if dump_configs.should_exit_early(&config)? { + Ok(()) + } else { + run(config).await + } } async fn run<'a>(config: ImportConfig) -> Result<(), String> { diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs index 04ff692401a..1b6b769378c 100644 --- a/validator_manager/src/validators/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -2,6 +2,7 @@ pub mod common; pub mod create_validators; pub mod import_validators; +use crate::DumpConfigs; use clap::{App, ArgMatches}; use types::{ChainSpec, EthSpec}; @@ -17,12 +18,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, + dump_configs: DumpConfigs, ) -> Result<(), String> { match matches.subcommand() { (create_validators::CMD, Some(matches)) => { - create_validators::cli_run::(matches, spec).await + create_validators::cli_run::(matches, spec, dump_configs).await + } + (import_validators::CMD, Some(matches)) => { + import_validators::cli_run(matches, dump_configs).await } - (import_validators::CMD, Some(matches)) => import_validators::cli_run(matches).await, (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown From 4886e278279f016b7a4f9d83bcf53bc98ae0c2d3 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 22 Aug 2022 15:29:11 +1000 Subject: [PATCH 034/138] Start adding CLI tests --- lighthouse/tests/main.rs | 1 + lighthouse/tests/validator_manager.rs | 63 +++++++++++++++++++ validator_manager/src/lib.rs | 2 +- .../src/validators/create_validators.rs | 2 +- 4 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 lighthouse/tests/validator_manager.rs diff --git a/lighthouse/tests/main.rs b/lighthouse/tests/main.rs index 806524cab05..bf587f79df7 100644 --- a/lighthouse/tests/main.rs +++ b/lighthouse/tests/main.rs @@ -5,3 +5,4 @@ mod beacon_node; mod boot_node; mod exec; mod validator_client; +mod validator_manager; diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs new file mode 100644 index 00000000000..d96f4c1a976 --- /dev/null +++ b/lighthouse/tests/validator_manager.rs @@ -0,0 +1,63 @@ +use serde::de::DeserializeOwned; +use std::fs; +use std::marker::PhantomData; +use std::path::PathBuf; +use std::process::Command; +use tempfile::{tempdir, TempDir}; +use validator_manager::validators::create_validators::CreateConfig; + +struct CommandLineTest { + cmd: Command, + dir: TempDir, + config_path: PathBuf, + _phantom: PhantomData, +} + +impl Default for CommandLineTest { + fn default() -> Self { + let dir = tempdir().unwrap(); + let config_path = dir.path().join("config.json"); + let mut cmd = Command::new(env!("CARGO_BIN_EXE_lighthouse")); + cmd.arg("--dump_config") + .arg(format!("{:?}", config_path)) + .arg("validator-manager"); + Self { + cmd, + dir, + config_path, + _phantom: PhantomData, + } + } +} + +impl CommandLineTest { + fn flag(mut self, flag: &str, value: Option<&str>) -> Self { + self.cmd.arg(flag); + if let Some(value) = value { + self.cmd.arg(value); + } + self + } +} + +impl CommandLineTest { + fn assert_success(mut self, func: F) { + let output = self.cmd.output().expect("should run command"); + assert!(output.status.success(), "command should succeed"); + + let contents = fs::read_to_string(self.config_path).unwrap(); + let config: T = serde_json::from_str(&contents).unwrap(); + func(config) + } +} + +impl CommandLineTest { + fn validator_create() -> Self { + Self::default().flag("validator", None).flag("create", None) + } +} + +#[test] +pub fn validator_create_defaults() { + todo!() +} diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 79d542e6c2d..439db1449bb 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use types::EthSpec; use validators::create_validators::write_to_json_file; -mod validators; +pub mod validators; pub const CMD: &str = "validator_manager"; diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index e3e00481ee9..ee73d02d2de 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -181,7 +181,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /// The CLI arguments are parsed into this struct before running the application. This step of /// indirection allows for testing the underlying logic without needing to parse CLI arguments. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct CreateConfig { pub output_path: PathBuf, pub first_index: u32, From 5b86fce56b983b4060f39e0b443b90747517c80b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 23 Aug 2022 12:09:02 +1000 Subject: [PATCH 035/138] Add tests for validator create --- Cargo.lock | 1 + lighthouse/Cargo.toml | 1 + lighthouse/tests/validator_manager.rs | 121 ++++++++++++++++-- validator_manager/src/lib.rs | 24 ++-- .../src/validators/create_validators.rs | 12 +- .../src/validators/import_validators.rs | 6 +- validator_manager/src/validators/mod.rs | 8 +- 7 files changed, 136 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ed823ccd0d..6740ba226d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3626,6 +3626,7 @@ dependencies = [ "env_logger 0.9.0", "environment", "eth1", + "eth2", "eth2_hashing", "eth2_network_config", "futures", diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 3b0416cf0f1..a9d49c59a5b 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -63,6 +63,7 @@ slashing_protection = { path = "../validator_client/slashing_protection" } lighthouse_network = { path = "../beacon_node/lighthouse_network" } sensitive_url = { path = "../common/sensitive_url" } eth1 = { path = "../beacon_node/eth1" } +eth2 = { path = "../common/eth2" } [[test]] name = "lighthouse_tests" diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index d96f4c1a976..48eaa1a689f 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -1,15 +1,20 @@ +use eth2::SensitiveUrl; use serde::de::DeserializeOwned; use std::fs; use std::marker::PhantomData; use std::path::PathBuf; -use std::process::Command; +use std::process::{Command, Stdio}; +use std::str::FromStr; use tempfile::{tempdir, TempDir}; +use types::*; use validator_manager::validators::create_validators::CreateConfig; +const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa"; + struct CommandLineTest { cmd: Command, - dir: TempDir, config_path: PathBuf, + _dir: TempDir, _phantom: PhantomData, } @@ -18,13 +23,16 @@ impl Default for CommandLineTest { let dir = tempdir().unwrap(); let config_path = dir.path().join("config.json"); let mut cmd = Command::new(env!("CARGO_BIN_EXE_lighthouse")); - cmd.arg("--dump_config") - .arg(format!("{:?}", config_path)) - .arg("validator-manager"); + cmd.arg("--dump-config") + .arg(config_path.as_os_str()) + .arg("validator-manager") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); Self { cmd, - dir, config_path, + _dir: dir, _phantom: PhantomData, } } @@ -38,26 +46,115 @@ impl CommandLineTest { } self } + + fn run(mut cmd: Command, should_succeed: bool) { + let output = cmd.output().expect("process should complete"); + if output.status.success() != should_succeed { + let stdout = String::from_utf8(output.stdout).unwrap(); + let stderr = String::from_utf8(output.stderr).unwrap(); + eprintln!("{}", stdout); + eprintln!("{}", stderr); + panic!( + "Command success was {} when expecting {}", + !should_succeed, should_succeed + ); + } + } } impl CommandLineTest { - fn assert_success(mut self, func: F) { - let output = self.cmd.output().expect("should run command"); - assert!(output.status.success(), "command should succeed"); - + fn assert_success(self, func: F) { + Self::run(self.cmd, true); let contents = fs::read_to_string(self.config_path).unwrap(); let config: T = serde_json::from_str(&contents).unwrap(); func(config) } + + fn assert_failed(self) { + Self::run(self.cmd, false); + } } impl CommandLineTest { fn validator_create() -> Self { - Self::default().flag("validator", None).flag("create", None) + Self::default() + .flag("validators", None) + .flag("create", None) } } +#[test] +pub fn validator_create_without_output_path() { + CommandLineTest::validator_create().assert_failed(); +} + #[test] pub fn validator_create_defaults() { - todo!() + CommandLineTest::validator_create() + .flag("--output-path", Some("./meow")) + .flag("--count", Some("1")) + .assert_success(|config| { + let expected = CreateConfig { + output_path: PathBuf::from("./meow"), + first_index: 0, + count: 1, + deposit_gwei: MainnetEthSpec::default_spec().max_effective_balance, + mnemonic_path: None, + stdin_inputs: false, + disable_deposits: false, + specify_voting_keystore_password: false, + eth1_withdrawal_address: None, + builder_proposals: false, + fee_recipient: None, + gas_limit: None, + bn_url: None, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_create_misc_flags_01() { + CommandLineTest::validator_create() + .flag("--output-path", Some("./meow")) + .flag("--deposit-gwei", Some("42")) + .flag("--first-index", Some("12")) + .flag("--count", Some("9")) + .flag("--mnemonic-path", Some("./woof")) + .flag("--stdin-inputs", None) + .flag("--specify-voting-keystore-password", None) + .flag("--eth1-withdrawal-address", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--builder-proposals", None) + .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--gas-limit", Some("1337")) + .flag("--beacon-node", Some("http://localhost:1001")) + .assert_success(|config| { + let expected = CreateConfig { + output_path: PathBuf::from("./meow"), + first_index: 12, + count: 9, + deposit_gwei: 42, + mnemonic_path: Some(PathBuf::from("./woof")), + stdin_inputs: true, + disable_deposits: false, + specify_voting_keystore_password: true, + eth1_withdrawal_address: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + builder_proposals: true, + fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + gas_limit: Some(1337), + bn_url: Some(SensitiveUrl::parse("http://localhost:1001").unwrap()), + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_create_disable_deposits() { + CommandLineTest::validator_create() + .flag("--output-path", Some("./meow")) + .flag("--count", Some("1")) + .flag("--disable-deposits", None) + .assert_success(|config| { + assert_eq!(config.disable_deposits, true); + }); } diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 439db1449bb..4b52b8edb39 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -11,29 +11,33 @@ pub mod validators; pub const CMD: &str = "validator_manager"; /// This flag is on the top-level `lighthouse` binary. -const DUMP_CONFIGS_FLAG: &str = "dump-configs"; +const DUMP_CONFIGS_FLAG: &str = "dump-config"; /// Used only in testing, this allows a command to dump its configuration to a file and then exit /// successfully. This allows for testing how the CLI arguments translate to some configuration. -pub enum DumpConfigs { +pub enum DumpConfig { Disabled, Enabled(PathBuf), } -impl DumpConfigs { +impl DumpConfig { /// Returns `Ok(true)` if the configuration was successfully written to a file and the /// application should exit successfully without doing anything else. pub fn should_exit_early(&self, config: &T) -> Result { match self { - DumpConfigs::Disabled => Ok(false), - DumpConfigs::Enabled(dump_path) => write_to_json_file(dump_path, config).map(|()| true), + DumpConfig::Disabled => Ok(false), + DumpConfig::Enabled(dump_path) => { + dbg!(dump_path); + write_to_json_file(dump_path, config)?; + Ok(true) + } } } } pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) - .visible_aliases(&["vm", CMD]) + .visible_aliases(&["vm", "validator-manager", CMD]) .about("Utilities for managing a Lighthouse validator client via the HTTP API.") .subcommand(validators::cli_app()) } @@ -45,9 +49,9 @@ pub fn run<'a, T: EthSpec>( ) -> Result<(), String> { let context = env.core_context(); let spec = context.eth2_config.spec.clone(); - let dump_configs = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? - .map(DumpConfigs::Enabled) - .unwrap_or_else(|| DumpConfigs::Disabled); + let dump_config = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? + .map(DumpConfig::Enabled) + .unwrap_or_else(|| DumpConfig::Disabled); context .executor @@ -58,7 +62,7 @@ pub fn run<'a, T: EthSpec>( async { match matches.subcommand() { (validators::CMD, Some(matches)) => { - validators::cli_run::(matches, &spec, dump_configs).await + validators::cli_run::(matches, &spec, dump_config).await } (unknown, _) => Err(format!( "{} is not a valid {} command. See --help.", diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index ee73d02d2de..e2a0ec8f7a6 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -1,5 +1,5 @@ use super::common::*; -use crate::DumpConfigs; +use crate::DumpConfig; use account_utils::{random_password_string, read_mnemonic_from_cli, read_password_from_user}; use clap::{App, Arg, ArgMatches}; use eth2::{ @@ -51,7 +51,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { "The path to a directory where the validator and (optionally) deposits \ files will be created. The directory will be created if it does not exist.", ) - .conflicts_with(DISABLE_DEPOSITS_FLAG) .required(true) .takes_value(true), ) @@ -100,7 +99,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name(DISABLE_DEPOSITS_FLAG) .long(DISABLE_DEPOSITS_FLAG) - .value_name("PATH") .help( "When provided don't generate the deposits JSON file that is \ commonly used for submitting validator deposits via a web UI. \ @@ -111,8 +109,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) .long(SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG) - .value_name("STRING") - .takes_value(true) .help( "If present, the user will be prompted to enter the voting keystore \ password that will be used to encrypt the voting keystores. If this \ @@ -181,7 +177,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /// The CLI arguments are parsed into this struct before running the application. This step of /// indirection allows for testing the underlying logic without needing to parse CLI arguments. -#[derive(Clone, PartialEq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct CreateConfig { pub output_path: PathBuf, pub first_index: u32, @@ -461,10 +457,10 @@ impl ValidatorsAndDeposits { pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, - dump_configs: DumpConfigs, + dump_config: DumpConfig, ) -> Result<(), String> { let config = CreateConfig::from_cli(matches, spec)?; - if dump_configs.should_exit_early(&config)? { + if dump_config.should_exit_early(&config)? { Ok(()) } else { run::(config, spec).await diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 418b561b6cc..851ee61ff3a 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -1,5 +1,5 @@ use super::common::*; -use crate::DumpConfigs; +use crate::DumpConfig; use clap::{App, Arg, ArgMatches}; use eth2::{ lighthouse_vc::{ @@ -95,10 +95,10 @@ impl ImportConfig { pub async fn cli_run<'a>( matches: &'a ArgMatches<'a>, - dump_configs: DumpConfigs, + dump_config: DumpConfig, ) -> Result<(), String> { let config = ImportConfig::from_cli(matches)?; - if dump_configs.should_exit_early(&config)? { + if dump_config.should_exit_early(&config)? { Ok(()) } else { run(config).await diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs index 1b6b769378c..4933c87746a 100644 --- a/validator_manager/src/validators/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -2,7 +2,7 @@ pub mod common; pub mod create_validators; pub mod import_validators; -use crate::DumpConfigs; +use crate::DumpConfig; use clap::{App, ArgMatches}; use types::{ChainSpec, EthSpec}; @@ -18,14 +18,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { pub async fn cli_run<'a, T: EthSpec>( matches: &'a ArgMatches<'a>, spec: &ChainSpec, - dump_configs: DumpConfigs, + dump_config: DumpConfig, ) -> Result<(), String> { match matches.subcommand() { (create_validators::CMD, Some(matches)) => { - create_validators::cli_run::(matches, spec, dump_configs).await + create_validators::cli_run::(matches, spec, dump_config).await } (import_validators::CMD, Some(matches)) => { - import_validators::cli_run(matches, dump_configs).await + import_validators::cli_run(matches, dump_config).await } (unknown, _) => Err(format!( "{} does not have a {} command. See --help", From 630cb9123690ee68d3db4113e1811a523237c661 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 23 Aug 2022 12:34:09 +1000 Subject: [PATCH 036/138] Add import tests --- lighthouse/tests/validator_manager.rs | 71 +++++++++++++++++-- .../src/validators/import_validators.rs | 12 ++-- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 48eaa1a689f..a0737032d43 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -7,7 +7,9 @@ use std::process::{Command, Stdio}; use std::str::FromStr; use tempfile::{tempdir, TempDir}; use types::*; -use validator_manager::validators::create_validators::CreateConfig; +use validator_manager::validators::{ + create_validators::CreateConfig, import_validators::ImportConfig, +}; const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa"; @@ -76,21 +78,29 @@ impl CommandLineTest { } impl CommandLineTest { - fn validator_create() -> Self { + fn validators_create() -> Self { Self::default() .flag("validators", None) .flag("create", None) } } +impl CommandLineTest { + fn validators_import() -> Self { + Self::default() + .flag("validators", None) + .flag("import", None) + } +} + #[test] pub fn validator_create_without_output_path() { - CommandLineTest::validator_create().assert_failed(); + CommandLineTest::validators_create().assert_failed(); } #[test] pub fn validator_create_defaults() { - CommandLineTest::validator_create() + CommandLineTest::validators_create() .flag("--output-path", Some("./meow")) .flag("--count", Some("1")) .assert_success(|config| { @@ -114,8 +124,8 @@ pub fn validator_create_defaults() { } #[test] -pub fn validator_create_misc_flags_01() { - CommandLineTest::validator_create() +pub fn validator_create_misc_flags() { + CommandLineTest::validators_create() .flag("--output-path", Some("./meow")) .flag("--deposit-gwei", Some("42")) .flag("--first-index", Some("12")) @@ -150,7 +160,7 @@ pub fn validator_create_misc_flags_01() { #[test] pub fn validator_create_disable_deposits() { - CommandLineTest::validator_create() + CommandLineTest::validators_create() .flag("--output-path", Some("./meow")) .flag("--count", Some("1")) .flag("--disable-deposits", None) @@ -158,3 +168,50 @@ pub fn validator_create_disable_deposits() { assert_eq!(config.disable_deposits, true); }); } + +#[test] +pub fn validator_import_defaults() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .flag("--validator-client-token", Some("./token.json")) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: PathBuf::from("./vals.json"), + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: false, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_import_misc_flags() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .flag("--validator-client-token", Some("./token.json")) + .flag("--ignore-duplicates", None) + .assert_success(|config| { + let expected = ImportConfig { + validators_file_path: PathBuf::from("./vals.json"), + vc_url: SensitiveUrl::parse("http://localhost:5062").unwrap(), + vc_token_path: PathBuf::from("./token.json"), + ignore_duplicates: true, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_import_missing_token() { + CommandLineTest::validators_import() + .flag("--validators-file", Some("./vals.json")) + .assert_failed(); +} + +#[test] +pub fn validator_import_missing_validators_file() { + CommandLineTest::validators_import() + .flag("--validator-client-token", Some("./token.json")) + .assert_failed(); +} diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 851ee61ff3a..e96fefee34f 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -74,12 +74,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) } -#[derive(Clone, Serialize, Deserialize)] -struct ImportConfig { - validators_file_path: PathBuf, - vc_url: SensitiveUrl, - vc_token_path: PathBuf, - ignore_duplicates: bool, +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct ImportConfig { + pub validators_file_path: PathBuf, + pub vc_url: SensitiveUrl, + pub vc_token_path: PathBuf, + pub ignore_duplicates: bool, } impl ImportConfig { From bcbafc07bda14debf54349ec3c43abba88ff4a56 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 23 Aug 2022 17:31:01 +1000 Subject: [PATCH 037/138] Start adding validator move --- validator_manager/src/validators/common.rs | 40 +- .../src/validators/import_validators.rs | 32 +- validator_manager/src/validators/mod.rs | 1 + .../src/validators/move_validators.rs | 473 ++++++++++++++++++ 4 files changed, 514 insertions(+), 32 deletions(-) create mode 100644 validator_manager/src/validators/move_validators.rs diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index 0b4743aaf5d..9e3d279c027 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -1,8 +1,12 @@ use account_utils::ZeroizeString; use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; -use eth2::SensitiveUrl; +use eth2::{ + lighthouse_vc::{http_client::ValidatorClientHttpClient, std_types::SingleKeystoreResponse}, + SensitiveUrl, +}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; use tree_hash::TreeHash; use types::*; @@ -189,3 +193,35 @@ mod bytes_4_without_0x_prefix { Ok(array) } } + +pub async fn vc_http_client>( + url: SensitiveUrl, + token_path: P, +) -> Result<(ValidatorClientHttpClient, Vec), String> { + let token_path = token_path.as_ref(); + let token_bytes = + fs::read(&token_path).map_err(|e| format!("Failed to read {:?}: {:?}", token_path, e))?; + let token_string = String::from_utf8(token_bytes) + .map_err(|e| format!("Failed to parse {:?} as utf8: {:?}", token_path, e))?; + let http_client = ValidatorClientHttpClient::new(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))? + .data; + + eprintln!( + "Validator client is reachable at {} and reports {} validators", + url, + remote_keystores.len() + ); + + Ok((http_client, remote_keystores)) +} diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index e96fefee34f..4d2c2a65309 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -2,10 +2,7 @@ use super::common::*; use crate::DumpConfig; use clap::{App, Arg, ArgMatches}; use eth2::{ - lighthouse_vc::{ - http_client::ValidatorClientHttpClient, std_types::ImportKeystoresRequest, - types::UpdateFeeRecipientRequest, - }, + lighthouse_vc::{std_types::ImportKeystoresRequest, types::UpdateFeeRecipientRequest}, SensitiveUrl, }; use serde::{Deserialize, Serialize}; @@ -132,32 +129,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { let count = validators.len(); - let http_client = { - 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() - ); - - http_client - }; + let (http_client, _keystores) = vc_http_client(vc_url.clone(), &vc_token_path).await?; eprintln!( "Starting to submit validators {} to VC, each validator may take several seconds", diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs index 4933c87746a..4e502647da6 100644 --- a/validator_manager/src/validators/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -1,6 +1,7 @@ pub mod common; pub mod create_validators; pub mod import_validators; +pub mod move_validators; use crate::DumpConfig; use clap::{App, ArgMatches}; diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs new file mode 100644 index 00000000000..49a379d8da4 --- /dev/null +++ b/validator_manager/src/validators/move_validators.rs @@ -0,0 +1,473 @@ +use super::common::*; +use crate::DumpConfig; +use clap::{App, Arg, ArgMatches}; +use eth2::{ + lighthouse_vc::{ + http_client::ValidatorClientHttpClient, + std_types::{DeleteKeystoresRequest, ImportKeystoresRequest}, + types::UpdateFeeRecipientRequest, + }, + SensitiveUrl, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; +use types::PublicKeyBytes; + +pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; + +pub const CMD: &str = "move"; +pub const WORKING_DIRECTORY_FLAG: &str = "working-directory"; +pub const SRC_VALIDATOR_CLIENT_URL_FLAG: &str = "src-validator-client-url"; +pub const SRC_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "src-validator-client-token"; +pub const DEST_VALIDATOR_CLIENT_URL_FLAG: &str = "dest-validator-client-url"; +pub const DEST_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "dest-validator-client-token"; +pub const VALIDATORS_FLAG: &str = "validators"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new(CMD) + .about( + "Uploads validators to a validator client using the HTTP API. The validators \ + are defined in a JSON file which can be generated using the \"create-validators\" \ + command.", + ) + .arg( + Arg::with_name(WORKING_DIRECTORY_FLAG) + .long(WORKING_DIRECTORY_FLAG) + .value_name("PATH_TO_DIRECTORY") + .help( + "The path to a directory where the application can write files.\ + Under certain failure scenarios this directory may contain files which \ + can be used to recover validators.", + ) + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(SRC_VALIDATOR_CLIENT_URL_FLAG) + .long(SRC_VALIDATOR_CLIENT_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a validator client using the keymanager-API. \ + This validator client is the \"source\" and contains the validators \ + that are to be moved.", + ) + .required(true) + .requires(SRC_VALIDATOR_CLIENT_TOKEN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(SRC_VALIDATOR_CLIENT_TOKEN_FLAG) + .long(SRC_VALIDATOR_CLIENT_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the source validator client.") + .takes_value(true), + ) + .arg( + Arg::with_name(DEST_VALIDATOR_CLIENT_URL_FLAG) + .long(DEST_VALIDATOR_CLIENT_URL_FLAG) + .value_name("HTTP_ADDRESS") + .help( + "A HTTP(S) address of a validator client using the keymanager-API. \ + This validator client is the \"destination\" and will have new validators \ + added as they are removed from the \"source\" validator client.", + ) + .required(true) + .requires(DEST_VALIDATOR_CLIENT_TOKEN_FLAG) + .takes_value(true), + ) + .arg( + Arg::with_name(DEST_VALIDATOR_CLIENT_TOKEN_FLAG) + .long(DEST_VALIDATOR_CLIENT_TOKEN_FLAG) + .value_name("PATH") + .help("The file containing a token required by the destination validator client.") + .takes_value(true), + ) + .arg( + Arg::with_name(VALIDATORS_FLAG) + .long(VALIDATORS_FLAG) + .value_name("STRING") + .help( + "One or more validator public keys (as 0x-prefixed hex) to be moved from \ + the source to destination validator clients. Alternatively, use \"all\" to \ + move all the validators from the source validator client.", + ) + .required(true) + .takes_value(true), + ) +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum Validators { + All, + Some(Vec), +} + +impl FromStr for Validators { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "all" => Ok(Validators::All), + other => other + .split(',') + .map(PublicKeyBytes::from_str) + .collect::>() + .map(Validators::Some), + } + } +} + +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub struct MoveConfig { + pub working_directory_path: PathBuf, + pub src_vc_url: SensitiveUrl, + pub src_vc_token_path: PathBuf, + pub dest_vc_url: SensitiveUrl, + pub dest_vc_token_path: PathBuf, + pub validators: Validators, +} + +impl MoveConfig { + fn from_cli(matches: &ArgMatches) -> Result { + Ok(Self { + working_directory_path: clap_utils::parse_required(matches, WORKING_DIRECTORY_FLAG)?, + src_vc_url: clap_utils::parse_required(matches, SRC_VALIDATOR_CLIENT_URL_FLAG)?, + src_vc_token_path: clap_utils::parse_required( + matches, + SRC_VALIDATOR_CLIENT_TOKEN_FLAG, + )?, + dest_vc_url: clap_utils::parse_required(matches, DEST_VALIDATOR_CLIENT_URL_FLAG)?, + dest_vc_token_path: clap_utils::parse_required( + matches, + DEST_VALIDATOR_CLIENT_TOKEN_FLAG, + )?, + validators: clap_utils::parse_required(matches, VALIDATORS_FLAG)?, + }) + } +} + +pub async fn cli_run<'a>( + matches: &'a ArgMatches<'a>, + dump_config: DumpConfig, +) -> Result<(), String> { + let config = MoveConfig::from_cli(matches)?; + if dump_config.should_exit_early(&config)? { + Ok(()) + } else { + run(config).await + } +} + +async fn run<'a>(config: MoveConfig) -> Result<(), String> { + let MoveConfig { + working_directory_path, + src_vc_url, + src_vc_token_path, + dest_vc_url, + dest_vc_token_path, + validators, + } = config; + + if !working_directory_path.exists() { + return Err(format!("{:?} does not exist", working_directory_path)); + } + + // Append another directory to the "working directory" provided by the user. By creating a new + // directory we can prove (to some degree) that we can write in the given directory. + // + // It also allows us to easily detect when another identical process is running or the previous + // run failed by checking to see if the directory already exists. + let working_directory_path = working_directory_path.join(MOVE_DIR_NAME); + if working_directory_path.exists() { + return Err(format!( + "{:?} already exists, exiting", + working_directory_path + )); + } + + fs::create_dir(&working_directory_path) + .map_err(|e| format!("Failed to create {:?}: {:?}", working_directory_path, e))?; + + // Moving validators between the same VC is unlikely to be useful and probably indicates a user + // error. + if src_vc_url == dest_vc_url { + return Err(format!( + "--{} and --{} must be different", + SRC_VALIDATOR_CLIENT_URL_FLAG, DEST_VALIDATOR_CLIENT_URL_FLAG + )); + } + + let (src_http_client, src_keystores) = + vc_http_client(src_vc_url.clone(), &src_vc_token_path).await?; + let (dest_http_client, dest_keystores) = + vc_http_client(dest_vc_url.clone(), &dest_vc_token_path).await?; + + let pubkeys_to_move = match validators { + Validators::All => src_keystores.iter().map(|v| v.validating_pubkey).collect(), + Validators::Some(request_pubkeys) => { + let request_pubkeys_set: HashSet<_> = request_pubkeys.iter().collect(); + let src_pubkeys_set: HashSet<_> = + src_keystores.iter().map(|v| &v.validating_pubkey).collect(); + let difference = request_pubkeys_set + .difference(&src_pubkeys_set) + .collect::>(); + if !difference.is_empty() { + for pk in &difference { + eprintln!("{:?} is not present on {:?}", pk, src_vc_url); + } + return Err(format!( + "{} validators not found on {:?}", + difference.len(), + src_vc_url + )); + } + request_pubkeys + } + }; + + // It doesn't make sense to move no validators. + if pubkeys_to_move.is_empty() { + return Err(format!("--{} cannot be an empty list", VALIDATORS_FLAG)); + } + + for pubkey_to_move in pubkeys_to_move { + let request = DeleteKeystoresRequest { + pubkeys: vec![pubkey_to_move], + }; + let deleted_keystore = match src_http_client.delete_keystores(&request).await { + Ok(deleted) => deleted, + Err(e) => { + match src_http_client.get_keystores().await { + Ok(response) => { + if response + .data + .iter() + .find(|v| v.validating_pubkey == pubkey_to_move) + .is_some() + { + eprintln!( + "There was an error removing a validator, however the validator \ + is still present on the source validator client. The recommended \ + solution is to run this command again." + ); + } + } + Err(_) => { + eprintln!( + "There was an error removing a validator and it's unclear if \ + the validator was removed or not. Manual user intervention is \ + required." + ); + } + }; + + return Err(format!("Deleting {:?} failed with {:?}", pubkey_to_move, e)); + } + }; + } + + Ok(()) +} + +/* +#[cfg(test)] +mod test { + use super::*; + use crate::validators::create_validators::tests::TestBuilder as CreateTestBuilder; + use std::fs; + use tempfile::{tempdir, TempDir}; + use validator_client::http_api::test_utils::ApiTester; + + const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; + + struct TestBuilder { + import_config: MoveConfig, + vc: ApiTester, + /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up + /// before we can read it. + create_dir: Option, + _dir: TempDir, + } + + impl TestBuilder { + async fn new() -> Self { + let dir = tempdir().unwrap(); + let vc = ApiTester::new().await; + let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); + fs::write(&vc_token_path, &vc.api_token).unwrap(); + + Self { + import_config: MoveConfig { + // This field will be overwritten later on. + validators_file_path: dir.path().into(), + vc_url: vc.url.clone(), + vc_token_path, + ignore_duplicates: false, + }, + vc, + create_dir: None, + _dir: dir, + } + } + + pub fn mutate_import_config(mut self, func: F) -> Self { + func(&mut self.import_config); + self + } + + async fn create_validators(mut self, count: u32, first_index: u32) -> Self { + let create_result = CreateTestBuilder::default() + .mutate_config(|config| { + config.count = count; + config.first_index = first_index; + }) + .run_test() + .await; + assert!( + create_result.result.is_ok(), + "precondition: validators are created" + ); + self.import_config.validators_file_path = create_result.validators_file_path(); + self.create_dir = Some(create_result.output_dir); + self + } + + /// Imports validators without running the entire test suite in `Self::run_test`. This is + /// useful for simulating duplicate imports. + async fn import_validators_without_checks(self) -> Self { + run(self.import_config.clone()).await.unwrap(); + self + } + + async fn run_test(self) -> TestResult { + let result = run(self.import_config.clone()).await; + + if result.is_ok() { + let local_validators: Vec = { + let contents = + fs::read_to_string(&self.import_config.validators_file_path).unwrap(); + serde_json::from_str(&contents).unwrap() + }; + let list_keystores_response = self.vc.client.get_keystores().await.unwrap().data; + + assert_eq!( + local_validators.len(), + list_keystores_response.len(), + "vc should have exactly the number of validators imported" + ); + + for local_validator in &local_validators { + let local_keystore = &local_validator.voting_keystore.0; + let local_pubkey = local_keystore.public_key().unwrap().into(); + let remote_validator = list_keystores_response + .iter() + .find(|validator| validator.validating_pubkey == local_pubkey) + .expect("validator must exist on VC"); + assert_eq!(&remote_validator.derivation_path, &local_keystore.path()); + // It's not immediately clear why Lighthouse returns `None` rather than + // `Some(false)` here, I would expect the latter to be the most accurate. + // However, it doesn't seem like a big deal. + // + // See: https://github.com/sigp/lighthouse/pull/3490 + // + // If that PR changes we'll need to change this line. + assert_eq!(remote_validator.readonly, None); + } + } + + TestResult { result } + } + } + + #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. + struct TestResult { + result: Result<(), String>, + } + + impl TestResult { + fn assert_ok(self) { + assert_eq!(self.result, Ok(())) + } + + fn assert_err_is(self, msg: String) { + assert_eq!(self.result, Err(msg)) + } + } + + #[tokio::test] + async fn create_one_validator() { + TestBuilder::new() + .await + .create_validators(1, 0) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_three_validators() { + TestBuilder::new() + .await + .create_validators(3, 0) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_one_validator_with_offset() { + TestBuilder::new() + .await + .create_validators(1, 42) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn create_three_validators_with_offset() { + TestBuilder::new() + .await + .create_validators(3, 1337) + .await + .run_test() + .await + .assert_ok(); + } + + #[tokio::test] + async fn import_duplicates_when_disallowed() { + TestBuilder::new() + .await + .create_validators(1, 0) + .await + .import_validators_without_checks() + .await + .run_test() + .await + .assert_err_is(DETECTED_DUPLICATE_MESSAGE.to_string()); + } + + #[tokio::test] + async fn import_duplicates_when_allowed() { + TestBuilder::new() + .await + .mutate_import_config(|config| { + config.ignore_duplicates = true; + }) + .create_validators(1, 0) + .await + .import_validators_without_checks() + .await + .run_test() + .await + .assert_ok(); + } +} +*/ From c0e9087c4b24e22689601b3f7fb73dfc24da760a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 23 Aug 2022 17:33:02 +1000 Subject: [PATCH 038/138] Add export endpoint to LH --- .../src/validator_definitions.rs | 24 ++++++- common/eth2/src/lighthouse_vc/http_client.rs | 15 +++++ common/eth2/src/lighthouse_vc/types.rs | 16 +++++ validator_client/src/http_api/keystores.rs | 67 ++++++++++++++----- validator_client/src/http_api/mod.rs | 29 +++++++- validator_client/src/http_api/test_utils.rs | 1 + .../src/initialized_validators.rs | 56 +++++++++++----- 7 files changed, 176 insertions(+), 32 deletions(-) diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 66e3b735473..579195299ce 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -3,7 +3,9 @@ //! Serves as the source-of-truth of which validators this validator client should attempt (or not //! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct. -use crate::{default_keystore_password_path, write_file_via_temporary, ZeroizeString}; +use crate::{ + default_keystore_password_path, read_password_string, write_file_via_temporary, ZeroizeString, +}; use directory::ensure_dir_exists; use eth2_keystore::Keystore; use regex::Regex; @@ -43,6 +45,8 @@ pub enum Error { UnableToOpenKeystore(eth2_keystore::Error), /// The validator directory could not be created. UnableToCreateValidatorDir(PathBuf), + UnableToReadKeystorePassword(String), + KeystoreWithoutPassword, } #[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] @@ -92,6 +96,24 @@ impl SigningDefinition { pub fn is_local_keystore(&self) -> bool { matches!(self, SigningDefinition::LocalKeystore { .. }) } + + pub fn voting_keystore_password(&self) -> Result, Error> { + match self { + SigningDefinition::LocalKeystore { + voting_keystore_password: Some(password), + .. + } => Ok(Some(password.clone())), + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => read_password_string(path) + .map(Into::into) + .map(Option::Some) + .map_err(Error::UnableToReadKeystorePassword), + SigningDefinition::LocalKeystore { .. } => Err(Error::KeystoreWithoutPassword), + SigningDefinition::Web3Signer(_) => Ok(None), + } + } } /// A validator that may be initialized by this validator client. diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index 88b5b684019..a2e3e3f6ff9 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -487,6 +487,21 @@ impl ValidatorClientHttpClient { .await } + /// `DELETE eth/v1/keystores` + pub async fn delete_lighthouse_keystores( + &self, + req: &DeleteKeystoresRequest, + ) -> Result { + let mut path = self.server.full.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("keystores"); + + self.delete_with_unsigned_response(path, req).await + } + fn make_keystores_url(&self) -> Result { let mut url = self.server.full.clone(); url.path_segments_mut() diff --git a/common/eth2/src/lighthouse_vc/types.rs b/common/eth2/src/lighthouse_vc/types.rs index 92439337f61..2d9f01c292a 100644 --- a/common/eth2/src/lighthouse_vc/types.rs +++ b/common/eth2/src/lighthouse_vc/types.rs @@ -144,3 +144,19 @@ pub struct UpdateGasLimitRequest { #[serde(with = "eth2_serde_utils::quoted_u64")] pub gas_limit: u64, } + +#[derive(Deserialize, Serialize)] +pub struct ExportKeystoresResponse { + pub data: Vec, + #[serde(with = "eth2_serde_utils::json_str")] + pub slashing_protection: Interchange, +} + +#[derive(Deserialize, Serialize)] +pub struct SingleExportKeystoresResponse { + pub status: Status, + #[serde(skip_serializing_if = "Option::is_none")] + pub validating_keystore: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub validating_keystore_password: Option, +} diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/src/http_api/keystores.rs index b886f604350..982b5f0171c 100644 --- a/validator_client/src/http_api/keystores.rs +++ b/validator_client/src/http_api/keystores.rs @@ -4,10 +4,13 @@ use crate::{ ValidatorStore, }; use account_utils::ZeroizeString; -use eth2::lighthouse_vc::std_types::{ - DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, ImportKeystoreStatus, - ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, KeystoreJsonStr, - ListKeystoresResponse, SingleKeystoreResponse, Status, +use eth2::lighthouse_vc::{ + std_types::{ + DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, + ImportKeystoreStatus, ImportKeystoresRequest, ImportKeystoresResponse, InterchangeJsonStr, + KeystoreJsonStr, ListKeystoresResponse, SingleKeystoreResponse, Status, + }, + types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, }; use eth2_keystore::Keystore; use slog::{info, warn, Logger}; @@ -219,11 +222,28 @@ pub fn delete( task_executor: TaskExecutor, log: Logger, ) -> Result { + let export_response = export(request, validator_store, task_executor, log)?; + Ok(DeleteKeystoresResponse { + data: export_response + .data + .into_iter() + .map(|response| response.status) + .collect(), + slashing_protection: export_response.slashing_protection, + }) +} + +pub fn export( + request: DeleteKeystoresRequest, + validator_store: Arc>, + task_executor: TaskExecutor, + log: Logger, +) -> Result { // Remove from initialized validators. let initialized_validators_rwlock = validator_store.initialized_validators(); let mut initialized_validators = initialized_validators_rwlock.write(); - let mut statuses = request + let mut responses = request .pubkeys .iter() .map(|pubkey_bytes| { @@ -232,7 +252,7 @@ pub fn delete( &mut initialized_validators, task_executor.clone(), ) { - Ok(status) => Status::ok(status), + Ok(status) => status, Err(error) => { warn!( log, @@ -240,7 +260,11 @@ pub fn delete( "pubkey" => ?pubkey_bytes, "error" => ?error, ); - Status::error(DeleteKeystoreStatus::Error, error) + SingleExportKeystoresResponse { + status: Status::error(DeleteKeystoreStatus::Error, error), + validating_keystore: None, + validating_keystore_password: None, + } } } }) @@ -263,19 +287,19 @@ pub fn delete( })?; // Update stasuses based on availability of slashing protection data. - for (pubkey, status) in request.pubkeys.iter().zip(statuses.iter_mut()) { - if status.status == DeleteKeystoreStatus::NotFound + for (pubkey, response) in request.pubkeys.iter().zip(responses.iter_mut()) { + if response.status.status == DeleteKeystoreStatus::NotFound && slashing_protection .data .iter() .any(|interchange_data| interchange_data.pubkey == *pubkey) { - status.status = DeleteKeystoreStatus::NotActive; + response.status.status = DeleteKeystoreStatus::NotActive; } } - Ok(DeleteKeystoresResponse { - data: statuses, + Ok(ExportKeystoresResponse { + data: responses, slashing_protection, }) } @@ -284,7 +308,7 @@ fn delete_single_keystore( pubkey_bytes: &PublicKeyBytes, initialized_validators: &mut InitializedValidators, task_executor: TaskExecutor, -) -> Result { +) -> Result { if let Some(handle) = task_executor.handle() { let pubkey = pubkey_bytes .decompress() @@ -292,9 +316,22 @@ fn delete_single_keystore( match handle.block_on(initialized_validators.delete_definition_and_keystore(&pubkey, true)) { - Ok(_) => Ok(DeleteKeystoreStatus::Deleted), + Ok(Some(keystore_and_password)) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::Deleted), + validating_keystore: Some(KeystoreJsonStr(keystore_and_password.keystore)), + validating_keystore_password: Some(keystore_and_password.password), + }), + Ok(None) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::Deleted), + validating_keystore: None, + validating_keystore_password: None, + }), Err(e) => match e { - Error::ValidatorNotInitialized(_) => Ok(DeleteKeystoreStatus::NotFound), + Error::ValidatorNotInitialized(_) => Ok(SingleExportKeystoresResponse { + status: Status::ok(DeleteKeystoreStatus::NotFound), + validating_keystore: None, + validating_keystore_password: None, + }), _ => Err(format!("unable to disable and delete: {:?}", e)), }, } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 4bb8f33a06c..ec7b00ac2b9 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -77,6 +77,7 @@ pub struct Config { pub listen_addr: IpAddr, pub listen_port: u16, pub allow_origin: Option, + pub allow_keystore_export: bool, } impl Default for Config { @@ -86,6 +87,7 @@ impl Default for Config { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 5062, allow_origin: None, + allow_keystore_export: false, } } } @@ -110,6 +112,7 @@ pub fn serve( shutdown: impl Future + Send + Sync + 'static, ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; + let allow_keystore_export = config.allow_keystore_export; let log = ctx.log.clone(); // Configure CORS. @@ -580,6 +583,29 @@ pub fn serve( }) }); + // DELETE /lighthouse/keystores + let delete_lighthouse_keystores = warp::path("lighthouse") + .and(warp::path("keystores")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(signer.clone()) + .and(validator_store_filter.clone()) + .and(task_executor_filter.clone()) + .and(log_filter.clone()) + .and_then( + move |request, signer, validator_store, task_executor, log| { + blocking_signed_json_task(signer, move || { + if allow_keystore_export { + keystores::export(request, validator_store, task_executor, log) + } else { + return Err(warp_utils::reject::custom_bad_request( + "keystore export is disabled".to_string(), + )); + } + }) + }, + ); + // Standard key-manager endpoints. let eth_v1 = warp::path("eth").and(warp::path("v1")); let std_keystores = eth_v1.and(warp::path("keystores")).and(warp::path::end()); @@ -913,7 +939,8 @@ pub fn serve( )) .or(warp::patch().and(patch_validators)) .or(warp::delete().and( - delete_fee_recipient + delete_lighthouse_keystores + .or(delete_fee_recipient) .or(delete_gas_limit) .or(delete_std_keystores) .or(delete_std_remotekeys), diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 25af9be6fe4..174235b8882 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -126,6 +126,7 @@ impl ApiTester { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, + allow_keystore_export: true, }, log, _phantom: PhantomData, diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 8d9fbe281fc..6213304a44c 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -8,7 +8,7 @@ use crate::signing_method::SigningMethod; use account_utils::{ - read_password, read_password_from_user, + read_password, read_password_from_user, read_password_string, validator_definitions::{ self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, Web3SignerDefinition, CONFIG_FILENAME, @@ -43,6 +43,11 @@ const DEFAULT_REMOTE_SIGNER_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); // Use TTY instead of stdin to capture passwords from users. const USE_STDIN: bool = false; +pub struct KeystoreAndPassword { + pub keystore: Keystore, + pub password: ZeroizeString, +} + #[derive(Debug)] pub enum Error { /// Refused to open a validator with an existing lockfile since that validator may be in-use by @@ -97,6 +102,9 @@ pub enum Error { UnableToBuildWeb3SignerClient(ReqwestError), /// Unable to apply an action to a validator. InvalidActionOnValidator, + UnableToReadValidatorPassword(String), + MissingKeystorePassword, + UnableToReadKeystoreFile(eth2_keystore::Error), } impl From for Error { @@ -534,31 +542,49 @@ impl InitializedValidators { &mut self, pubkey: &PublicKey, is_local_keystore: bool, - ) -> Result<(), Error> { + ) -> Result, Error> { // 1. Disable the validator definition. // // We disable before removing so that in case of a crash the auto-discovery mechanism // won't re-activate the keystore. - if let Some(def) = self + let keystore_and_password = if let Some(def) = self .definitions .as_mut_slice() .iter_mut() .find(|def| &def.voting_public_key == pubkey) { - // Update definition for local keystore - if def.signing_definition.is_local_keystore() && is_local_keystore { - def.enabled = false; - self.definitions - .save(&self.validators_dir) - .map_err(Error::UnableToSaveDefinitions)?; - } else if !def.signing_definition.is_local_keystore() && !is_local_keystore { - def.enabled = false; - } else { - return Err(Error::InvalidActionOnValidator); + match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password, + voting_keystore_password_path, + .. + } if is_local_keystore => { + let password = match (voting_keystore_password, voting_keystore_password_path) { + (Some(password), _) => password.clone(), + (_, Some(path)) => read_password_string(path) + .map_err(Error::UnableToReadValidatorPassword)?, + (None, None) => return Err(Error::MissingKeystorePassword), + }; + let keystore = Keystore::from_json_file(voting_keystore_path) + .map_err(Error::UnableToReadKeystoreFile)?; + + def.enabled = false; + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Some(KeystoreAndPassword { keystore, password }) + } + SigningDefinition::Web3Signer(_) if !is_local_keystore => { + def.enabled = false; + None + } + _ => return Err(Error::InvalidActionOnValidator), } } else { return Err(Error::ValidatorNotInitialized(pubkey.clone())); - } + }; // 2. Delete from `self.validators`, which holds the signing method. // Delete the keystore files. @@ -585,7 +611,7 @@ impl InitializedValidators { .save(&self.validators_dir) .map_err(Error::UnableToSaveDefinitions)?; - Ok(()) + Ok(keystore_and_password) } /// Attempt to delete the voting keystore file, or its entire validator directory. From 71409e48972c9848d17dde54b2e2bdfbca07a414 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 23 Aug 2022 18:25:27 +1000 Subject: [PATCH 039/138] Progress with move command --- .../src/validators/move_validators.rs | 165 ++++++++++++++++-- 1 file changed, 155 insertions(+), 10 deletions(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 49a379d8da4..4ac5256245d 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -3,18 +3,17 @@ use crate::DumpConfig; use clap::{App, Arg, ArgMatches}; use eth2::{ lighthouse_vc::{ - http_client::ValidatorClientHttpClient, - std_types::{DeleteKeystoresRequest, ImportKeystoresRequest}, - types::UpdateFeeRecipientRequest, + std_types::{DeleteKeystoreStatus, DeleteKeystoresRequest, InterchangeJsonStr, Status}, + types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, }, SensitiveUrl, }; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; use std::str::FromStr; -use types::PublicKeyBytes; +use types::{Address, PublicKeyBytes}; pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; @@ -25,6 +24,9 @@ pub const SRC_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "src-validator-client-token"; pub const DEST_VALIDATOR_CLIENT_URL_FLAG: &str = "dest-validator-client-url"; pub const DEST_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "dest-validator-client-token"; pub const VALIDATORS_FLAG: &str = "validators"; +pub const GAS_LIMIT_FLAG: &str = "gas-limit"; +pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; +pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) @@ -97,6 +99,37 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .required(true) .takes_value(true), ) + .arg( + Arg::with_name(GAS_LIMIT_FLAG) + .long(GAS_LIMIT_FLAG) + .value_name("UINT64") + .help( + "All created validators will use this gas limit. It is recommended \ + to leave this as the default value by not specifying this flag.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(FEE_RECIPIENT_FLAG) + .long(FEE_RECIPIENT_FLAG) + .value_name("ETH1_ADDRESS") + .help( + "All created validators will use this value for the suggested \ + fee recipient. Omit this flag to use the default value from the VC.", + ) + .required(false) + .takes_value(true), + ) + .arg( + Arg::with_name(BUILDER_PROPOSALS_FLAG) + .long(BUILDER_PROPOSALS_FLAG) + .help( + "When provided, all created validators will attempt to create \ + blocks via builder rather than the local EL.", + ) + .required(false), + ) } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] @@ -128,6 +161,9 @@ pub struct MoveConfig { pub dest_vc_url: SensitiveUrl, pub dest_vc_token_path: PathBuf, pub validators: Validators, + pub builder_proposals: bool, + pub fee_recipient: Option
, + pub gas_limit: Option, } impl MoveConfig { @@ -145,6 +181,9 @@ impl MoveConfig { DEST_VALIDATOR_CLIENT_TOKEN_FLAG, )?, validators: clap_utils::parse_required(matches, VALIDATORS_FLAG)?, + builder_proposals: matches.is_present(BUILDER_PROPOSALS_FLAG), + fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, + gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, }) } } @@ -169,6 +208,9 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { dest_vc_url, dest_vc_token_path, validators, + builder_proposals, + fee_recipient, + gas_limit, } = config; if !working_directory_path.exists() { @@ -228,16 +270,27 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } }; - // It doesn't make sense to move no validators. - if pubkeys_to_move.is_empty() { - return Err(format!("--{} cannot be an empty list", VALIDATORS_FLAG)); - } + let src_keystores_map: HashMap<_, _> = src_keystores + .iter() + .map(|k| (k.validating_pubkey, k)) + .collect(); for pubkey_to_move in pubkeys_to_move { + // Skip read-only validators rather than exiting. This makes it a bit easier to use the + // "all" flag. + if src_keystores_map + .get(&pubkey_to_move) + .ok_or("Inconsistent src keystore map")? + .readonly + .unwrap_or(true) + { + eprintln!("Skipping read-only validator {:?}", pubkey_to_move); + } + let request = DeleteKeystoresRequest { pubkeys: vec![pubkey_to_move], }; - let deleted_keystore = match src_http_client.delete_keystores(&request).await { + let deleted = match src_http_client.delete_lighthouse_keystores(&request).await { Ok(deleted) => deleted, Err(e) => { match src_http_client.get_keystores().await { @@ -267,6 +320,98 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { return Err(format!("Deleting {:?} failed with {:?}", pubkey_to_move, e)); } }; + + let ExportKeystoresResponse { + mut data, + slashing_protection, + } = deleted; + + if data.len() != 1 { + return Err(format!( + "Too many deleted validators from VC: {}", + data.len() + )); + } + + let exported_validator = data + .pop() + .ok_or("VC responded with zero deleted validators")?; + + let (voting_keystore, voting_keystore_password) = match exported_validator { + SingleExportKeystoresResponse { + status: + Status { + status: DeleteKeystoreStatus::Deleted, + message: _, + }, + validating_keystore, + validating_keystore_password, + } => match (validating_keystore, validating_keystore_password) { + (Some(keystore), Some(password)) => (keystore, password), + (keystore_opt, password_opt) => { + eprintln!( + "Validator {:?} was not moved since the validator client did \ + not return both a keystore and password. It is likely that the \ + validator has been deleted from the source validator client \ + without being moved to the destination validator client. \ + This validator will most likely need to be manually recovered \ + from a mnemonic or backup.", + pubkey_to_move + ); + return Err(format!( + "VC returned deleted but keystore {}, password {}", + keystore_opt.is_some(), + password_opt.is_some() + )); + } + }, + SingleExportKeystoresResponse { + status: Status { status, .. }, + .. + } if matches!( + status, + DeleteKeystoreStatus::NotFound | DeleteKeystoreStatus::NotActive + ) => + { + eprintln!( + "Validator {:?} was not moved since it was not found or not active. This scenario \ + is unexpected and might indicate that another process is also performing \ + an export from the source validator client. Exiting now for safety. \ + If there is definitely no other process exporting validators then it \ + may be safe to run this command again.", + pubkey_to_move + ); + return Err(format!( + "VC indicated that a previously known validator was {:?}", + status, + )); + } + SingleExportKeystoresResponse { + status: Status { status, message }, + .. + } => { + eprintln!( + "Validator {:?} was not moved because the source validator client \ + indicated there was an error disabling it. Manual intervention is \ + required to recover from this scenario.", + pubkey_to_move + ); + return Err(format!( + "VC returned status {:?} with message {:?}", + status, message + )); + } + }; + + let validator_specification = ValidatorSpecification { + voting_keystore, + voting_keystore_password, + slashing_protection: Some(InterchangeJsonStr(slashing_protection)), + fee_recipient, + gas_limit, + builder_proposals: Some(builder_proposals), + enabled: Some(true), + }; } Ok(()) From e98a0155bd0b75814a73d03321ae5207e4794e62 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 15:54:52 +1000 Subject: [PATCH 040/138] Progress with `move` implementation --- validator_manager/src/lib.rs | 2 +- validator_manager/src/validators/common.rs | 126 ++++++++++++++- .../src/validators/create_validators.rs | 20 +-- .../src/validators/import_validators.rs | 144 ++++++------------ .../src/validators/move_validators.rs | 115 +++++++++++++- 5 files changed, 286 insertions(+), 121 deletions(-) diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 4b52b8edb39..0571cd7f4ce 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -4,7 +4,7 @@ use environment::Environment; use serde::Serialize; use std::path::PathBuf; use types::EthSpec; -use validators::create_validators::write_to_json_file; +use validators::common::write_to_json_file; pub mod validators; diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index 9e3d279c027..9bd4358af95 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -1,7 +1,11 @@ use account_utils::ZeroizeString; use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::{ - lighthouse_vc::{http_client::ValidatorClientHttpClient, std_types::SingleKeystoreResponse}, + lighthouse_vc::{ + http_client::ValidatorClientHttpClient, + std_types::{ImportKeystoresRequest, SingleKeystoreResponse}, + types::UpdateFeeRecipientRequest, + }, SensitiveUrl, }; use serde::{Deserialize, Serialize}; @@ -10,6 +14,8 @@ use std::path::{Path, PathBuf}; use tree_hash::TreeHash; use types::*; +pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; + /// When the `ethereum/staking-deposit-cli` tool generates deposit data JSON, it adds a /// `deposit_cli_version` to protect the web-based "Launchpad" tool against a breaking change that /// was introduced in `ethereum/staking-deposit-cli`. Lighthouse don't really have a version that it @@ -19,6 +25,16 @@ use types::*; /// 2. Weird enough to identify Lighthouse. const LIGHTHOUSE_DEPOSIT_CLI_VERSION: &str = "20.18.20"; +#[derive(Debug)] +pub enum UploadError { + InvalidPublicKey, + DuplicateValidator(PublicKeyBytes), + FailedToListKeys(eth2::Error), + KeyUploadFailed(eth2::Error), + FeeRecipientUpdateFailed(eth2::Error), + PatchValidatorFailed(eth2::Error), +} + #[derive(Serialize, Deserialize)] pub struct ValidatorSpecification { pub voting_keystore: KeystoreJsonStr, @@ -30,6 +46,97 @@ pub struct ValidatorSpecification { pub enabled: Option, } +impl ValidatorSpecification { + /// Upload the validator to a validator client via HTTP. + pub async fn upload( + self, + http_client: &ValidatorClientHttpClient, + ignore_duplicates: bool, + ) -> Result<(), UploadError> { + let ValidatorSpecification { + voting_keystore, + voting_keystore_password, + slashing_protection, + fee_recipient, + gas_limit, + builder_proposals, + enabled, + } = self; + + let voting_public_key = voting_keystore + .public_key() + .ok_or(UploadError::InvalidPublicKey)? + .into(); + + let request = ImportKeystoresRequest { + keystores: vec![voting_keystore], + passwords: vec![voting_keystore_password], + slashing_protection, + }; + + // Check to see if this validator already exists on the remote validator. + match http_client.get_keystores().await { + Ok(response) => { + if response + .data + .iter() + .find(|validator| validator.validating_pubkey == voting_public_key) + .is_some() + { + if ignore_duplicates { + eprintln!( + "Duplicate validators are ignored, ignoring {:?} which exists \ + on the destination validator client", + voting_public_key + ); + } else { + return Err(UploadError::DuplicateValidator(voting_public_key)); + } + } + } + Err(e) => { + return Err(UploadError::FailedToListKeys(e)); + } + }; + + if let Err(e) = http_client.post_keystores(&request).await { + // Return here *without* writing the deposit JSON file. This might help prevent + // users from submitting duplicate deposits or deposits for validators that weren't + // initialized on a VC. + // + // Next the the user runs with the --ignore-duplicates flag there should be a new, + // complete deposit JSON file created. + return Err(UploadError::KeyUploadFailed(e)); + } + + if let Some(fee_recipient) = fee_recipient { + http_client + .post_fee_recipient( + &voting_public_key, + &UpdateFeeRecipientRequest { + ethaddress: fee_recipient, + }, + ) + .await + .map_err(UploadError::FeeRecipientUpdateFailed)?; + } + + if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { + http_client + .patch_lighthouse_validators( + &voting_public_key, + enabled, + gas_limit, + builder_proposals, + ) + .await + .map_err(UploadError::PatchValidatorFailed)?; + } + + Ok(()) + } +} + #[derive(Serialize, Deserialize)] pub struct CreateSpec { pub mnemonic: String, @@ -225,3 +332,20 @@ pub async fn vc_http_client>( Ok((http_client, remote_keystores)) } + +/// Write some object to a file as JSON. +/// +/// The file must be created new, it must not already exist. +pub fn write_to_json_file, S: Serialize>( + path: P, + contents: &S, +) -> Result<(), String> { + eprintln!("Writing {:?}", path.as_ref()); + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|e| format!("Failed to open {:?}: {:?}", path.as_ref(), e))?; + serde_json::to_writer(&mut file, contents) + .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) +} diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index e2a0ec8f7a6..8c5d43d57a0 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -10,7 +10,7 @@ use eth2::{ use eth2_wallet::WalletBuilder; use serde::{Deserialize, Serialize}; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::time::Duration; use types::*; @@ -505,28 +505,12 @@ async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<( Ok(()) } -/// Write some object to a file as JSON. -/// -/// The file must be created new, it must not already exist. -pub fn write_to_json_file, S: Serialize>( - path: P, - contents: &S, -) -> Result<(), String> { - eprintln!("Writing {:?}", path.as_ref()); - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&path) - .map_err(|e| format!("Failed to open {:?}: {:?}", path.as_ref(), e))?; - serde_json::to_writer(&mut file, contents) - .map_err(|e| format!("Failed to write JSON to {:?}: {:?}", path.as_ref(), e)) -} - #[cfg(test)] pub mod tests { use super::*; use eth2_network_config::Eth2NetworkConfig; use regex::Regex; + use std::path::Path; use std::str::FromStr; use tempfile::{tempdir, TempDir}; use tree_hash::TreeHash; diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 4d2c2a65309..0247aa695b9 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -1,10 +1,7 @@ use super::common::*; use crate::DumpConfig; use clap::{App, Arg, ArgMatches}; -use eth2::{ - lighthouse_vc::{std_types::ImportKeystoresRequest, types::UpdateFeeRecipientRequest}, - SensitiveUrl, -}; +use eth2::SensitiveUrl; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -13,7 +10,6 @@ pub const CMD: &str = "import"; pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; 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 DETECTED_DUPLICATE_MESSAGE: &str = "Duplicate validator detected!"; @@ -137,105 +133,57 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { ); for (i, validator) in validators.into_iter().enumerate() { - let ValidatorSpecification { - voting_keystore, - voting_keystore_password, - slashing_protection, - fee_recipient, - gas_limit, - builder_proposals, - enabled, - } = validator; - - let voting_public_key = voting_keystore - .public_key() - .ok_or_else(|| format!("Validator keystore at index {} is missing a public key", i))? - .into(); - - let request = ImportKeystoresRequest { - keystores: vec![voting_keystore], - passwords: vec![voting_keystore_password], - slashing_protection, - }; - - // Check to see if this validator already exists on the remote validator. - match http_client.get_keystores().await { - Ok(response) => { - if response - .data - .iter() - .find(|validator| validator.validating_pubkey == voting_public_key) - .is_some() - { - if ignore_duplicates { - eprintln!( - "Duplicate validators are ignored, ignoring {:?} which exists \ - on the validator client and in {:?}", - voting_public_key, validators_file_path - ); - } else { - eprintln!( - "{} {:?} exists on the remote validator client and in {:?}", - DETECTED_DUPLICATE_MESSAGE, voting_public_key, validators_file_path - ); - return Err(DETECTED_DUPLICATE_MESSAGE.to_string()); - } - } + match validator.upload(&http_client, ignore_duplicates).await { + Ok(()) => eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count), + e @ Err(UploadError::InvalidPublicKey) => { + eprintln!("Validator {} has an invalid public key", i); + return Err(format!("{:?}", e)); } - Err(e) => { + ref e @ Err(UploadError::DuplicateValidator(voting_public_key)) => { eprintln!( - "Failed to list keystores during batch {}. Some keys may have been imported whilst \ - others may not have been imported. A potential solution is to use the \ - --{} flag, however care should be taken to ensure that there are no \ - duplicate deposits submitted.", - i, IGNORE_DUPLICATES_FLAG + "Duplicate validator {:?} already exists on the destination validator client. \ + This may indicate that some validators are running in two places at once, which \ + can lead to slashing. If you are certain that there is no risk, add the --{} flag.", + voting_public_key, IGNORE_DUPLICATES_FLAG ); - return Err(format!("Failed to list keys: {:?}", e)); + return Err(format!("{:?}", e)); } - }; - - if let Err(e) = http_client.post_keystores(&request).await { - eprintln!( - "Failed to upload batch {}. Some keys may have been imported whilst \ - others may not have been imported. A potential solution is to use the \ - --{} flag, however care should be taken to ensure that there are no \ + Err(UploadError::FailedToListKeys(e)) => { + eprintln!( + "Failed to list keystores. Some keys may have been imported whilst \ + others may not have been imported. A potential solution is run this command again \ + using the --{} flag, however care should be taken to ensure that there are no \ duplicate deposits submitted.", - i, IGNORE_DUPLICATES_FLAG - ); - // Return here *without* writing the deposit JSON file. This might help prevent - // users from submitting duplicate deposits or deposits for validators that weren't - // initialized on a VC. - // - // Next the the user runs with the --ignore-duplicates flag there should be a new, - // complete deposit JSON file created. - return Err(format!("Key upload failed: {:?}", e)); - } - - if let Some(fee_recipient) = fee_recipient { - http_client - .post_fee_recipient( - &voting_public_key, - &UpdateFeeRecipientRequest { - ethaddress: fee_recipient, - }, - ) - .await - .map_err(|e| format!("Failed to update fee recipient on VC: {:?}", e))?; - } - - if gas_limit.is_some() || builder_proposals.is_some() || enabled.is_some() { - http_client - .patch_lighthouse_validators( - &voting_public_key, - enabled, - gas_limit, - builder_proposals, - ) - .await - .map_err(|e| format!("Failed to update lighthouse validator on VC: {:?}", e))?; + IGNORE_DUPLICATES_FLAG + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::KeyUploadFailed(e)) => { + eprintln!( + "Failed to upload keystore. Some keys may have been imported whilst \ + others may not have been imported. A potential solution is run this command again \ + using the --{} flag, however care should be taken to ensure that there are no \ + duplicate deposits submitted.", + IGNORE_DUPLICATES_FLAG + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::FeeRecipientUpdateFailed(e)) => { + eprintln!( + "Failed to set fee recipient for validator {}. This value may need \ + to be set manually. Continuing with other validators. Error was {:?}", + i, e + ); + } + Err(UploadError::PatchValidatorFailed(e)) => { + eprintln!( + "Failed to set some values on validator {} (e.g., builder, enabled or gas limit. \ + These values value may need to be set manually. Continuing with other validators. \ + Error was {:?}", + i, e + ); + } } - - eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count); } Ok(()) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 4ac5256245d..9070dea31fc 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -11,11 +11,12 @@ use eth2::{ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; use types::{Address, PublicKeyBytes}; pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; +pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json"; pub const CMD: &str = "move"; pub const WORKING_DIRECTORY_FLAG: &str = "working-directory"; @@ -244,7 +245,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { let (src_http_client, src_keystores) = vc_http_client(src_vc_url.clone(), &src_vc_token_path).await?; - let (dest_http_client, dest_keystores) = + let (dest_http_client, _dest_keystores) = vc_http_client(dest_vc_url.clone(), &dest_vc_token_path).await?; let pubkeys_to_move = match validators { @@ -275,7 +276,8 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { .map(|k| (k.validating_pubkey, k)) .collect(); - for pubkey_to_move in pubkeys_to_move { + let count = pubkeys_to_move.len(); + for (i, &pubkey_to_move) in pubkeys_to_move.iter().enumerate() { // Skip read-only validators rather than exiting. This makes it a bit easier to use the // "all" flag. if src_keystores_map @@ -412,11 +414,118 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { builder_proposals: Some(builder_proposals), enabled: Some(true), }; + + let validator_specification_path = + working_directory_path.join(VALIDATOR_SPECIFICATION_FILE); + if let Err(e) = write_to_json_file(&validator_specification_path, &validator_specification) + { + eprintln!( + "Validator {:?} was removed from the source validator but it could not be \ + saved to disk locally in the case of an upload failure. The application will \ + continue since it may be possible to upload the validator successfully, \ + however recovery options are limited. Write filed with {:?}", + pubkey_to_move, e + ); + } + + // We might as well just ignore validators that already exist on the destination machine, + // there doesn't appear to be much harm just adding them again. + let ignore_duplicates = true; + + match validator_specification + .upload(&dest_http_client, ignore_duplicates) + .await + { + Ok(()) => eprintln!( + "Uploaded keystore {} of {} to the destination VC", + i + 1, + count + ), + e @ Err(UploadError::InvalidPublicKey) => { + eprintln!("Validator {} has an invalid public key", i); + return Err(format!("{:?}", e)); + } + Err(UploadError::DuplicateValidator(_)) => { + return Err(format!( + "Duplicate validator detected when duplicates are ignored" + )); + } + Err(UploadError::FailedToListKeys(e)) => { + eprintln!( + "Failed to list keystores. Some keys may have been moved whilst \ + others may not.", + ); + eprint_recovery_advice( + &working_directory_path, + &validator_specification_path, + &dest_vc_url, + &dest_vc_token_path, + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::KeyUploadFailed(e)) => { + eprintln!( + "Failed to upload keystore. Some keys may have been moved whilst \ + others may not.", + ); + eprint_recovery_advice( + &working_directory_path, + &validator_specification_path, + &dest_vc_url, + &dest_vc_token_path, + ); + return Err(format!("{:?}", e)); + } + Err(UploadError::FeeRecipientUpdateFailed(e)) => { + eprintln!( + "Failed to set fee recipient for validator {}. This value may need \ + to be set manually. Continuing with other validators. Error was {:?}", + i, e + ); + } + Err(UploadError::PatchValidatorFailed(e)) => { + eprintln!( + "Failed to set some values on validator {} (e.g., builder, enabled or gas limit. \ + These values value may need to be set manually. Continuing with other validators. \ + Error was {:?}", + i, e + ); + } + } } Ok(()) } +pub fn eprint_recovery_advice>( + working_directory_path: P, + validator_file: P, + dest_vc_url: &SensitiveUrl, + dest_vc_token_path: P, +) { + use crate::validators::import_validators::{ + CMD, VALIDATORS_FILE_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG, VALIDATOR_CLIENT_URL_FLAG, + }; + + eprintln!( + "It may be possible to recover this validator by running the following command: \n\n\ + lighthouse {} {} {} --{} {:?} --{} {} --{} {:?} \n\n\ + The {:?} directory contains a backup of the validator that was unable to be uploaded. \ + That backup contains the unencrypted validator secret key and should not be shared with \ + anyone. If the recovery command (above) succeeds, it is safe to remove that directory.", + crate::CMD, + crate::validators::CMD, + CMD, + VALIDATORS_FILE_FLAG, + validator_file.as_ref().as_os_str(), + VALIDATOR_CLIENT_URL_FLAG, + dest_vc_url.full, + VALIDATOR_CLIENT_TOKEN_FLAG, + dest_vc_token_path.as_ref().as_os_str(), + working_directory_path.as_ref().as_os_str(), + ) +} + /* #[cfg(test)] mod test { From adc87f71e75f143a2b581c917c0536fbf25ae8b8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 16:31:21 +1000 Subject: [PATCH 041/138] Add basic `move` testing --- validator_manager/src/validators/common.rs | 2 +- .../src/validators/import_validators.rs | 28 ++- .../src/validators/move_validators.rs | 217 +++++------------- 3 files changed, 80 insertions(+), 167 deletions(-) diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index 9bd4358af95..f64fcd56d5a 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -35,7 +35,7 @@ pub enum UploadError { PatchValidatorFailed(eth2::Error), } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct ValidatorSpecification { pub voting_keystore: KeystoreJsonStr, pub voting_keystore_password: ZeroizeString, diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 0247aa695b9..90e5db87f3b 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -190,7 +190,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { } #[cfg(test)] -mod test { +pub mod tests { use super::*; use crate::validators::create_validators::tests::TestBuilder as CreateTestBuilder; use std::fs; @@ -199,7 +199,7 @@ mod test { const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; - struct TestBuilder { + pub struct TestBuilder { import_config: ImportConfig, vc: ApiTester, /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up @@ -209,7 +209,7 @@ mod test { } impl TestBuilder { - async fn new() -> Self { + pub async fn new() -> Self { let dir = tempdir().unwrap(); let vc = ApiTester::new().await; let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); @@ -234,7 +234,7 @@ mod test { self } - async fn create_validators(mut self, count: u32, first_index: u32) -> Self { + pub async fn create_validators(mut self, count: u32, first_index: u32) -> Self { let create_result = CreateTestBuilder::default() .mutate_config(|config| { config.count = count; @@ -253,12 +253,12 @@ mod test { /// Imports validators without running the entire test suite in `Self::run_test`. This is /// useful for simulating duplicate imports. - async fn import_validators_without_checks(self) -> Self { + pub async fn import_validators_without_checks(self) -> Self { run(self.import_config.clone()).await.unwrap(); self } - async fn run_test(self) -> TestResult { + pub async fn run_test(self) -> TestResult { let result = run(self.import_config.clone()).await; if result.is_ok() { @@ -294,13 +294,17 @@ mod test { } } - TestResult { result } + TestResult { + result, + vc: self.vc, + } } } #[must_use] // Use the `assert_ok` or `assert_err` fns to "use" this value. - struct TestResult { - result: Result<(), String>, + pub struct TestResult { + pub result: Result<(), String>, + pub vc: ApiTester, } impl TestResult { @@ -308,8 +312,8 @@ mod test { assert_eq!(self.result, Ok(())) } - fn assert_err_is(self, msg: String) { - assert_eq!(self.result, Err(msg)) + fn assert_err_contains(self, msg: &str) { + assert!(self.result.unwrap_err().contains(msg)) } } @@ -367,7 +371,7 @@ mod test { .await .run_test() .await - .assert_err_is(DETECTED_DUPLICATE_MESSAGE.to_string()); + .assert_err_contains("DuplicateValidator"); } #[tokio::test] diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 9070dea31fc..d1054704d85 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -433,6 +433,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { let ignore_duplicates = true; match validator_specification + .clone() .upload(&dest_http_client, ignore_duplicates) .await { @@ -455,9 +456,9 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { "Failed to list keystores. Some keys may have been moved whilst \ others may not.", ); - eprint_recovery_advice( + backup_validator( + &validator_specification, &working_directory_path, - &validator_specification_path, &dest_vc_url, &dest_vc_token_path, ); @@ -468,9 +469,9 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { "Failed to upload keystore. Some keys may have been moved whilst \ others may not.", ); - eprint_recovery_advice( + backup_validator( + &validator_specification, &working_directory_path, - &validator_specification_path, &dest_vc_url, &dest_vc_token_path, ); @@ -497,9 +498,9 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { Ok(()) } -pub fn eprint_recovery_advice>( +pub fn backup_validator>( + validator_specification: &ValidatorSpecification, working_directory_path: P, - validator_file: P, dest_vc_url: &SensitiveUrl, dest_vc_token_path: P, ) { @@ -507,6 +508,18 @@ pub fn eprint_recovery_advice>( CMD, VALIDATORS_FILE_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG, VALIDATOR_CLIENT_URL_FLAG, }; + let validator_specification_path = working_directory_path + .as_ref() + .join(VALIDATOR_SPECIFICATION_FILE); + if let Err(e) = write_to_json_file(&validator_specification_path, &validator_specification) { + eprintln!( + "A validator was removed from the source validator client but it could not be \ + saved to disk after an upload failure. The validator may need to be recovered \ + from a backup or mnemonic. Error was {:?}", + e + ); + } + eprintln!( "It may be possible to recover this validator by running the following command: \n\n\ lighthouse {} {} {} --{} {:?} --{} {} --{} {:?} \n\n\ @@ -517,7 +530,7 @@ pub fn eprint_recovery_advice>( crate::validators::CMD, CMD, VALIDATORS_FILE_FLAG, - validator_file.as_ref().as_os_str(), + validator_specification_path.as_os_str(), VALIDATOR_CLIENT_URL_FLAG, dest_vc_url.full, VALIDATOR_CLIENT_TOKEN_FLAG, @@ -526,111 +539,70 @@ pub fn eprint_recovery_advice>( ) } -/* #[cfg(test)] mod test { use super::*; - use crate::validators::create_validators::tests::TestBuilder as CreateTestBuilder; + use crate::validators::import_validators::tests::TestBuilder as ImportTestBuilder; use std::fs; use tempfile::{tempdir, TempDir}; use validator_client::http_api::test_utils::ApiTester; - const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; + const SRC_VC_TOKEN_FILE_NAME: &str = "src_vc_token.json"; + const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; struct TestBuilder { - import_config: MoveConfig, - vc: ApiTester, - /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up - /// before we can read it. - create_dir: Option, - _dir: TempDir, + import_builder: Option, + validators: Validators, + dir: TempDir, } impl TestBuilder { async fn new() -> Self { let dir = tempdir().unwrap(); - let vc = ApiTester::new().await; - let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); - fs::write(&vc_token_path, &vc.api_token).unwrap(); - Self { - import_config: MoveConfig { - // This field will be overwritten later on. - validators_file_path: dir.path().into(), - vc_url: vc.url.clone(), - vc_token_path, - ignore_duplicates: false, - }, - vc, - create_dir: None, - _dir: dir, + import_builder: None, + validators: Validators::All, + dir: dir, } } - pub fn mutate_import_config(mut self, func: F) -> Self { - func(&mut self.import_config); - self - } - - async fn create_validators(mut self, count: u32, first_index: u32) -> Self { - let create_result = CreateTestBuilder::default() - .mutate_config(|config| { - config.count = count; - config.first_index = first_index; - }) - .run_test() + async fn with_src_validators(mut self, count: u32, first_index: u32) -> Self { + let builder = ImportTestBuilder::new() + .await + .create_validators(count, first_index) .await; - assert!( - create_result.result.is_ok(), - "precondition: validators are created" - ); - self.import_config.validators_file_path = create_result.validators_file_path(); - self.create_dir = Some(create_result.output_dir); - self - } - - /// Imports validators without running the entire test suite in `Self::run_test`. This is - /// useful for simulating duplicate imports. - async fn import_validators_without_checks(self) -> Self { - run(self.import_config.clone()).await.unwrap(); + self.import_builder = Some(builder); self } async fn run_test(self) -> TestResult { - let result = run(self.import_config.clone()).await; - - if result.is_ok() { - let local_validators: Vec = { - let contents = - fs::read_to_string(&self.import_config.validators_file_path).unwrap(); - serde_json::from_str(&contents).unwrap() - }; - let list_keystores_response = self.vc.client.get_keystores().await.unwrap().data; - - assert_eq!( - local_validators.len(), - list_keystores_response.len(), - "vc should have exactly the number of validators imported" - ); - - for local_validator in &local_validators { - let local_keystore = &local_validator.voting_keystore.0; - let local_pubkey = local_keystore.public_key().unwrap().into(); - let remote_validator = list_keystores_response - .iter() - .find(|validator| validator.validating_pubkey == local_pubkey) - .expect("validator must exist on VC"); - assert_eq!(&remote_validator.derivation_path, &local_keystore.path()); - // It's not immediately clear why Lighthouse returns `None` rather than - // `Some(false)` here, I would expect the latter to be the most accurate. - // However, it doesn't seem like a big deal. - // - // See: https://github.com/sigp/lighthouse/pull/3490 - // - // If that PR changes we'll need to change this line. - assert_eq!(remote_validator.readonly, None); - } - } + let import_test_result = self + .import_builder + .expect("test requires an import builder") + .run_test() + .await; + assert!(import_test_result.result.is_ok()); + let src_vc = import_test_result.vc; + let src_vc_token_path = self.dir.path().join(SRC_VC_TOKEN_FILE_NAME); + fs::write(&src_vc_token_path, &src_vc.api_token).unwrap(); + + let dest_vc = ApiTester::new().await; + let dest_vc_token_path = self.dir.path().join(DEST_VC_TOKEN_FILE_NAME); + fs::write(&dest_vc_token_path, &dest_vc.api_token).unwrap(); + + let move_config = MoveConfig { + working_directory_path: self.dir.path().into(), + src_vc_url: src_vc.url.clone(), + src_vc_token_path, + dest_vc_url: dest_vc.url.clone(), + dest_vc_token_path, + validators: self.validators, + builder_proposals: false, + fee_recipient: None, + gas_limit: None, + }; + + let result = run(move_config).await; TestResult { result } } @@ -652,76 +624,13 @@ mod test { } #[tokio::test] - async fn create_one_validator() { - TestBuilder::new() - .await - .create_validators(1, 0) - .await - .run_test() - .await - .assert_ok(); - } - - #[tokio::test] - async fn create_three_validators() { - TestBuilder::new() - .await - .create_validators(3, 0) - .await - .run_test() - .await - .assert_ok(); - } - - #[tokio::test] - async fn create_one_validator_with_offset() { + async fn one_validator() { TestBuilder::new() .await - .create_validators(1, 42) - .await - .run_test() - .await - .assert_ok(); - } - - #[tokio::test] - async fn create_three_validators_with_offset() { - TestBuilder::new() - .await - .create_validators(3, 1337) - .await - .run_test() - .await - .assert_ok(); - } - - #[tokio::test] - async fn import_duplicates_when_disallowed() { - TestBuilder::new() - .await - .create_validators(1, 0) - .await - .import_validators_without_checks() - .await - .run_test() - .await - .assert_err_is(DETECTED_DUPLICATE_MESSAGE.to_string()); - } - - #[tokio::test] - async fn import_duplicates_when_allowed() { - TestBuilder::new() - .await - .mutate_import_config(|config| { - config.ignore_duplicates = true; - }) - .create_validators(1, 0) - .await - .import_validators_without_checks() + .with_src_validators(1, 0) .await .run_test() .await .assert_ok(); } } -*/ From 92edb1bf9b436740e864cf19a77d965edf470876 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 16:40:22 +1000 Subject: [PATCH 042/138] Progress more with testing --- .../src/validators/move_validators.rs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index d1054704d85..d94febda1e2 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -585,6 +585,10 @@ mod test { let src_vc = import_test_result.vc; let src_vc_token_path = self.dir.path().join(SRC_VC_TOKEN_FILE_NAME); fs::write(&src_vc_token_path, &src_vc.api_token).unwrap(); + let (src_vc_client, src_vc_initial_keystores) = + vc_http_client(src_vc.url.clone(), &src_vc_token_path) + .await + .unwrap(); let dest_vc = ApiTester::new().await; let dest_vc_token_path = self.dir.path().join(DEST_VC_TOKEN_FILE_NAME); @@ -595,8 +599,8 @@ mod test { src_vc_url: src_vc.url.clone(), src_vc_token_path, dest_vc_url: dest_vc.url.clone(), - dest_vc_token_path, - validators: self.validators, + dest_vc_token_path: dest_vc_token_path.clone(), + validators: self.validators.clone(), builder_proposals: false, fee_recipient: None, gas_limit: None, @@ -604,6 +608,22 @@ mod test { let result = run(move_config).await; + let (dest_vc_client, dest_vc_keystores) = + vc_http_client(dest_vc.url.clone(), &dest_vc_token_path) + .await + .unwrap(); + let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; + + match self.validators { + Validators::All => { + assert!(src_vc_final_keystores.is_empty()); + for initial_keystore in &src_vc_initial_keystores { + assert!(dest_vc_keystores.contains(initial_keystore)) + } + } + Validators::Some(_) => unimplemented!(), + } + TestResult { result } } } From d20e65a271ff92ddbeb591cfdef76fbc4e61f430 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 17:19:49 +1000 Subject: [PATCH 043/138] Progress with testing --- .../src/validators/move_validators.rs | 89 ++++++++++++++----- 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index d94febda1e2..1ff9592e23f 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -415,19 +415,6 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { enabled: Some(true), }; - let validator_specification_path = - working_directory_path.join(VALIDATOR_SPECIFICATION_FILE); - if let Err(e) = write_to_json_file(&validator_specification_path, &validator_specification) - { - eprintln!( - "Validator {:?} was removed from the source validator but it could not be \ - saved to disk locally in the case of an upload failure. The application will \ - continue since it may be possible to upload the validator successfully, \ - however recovery options are limited. Write filed with {:?}", - pubkey_to_move, e - ); - } - // We might as well just ignore validators that already exist on the destination machine, // there doesn't appear to be much harm just adding them again. let ignore_duplicates = true; @@ -552,7 +539,6 @@ mod test { struct TestBuilder { import_builder: Option, - validators: Validators, dir: TempDir, } @@ -561,7 +547,6 @@ mod test { let dir = tempdir().unwrap(); Self { import_builder: None, - validators: Validators::All, dir: dir, } } @@ -575,7 +560,10 @@ mod test { self } - async fn run_test(self) -> TestResult { + async fn run_test(self, gen_validators_enum: F) -> TestResult + where + F: Fn(&[PublicKeyBytes]) -> Validators, + { let import_test_result = self .import_builder .expect("test requires an import builder") @@ -590,6 +578,12 @@ mod test { .await .unwrap(); + let src_vc_initial_pubkeys: Vec<_> = src_vc_initial_keystores + .iter() + .map(|k| k.validating_pubkey) + .collect(); + let validators = gen_validators_enum(&src_vc_initial_pubkeys); + let dest_vc = ApiTester::new().await; let dest_vc_token_path = self.dir.path().join(DEST_VC_TOKEN_FILE_NAME); fs::write(&dest_vc_token_path, &dest_vc.api_token).unwrap(); @@ -600,7 +594,7 @@ mod test { src_vc_token_path, dest_vc_url: dest_vc.url.clone(), dest_vc_token_path: dest_vc_token_path.clone(), - validators: self.validators.clone(), + validators: validators.clone(), builder_proposals: false, fee_recipient: None, gas_limit: None, @@ -614,7 +608,7 @@ mod test { .unwrap(); let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; - match self.validators { + match validators { Validators::All => { assert!(src_vc_final_keystores.is_empty()); for initial_keystore in &src_vc_initial_keystores { @@ -644,12 +638,67 @@ mod test { } #[tokio::test] - async fn one_validator() { + async fn one_validator_move_all() { TestBuilder::new() .await .with_src_validators(1, 0) .await - .run_test() + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn one_validator_move_one() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .run_test(|pubkeys| Validators::Some(pubkeys.to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_all() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_one() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|pubkeys| Validators::Some(pubkeys[0..1].to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_two() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|pubkeys| Validators::Some(pubkeys[0..2].to_vec())) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_three() { + TestBuilder::new() + .await + .with_src_validators(3, 42) + .await + .run_test(|pubkeys| Validators::Some(pubkeys.to_vec())) .await .assert_ok(); } From 43b16b138d07f105bd0e9e7a8e2a1c7bd28979e3 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 17:46:46 +1000 Subject: [PATCH 044/138] Fix clippy lints --- validator_client/src/http_api/mod.rs | 4 +-- validator_client/src/http_api/test_utils.rs | 25 ++++++++++--------- .../src/http_api/tests/keystores.rs | 5 ++-- validator_manager/src/validators/common.rs | 5 ++-- .../src/validators/move_validators.rs | 7 ++---- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index ec7b00ac2b9..a25f9ea271c 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -598,9 +598,9 @@ pub fn serve( if allow_keystore_export { keystores::export(request, validator_store, task_executor, log) } else { - return Err(warp_utils::reject::custom_bad_request( + Err(warp_utils::reject::custom_bad_request( "keystore export is disabled".to_string(), - )); + )) } }) }, diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 174235b8882..862c83a21f0 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -82,10 +82,12 @@ impl ApiTester { let api_secret = ApiSecret::create_or_open(validator_dir.path()).unwrap(); let api_pubkey = api_secret.api_token(); - let mut config = Config::default(); - config.validator_dir = validator_dir.path().into(); - config.secrets_dir = secrets_dir.path().into(); - config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT); + let config = Config { + validator_dir: validator_dir.path().into(), + secrets_dir: secrets_dir.path().into(), + fee_recipient: Some(TEST_DEFAULT_FEE_RECIPIENT), + ..Default::default() + }; let spec = E::default_spec(); @@ -117,7 +119,7 @@ impl ApiTester { let context = Arc::new(Context { task_executor: test_runtime.task_executor.clone(), - api_secret: api_secret, + api_secret, validator_dir: Some(validator_dir.path().into()), validator_store: Some(validator_store.clone()), spec: E::default_spec(), @@ -131,7 +133,7 @@ impl ApiTester { log, _phantom: PhantomData, }); - let ctx = context.clone(); + let ctx = context; let (shutdown_tx, shutdown_rx) = oneshot::channel(); let server_shutdown = async { // It's not really interesting why this triggered, just that it happened. @@ -166,7 +168,7 @@ impl ApiTester { let tmp = tempdir().unwrap(); let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); let invalid_pubkey = api_secret.api_token(); - ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey.clone()).unwrap() + ValidatorClientHttpClient::new(self.url.clone(), invalid_pubkey).unwrap() } pub async fn test_with_invalid_auth(self, func: F) -> Self @@ -308,7 +310,7 @@ impl ApiTester { .await .unwrap() .data; - (response.validators.clone(), response.mnemonic.clone()) + (response.validators.clone(), response.mnemonic) }; assert_eq!(response.len(), s.count); @@ -343,22 +345,21 @@ impl ApiTester { .set_nextaccount(s.key_derivation_path_offset) .unwrap(); - for i in 0..s.count { + for item in response.iter().take(s.count) { let keypairs = wallet .next_validator(PASSWORD_BYTES, PASSWORD_BYTES, PASSWORD_BYTES) .unwrap(); let voting_keypair = keypairs.voting.decrypt_keypair(PASSWORD_BYTES).unwrap(); assert_eq!( - response[i].voting_pubkey, + item.voting_pubkey, voting_keypair.pk.clone().into(), "the locally generated voting pk should match the server response" ); let withdrawal_keypair = keypairs.withdrawal.decrypt_keypair(PASSWORD_BYTES).unwrap(); - let deposit_bytes = - eth2_serde_utils::hex::decode(&response[i].eth1_deposit_tx_data).unwrap(); + let deposit_bytes = eth2_serde_utils::hex::decode(&item.eth1_deposit_tx_data).unwrap(); let (deposit_data, _) = decode_eth1_tx_data(&deposit_bytes, E::default_spec().max_effective_balance) diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index 6a401575579..479451f751e 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -774,8 +774,8 @@ async fn check_get_set_fee_recipient() { .await; } -#[test] -fn check_get_set_gas_limit() { +#[tokio::test] +async fn check_get_set_gas_limit() { run_test(|tester: ApiTester| async move { let _ = &tester; let password = random_password_string(); @@ -947,6 +947,7 @@ fn check_get_set_gas_limit() { ); } }) + .await } fn all_indices(count: usize) -> Vec { diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index f64fcd56d5a..d433ec8089a 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -80,8 +80,7 @@ impl ValidatorSpecification { if response .data .iter() - .find(|validator| validator.validating_pubkey == voting_public_key) - .is_some() + .any(|validator| validator.validating_pubkey == voting_public_key) { if ignore_duplicates { eprintln!( @@ -278,7 +277,7 @@ mod bytes_4_without_0x_prefix { S: serde::Serializer, { let hex_string = &hex::encode(&bytes); - serializer.serialize_str(&hex_string) + serializer.serialize_str(hex_string) } pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; BYTES_LEN], D::Error> diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 1ff9592e23f..18dc15b511c 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -300,8 +300,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { if response .data .iter() - .find(|v| v.validating_pubkey == pubkey_to_move) - .is_some() + .any(|v| v.validating_pubkey == pubkey_to_move) { eprintln!( "There was an error removing a validator, however the validator \ @@ -434,9 +433,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { return Err(format!("{:?}", e)); } Err(UploadError::DuplicateValidator(_)) => { - return Err(format!( - "Duplicate validator detected when duplicates are ignored" - )); + return Err("Duplicate validator detected when duplicates are ignored".to_string()); } Err(UploadError::FailedToListKeys(e)) => { eprintln!( From bd00a795cbd662aa6aeba8bd09a67f7c5f3f6ed1 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 17:46:59 +1000 Subject: [PATCH 045/138] Remove commented-out code --- validator_client/src/http_api/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index 6de088c37cc..552c11a86cc 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -1,5 +1,5 @@ #![cfg(test)] -// #![cfg(not(debug_assertions))] +#![cfg(not(debug_assertions))] mod keystores; From 0f5092e2624ca12860ba7d79ba2f24fe55a3be94 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 17:47:28 +1000 Subject: [PATCH 046/138] Fix clippy lint --- validator_manager/src/validators/move_validators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 18dc15b511c..11abfd67347 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -544,7 +544,7 @@ mod test { let dir = tempdir().unwrap(); Self { import_builder: None, - dir: dir, + dir, } } From 06fa52ef5a8857fcbc9ca5cb3ed6bff692da8978 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 17:54:33 +1000 Subject: [PATCH 047/138] Improve tests --- .../src/validators/import_validators.rs | 9 +----- .../src/validators/move_validators.rs | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 90e5db87f3b..c551e552a52 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -283,14 +283,7 @@ pub mod tests { .find(|validator| validator.validating_pubkey == local_pubkey) .expect("validator must exist on VC"); assert_eq!(&remote_validator.derivation_path, &local_keystore.path()); - // It's not immediately clear why Lighthouse returns `None` rather than - // `Some(false)` here, I would expect the latter to be the most accurate. - // However, it doesn't seem like a big deal. - // - // See: https://github.com/sigp/lighthouse/pull/3490 - // - // If that PR changes we'll need to change this line. - assert_eq!(remote_validator.readonly, None); + assert_eq!(remote_validator.readonly, Some(false)); } } diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 11abfd67347..127dc689803 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -29,6 +29,8 @@ pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; +const NO_VALIDATORS_MSG: &str = "No validators present on source validator client"; + pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about( @@ -248,6 +250,10 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { let (dest_http_client, _dest_keystores) = vc_http_client(dest_vc_url.clone(), &dest_vc_token_path).await?; + if src_keystores.is_empty() { + return Err(NO_VALIDATORS_MSG.to_string()); + } + let pubkeys_to_move = match validators { Validators::All => src_keystores.iter().map(|v| v.validating_pubkey).collect(), Validators::Some(request_pubkeys) => { @@ -561,13 +567,14 @@ mod test { where F: Fn(&[PublicKeyBytes]) -> Validators, { - let import_test_result = self - .import_builder - .expect("test requires an import builder") - .run_test() - .await; - assert!(import_test_result.result.is_ok()); - let src_vc = import_test_result.vc; + let src_vc = if let Some(import_builder) = self.import_builder { + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + import_test_result.vc + } else { + ApiTester::new().await + }; + let src_vc_token_path = self.dir.path().join(SRC_VC_TOKEN_FILE_NAME); fs::write(&src_vc_token_path, &src_vc.api_token).unwrap(); let (src_vc_client, src_vc_initial_keystores) = @@ -599,7 +606,7 @@ mod test { let result = run(move_config).await; - let (dest_vc_client, dest_vc_keystores) = + let (_dest_vc_client, dest_vc_keystores) = vc_http_client(dest_vc.url.clone(), &dest_vc_token_path) .await .unwrap(); @@ -634,6 +641,15 @@ mod test { } } + #[tokio::test] + async fn no_validators() { + TestBuilder::new() + .await + .run_test(|_| Validators::All) + .await + .assert_err_is(NO_VALIDATORS_MSG.to_string()); + } + #[tokio::test] async fn one_validator_move_all() { TestBuilder::new() From 65e0fe2fb0572018dea5d277f64ca9db308bd44d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 18:13:54 +1000 Subject: [PATCH 048/138] Fix failing tests --- .../src/validators/move_validators.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 127dc689803..8be2efb8455 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -619,7 +619,24 @@ mod test { assert!(dest_vc_keystores.contains(initial_keystore)) } } - Validators::Some(_) => unimplemented!(), + Validators::Some(pubkeys) => { + assert_eq!( + src_vc_final_keystores.len(), + src_vc_initial_keystores + .len() + .checked_sub(pubkeys.len()) + .unwrap() + ); + assert_eq!(dest_vc_keystores.len(), pubkeys.len()); + for pubkey in pubkeys { + let initial_keystore = src_vc_initial_keystores + .iter() + .find(|k| k.validating_pubkey == pubkey) + .unwrap(); + assert!(!src_vc_final_keystores.contains(initial_keystore)); + assert!(dest_vc_keystores.contains(initial_keystore)); + } + } } TestResult { result } From bdffff16965033dbf747b57b622bb49c1c03b9bd Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 18:15:03 +1000 Subject: [PATCH 049/138] Remove abandoned files --- scripts/staking_deposit_cli/.gitignore | 1 - scripts/staking_deposit_cli/test.py | 116 ------------------------- 2 files changed, 117 deletions(-) delete mode 100644 scripts/staking_deposit_cli/.gitignore delete mode 100644 scripts/staking_deposit_cli/test.py diff --git a/scripts/staking_deposit_cli/.gitignore b/scripts/staking_deposit_cli/.gitignore deleted file mode 100644 index 3fec32c8427..00000000000 --- a/scripts/staking_deposit_cli/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tmp/ diff --git a/scripts/staking_deposit_cli/test.py b/scripts/staking_deposit_cli/test.py deleted file mode 100644 index 8cc2d501fed..00000000000 --- a/scripts/staking_deposit_cli/test.py +++ /dev/null @@ -1,116 +0,0 @@ -import os -import sys -import shutil -import subprocess -from subprocess import Popen, PIPE, STDOUT - -NUM_VALIDATORS=3 -TEST_MNEMONIC = "test test test test test test test test test test test waste" -WALLET_NAME="test_wallet" - -tmp_dir = os.path.join(".", "tmp") -mnemonic_path = os.path.join(tmp_dir, "mnemonic.txt") -lh_dir = os.path.join(tmp_dir, "lh") -lh_json_path = os.path.join(lh_dir, "deposit-data.json") -lh_wallet_password_path = os.path.join(lh_dir, "wallet.pass") -sdc_dir = os.path.join(tmp_dir, "sdc") -sdc_git_dir = os.path.join(sdc_dir, "staking-deposit-cli") - - -def setup(): - if os.path.exists(tmp_dir): - cleanup() - - os.mkdir(tmp_dir) - os.mkdir(lh_dir) - os.mkdir(sdc_dir) - - setup_sdc() - with open(mnemonic_path, "x") as file: - file.write(TEST_MNEMONIC) - - -def cleanup(): - shutil.rmtree(tmp_dir) - - -def setup_sdc(): - result = subprocess.run([ - "git", - "clone", - "--single-branch", - "https://github.com/ethereum/staking-deposit-cli.git", - str(sdc_git_dir) - ]) - assert(result.returncode == 0) - result = subprocess.run([ - "pip", - "install", - "-r", - "requirements.txt", - ], cwd=sdc_git_dir) - assert(result.returncode == 0) - result = subprocess.run([ - "python", - "setup.py", - "install", - ], cwd=sdc_git_dir) - assert(result.returncode == 0) - - -def sdc_generate(network): - p = Popen([ - '/bin/sh', - 'deposit.sh', - ], stdin=PIPE, cwd=sdc_git_dir) - p.communicate(input=TEST_MNEMONIC.encode('utf-8'))[0] - - -def lighthouse_generate(network): - result = subprocess.run([ - "lighthouse", - "--network", - network, - "account", - "wallet", - "recover", - "--datadir", - str(lh_dir), - "--name", - WALLET_NAME, - "--mnemonic-path", - str(mnemonic_path), - "--password-file", - str(lh_wallet_password_path) - ]) - assert(result.returncode == 0) - - result = subprocess.run([ - "lighthouse", - "--network", - network, - "account", - "validator", - "create", - "--datadir", - str(lh_dir), - "--wallet-name", - WALLET_NAME, - "--wallet-password", - str(lh_wallet_password_path), - "--count", - str(NUM_VALIDATORS), - "--json-deposit-data-path", - str(lh_json_path) - ]) - assert(result.returncode == 0) - - -def test(network): - setup() - sdc_generate(network) - #lighthouse_generate(network) - # cleanup() - - -test("mainnet") From 1b40dd7a4737b23c7b1f6f9d4f7b42bdfb8e2072 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 18:15:42 +1000 Subject: [PATCH 050/138] Revert changes to fork choice tests --- testing/ef_tests/src/cases/fork_choice.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 2d22d9cb531..650452d7831 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -153,7 +153,7 @@ impl Case for ForkChoiceTest { self.description.clone() } - fn result(&self, case_index: usize, fork_name: ForkName) -> Result<(), Error> { + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { let tester = Tester::new(self, testing_spec::(fork_name))?; for step in &self.steps { From 328a450b2fc183e3390aeb74998dc183c579cfbe Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 24 Aug 2022 18:21:18 +1000 Subject: [PATCH 051/138] Revert abandoned changes --- Cargo.lock | 2 - account_manager/src/validator/create.rs | 42 +-------------- common/validator_dir/Cargo.toml | 2 - common/validator_dir/src/validator_dir.rs | 65 +---------------------- 4 files changed, 2 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6740ba226d0..27cc1d89dbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7346,12 +7346,10 @@ dependencies = [ "deposit_contract", "derivative", "eth2_keystore", - "eth2_serde_utils", "filesystem", "hex", "lockfile", "rand 0.8.5", - "serde", "tempfile", "tree_hash", "types", diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 17ff97c9eaa..bbd2cbc9991 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -22,7 +22,6 @@ 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"; -pub const JSON_DEPOSIT_DATA_PATH: &str = "json-deposit-data-path"; 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:"; @@ -111,17 +110,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .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), - ) } pub fn cli_run( @@ -152,9 +140,6 @@ pub fn cli_run( let count: Option = clap_utils::parse_optional(matches, COUNT_FLAG)?; let at_most: Option = clap_utils::parse_optional(matches, AT_MOST_FLAG)?; - let json_deposit_data_path: Option = - clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; - // The command will always fail if the wallet dir does not exist. if !wallet_base_dir.exists() { return Err(format!( @@ -227,8 +212,6 @@ pub fn cli_run( ) })?; - let mut json_deposit_data = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); - for i in 0..n { let voting_password = random_password(); let withdrawal_password = random_password(); @@ -258,7 +241,7 @@ pub fn cli_run( ) })?; - let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) + ValidatorDirBuilder::new(validator_dir.clone()) .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) @@ -267,32 +250,9 @@ pub fn cli_run( .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; - if let Some(json_deposit_data) = &mut json_deposit_data { - let standard_deposit_data_json = validator_dir - .standard_deposit_data_json(&spec) - .map_err(|e| format!("Unable to create standard JSON deposit data: {:?}", e))?; - json_deposit_data.push(standard_deposit_data_json); - } - println!("{}/{}\t{}", i + 1, n, voting_pubkey.as_hex_string()); } - // If configured, create a single JSON file which contains deposit data information for all - // validators. - if let Some(json_deposit_data_path) = json_deposit_data_path { - let json_deposit_data = - json_deposit_data.ok_or("Internal error: JSON deposit data is None")?; - - let mut file = fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&json_deposit_data_path) - .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; - - serde_json::to_writer(&mut file, &json_deposit_data) - .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; - } - Ok(()) } diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 1ce0806a186..0eba4cf2327 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -20,8 +20,6 @@ tree_hash = "0.4.1" hex = "0.4.2" derivative = "2.1.1" lockfile = { path = "../lockfile" } -serde = { version = "1.0.116", features = ["derive"] } -eth2_serde_utils = "0.1.1" [dev-dependencies] tempfile = "3.1.0" diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index 0ef0cb590ef..cb1ddde24a4 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -6,12 +6,11 @@ use deposit_contract::decode_eth1_tx_data; use derivative::Derivative; use eth2_keystore::{Error as KeystoreError, Keystore, PlainText}; use lockfile::{Lockfile, LockfileError}; -use serde::{Deserialize, Serialize}; use std::fs::{read, write, File}; use std::io; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; -use types::*; +use types::{DepositData, Hash256, Keypair}; /// The file used to save the Eth1 transaction hash from a deposit. pub const ETH1_DEPOSIT_TX_HASH_FILE: &str = "eth1-deposit-tx-hash.txt"; @@ -42,8 +41,6 @@ pub enum Error { Eth1DepositRootMismatch, #[cfg(feature = "unencrypted_keys")] SszKeypairError(String), - DepositDataMissing, - ConfigNameUnspecified, } /// Information required to submit a deposit to the Eth1 deposit contract. @@ -57,26 +54,6 @@ pub struct Eth1DepositData { pub root: Hash256, } -/// 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, Deserialize)] -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, -} - /// Provides a wrapper around a directory containing validator information. /// /// Holds a lockfile in `self.dir` to attempt to prevent concurrent access from multiple @@ -226,46 +203,6 @@ impl ValidatorDir { root, })) } - - /// Calls `Self::eth1_deposit_data` and then builds a `StandardDepositDataJson` from the result. - /// - /// The provided `spec` must match the value that was used to create the deposit, otherwise - /// an inconsistent result may be returned. - pub fn standard_deposit_data_json( - &self, - spec: &ChainSpec, - ) -> Result { - let deposit_data = self.eth1_deposit_data()?.ok_or(Error::DepositDataMissing)?; - - let domain = spec.get_deposit_domain(); - let deposit_message_root = deposit_data - .deposit_data - .as_deposit_message() - .signing_root(domain); - - let deposit_data_root = deposit_data.deposit_data.tree_hash_root(); - - let DepositData { - pubkey, - withdrawal_credentials, - amount, - signature, - } = deposit_data.deposit_data; - - Ok(StandardDepositDataJson { - pubkey, - withdrawal_credentials, - amount, - signature, - fork_version: spec.genesis_fork_version, - eth2_network_name: spec - .config_name - .clone() - .ok_or(Error::ConfigNameUnspecified)?, - deposit_message_root, - deposit_data_root, - }) - } } /// Attempts to load and decrypt a Keypair given path to the keystore. From 40b1619317761cc7da657dad827941128dbab884 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 09:13:43 +1000 Subject: [PATCH 052/138] Don't run validator-manager tests in debug --- validator_manager/src/validators/create_validators.rs | 2 ++ validator_manager/src/validators/import_validators.rs | 2 ++ validator_manager/src/validators/move_validators.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 8c5d43d57a0..6d8d1b5a420 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -505,6 +505,8 @@ async fn run<'a, T: EthSpec>(config: CreateConfig, spec: &ChainSpec) -> Result<( Ok(()) } +// The tests use crypto and are too slow in debug. +#[cfg(not(debug_assertions))] #[cfg(test)] pub mod tests { use super::*; diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index c551e552a52..f1ad622502a 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -189,6 +189,8 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { Ok(()) } +// The tests use crypto and are too slow in debug. +#[cfg(not(debug_assertions))] #[cfg(test)] pub mod tests { use super::*; diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 8be2efb8455..02223df5872 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -529,6 +529,8 @@ pub fn backup_validator>( ) } +// The tests use crypto and are too slow in debug. +#[cfg(not(debug_assertions))] #[cfg(test)] mod test { use super::*; From 05bafee5a8bd070a3a958283f19c314e243976c8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 09:40:01 +1000 Subject: [PATCH 053/138] Retry rather than saving file to disk --- validator_manager/Cargo.toml | 2 +- .../src/validators/move_validators.rs | 131 ++++++++++-------- 2 files changed, 77 insertions(+), 56 deletions(-) diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index 6e56d523105..e32f35bb7be 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -21,10 +21,10 @@ eth2_serde_utils = "0.1.1" tree_hash = "0.4.1" eth2 = { path = "../common/eth2", features = ["lighthouse"]} hex = "0.4.2" +tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } [dev-dependencies] tempfile = "3.1.0" -tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } regex = "1.6.0" eth2_network_config = { path = "../common/eth2_network_config" } validator_client = { path = "../validator_client" } diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 02223df5872..055dd417a73 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -13,6 +13,8 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; +use std::time::Duration; +use tokio::time::sleep; use types::{Address, PublicKeyBytes}; pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; @@ -31,6 +33,8 @@ pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; const NO_VALIDATORS_MSG: &str = "No validators present on source validator client"; +const UPLOAD_RETRY_WAIT: Duration = Duration::from_secs(5); + pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about( @@ -410,6 +414,8 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } }; + let keystore_derivation_path = voting_keystore.0.path(); + let validator_specification = ValidatorSpecification { voting_keystore, voting_keystore_password, @@ -424,70 +430,85 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { // there doesn't appear to be much harm just adding them again. let ignore_duplicates = true; - match validator_specification - .clone() - .upload(&dest_http_client, ignore_duplicates) - .await - { - Ok(()) => eprintln!( + loop { + match validator_specification + .clone() + .upload(&dest_http_client, ignore_duplicates) + .await + { + Ok(()) => break, + e @ Err(UploadError::InvalidPublicKey) => { + eprintln!("Validator {} has an invalid public key", i); + return Err(format!("{:?}", e)); + } + Err(UploadError::DuplicateValidator(_)) => { + return Err( + "Duplicate validator detected when duplicates are ignored".to_string() + ); + } + Err(UploadError::FailedToListKeys(e)) => { + eprintln!( + "Failed to list keystores. Some keys may have been moved whilst \ + others may not. Error was {:?}", + e + ); + // Retry uploading this validator. + sleep_with_retry_message(&pubkey_to_move, keystore_derivation_path.as_deref()) + .await; + } + Err(UploadError::KeyUploadFailed(e)) => { + eprintln!( + "Failed to upload keystore. Some keys may have been moved whilst \ + others may not. Error was {:?}", + e + ); + // Retry uploading this validator. + sleep_with_retry_message(&pubkey_to_move, keystore_derivation_path.as_deref()) + .await; + } + Err(UploadError::FeeRecipientUpdateFailed(e)) => { + eprintln!( + "Failed to set fee recipient for validator {}. This value may need \ + to be set manually. Continuing with other validators. Error was {:?}", + i, e + ); + // Continue onto the next validator. + break; + } + Err(UploadError::PatchValidatorFailed(e)) => { + eprintln!( + "Failed to set some values on validator {} (e.g., builder, enabled or gas limit. \ + These values value may need to be set manually. Continuing with other validators. \ + Error was {:?}", + i, e + ); + // Continue onto the next validator. + break; + } + } + eprintln!( "Uploaded keystore {} of {} to the destination VC", i + 1, count - ), - e @ Err(UploadError::InvalidPublicKey) => { - eprintln!("Validator {} has an invalid public key", i); - return Err(format!("{:?}", e)); - } - Err(UploadError::DuplicateValidator(_)) => { - return Err("Duplicate validator detected when duplicates are ignored".to_string()); - } - Err(UploadError::FailedToListKeys(e)) => { - eprintln!( - "Failed to list keystores. Some keys may have been moved whilst \ - others may not.", - ); - backup_validator( - &validator_specification, - &working_directory_path, - &dest_vc_url, - &dest_vc_token_path, - ); - return Err(format!("{:?}", e)); - } - Err(UploadError::KeyUploadFailed(e)) => { - eprintln!( - "Failed to upload keystore. Some keys may have been moved whilst \ - others may not.", - ); - backup_validator( - &validator_specification, - &working_directory_path, - &dest_vc_url, - &dest_vc_token_path, - ); - return Err(format!("{:?}", e)); - } - Err(UploadError::FeeRecipientUpdateFailed(e)) => { - eprintln!( - "Failed to set fee recipient for validator {}. This value may need \ - to be set manually. Continuing with other validators. Error was {:?}", - i, e - ); - } - Err(UploadError::PatchValidatorFailed(e)) => { - eprintln!( - "Failed to set some values on validator {} (e.g., builder, enabled or gas limit. \ - These values value may need to be set manually. Continuing with other validators. \ - Error was {:?}", - i, e - ); - } + ); } } Ok(()) } +async fn sleep_with_retry_message(pubkey: &PublicKeyBytes, path: Option<&str>) { + let path = path.unwrap_or(""); + eprintln!( + "Sleeping for {:?} before retrying. Exiting the application before it completes \ + may result in the loss of a validator keystore. The keystore would need to be \ + restored from a backup or mnemonic. The keystore which may be lost has a public \ + key of {:?} and a derivation path of {}", + UPLOAD_RETRY_WAIT, pubkey, path + ); + sleep(UPLOAD_RETRY_WAIT).await +} + pub fn backup_validator>( validator_specification: &ValidatorSpecification, working_directory_path: P, From 0c32b5a48bc6d42814e1de09b266946dfe910de2 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 10:05:43 +1000 Subject: [PATCH 054/138] Remove intermediate file from `move` --- .../src/validators/move_validators.rs | 81 +------------------ 1 file changed, 1 insertion(+), 80 deletions(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 055dd417a73..64225db6e46 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -10,8 +10,7 @@ use eth2::{ }; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; use tokio::time::sleep; @@ -21,7 +20,6 @@ pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json"; pub const CMD: &str = "move"; -pub const WORKING_DIRECTORY_FLAG: &str = "working-directory"; pub const SRC_VALIDATOR_CLIENT_URL_FLAG: &str = "src-validator-client-url"; pub const SRC_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "src-validator-client-token"; pub const DEST_VALIDATOR_CLIENT_URL_FLAG: &str = "dest-validator-client-url"; @@ -42,18 +40,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { are defined in a JSON file which can be generated using the \"create-validators\" \ command.", ) - .arg( - Arg::with_name(WORKING_DIRECTORY_FLAG) - .long(WORKING_DIRECTORY_FLAG) - .value_name("PATH_TO_DIRECTORY") - .help( - "The path to a directory where the application can write files.\ - Under certain failure scenarios this directory may contain files which \ - can be used to recover validators.", - ) - .required(true) - .takes_value(true), - ) .arg( Arg::with_name(SRC_VALIDATOR_CLIENT_URL_FLAG) .long(SRC_VALIDATOR_CLIENT_URL_FLAG) @@ -162,7 +148,6 @@ impl FromStr for Validators { #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct MoveConfig { - pub working_directory_path: PathBuf, pub src_vc_url: SensitiveUrl, pub src_vc_token_path: PathBuf, pub dest_vc_url: SensitiveUrl, @@ -176,7 +161,6 @@ pub struct MoveConfig { impl MoveConfig { fn from_cli(matches: &ArgMatches) -> Result { Ok(Self { - working_directory_path: clap_utils::parse_required(matches, WORKING_DIRECTORY_FLAG)?, src_vc_url: clap_utils::parse_required(matches, SRC_VALIDATOR_CLIENT_URL_FLAG)?, src_vc_token_path: clap_utils::parse_required( matches, @@ -209,7 +193,6 @@ pub async fn cli_run<'a>( async fn run<'a>(config: MoveConfig) -> Result<(), String> { let MoveConfig { - working_directory_path, src_vc_url, src_vc_token_path, dest_vc_url, @@ -220,26 +203,6 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { gas_limit, } = config; - if !working_directory_path.exists() { - return Err(format!("{:?} does not exist", working_directory_path)); - } - - // Append another directory to the "working directory" provided by the user. By creating a new - // directory we can prove (to some degree) that we can write in the given directory. - // - // It also allows us to easily detect when another identical process is running or the previous - // run failed by checking to see if the directory already exists. - let working_directory_path = working_directory_path.join(MOVE_DIR_NAME); - if working_directory_path.exists() { - return Err(format!( - "{:?} already exists, exiting", - working_directory_path - )); - } - - fs::create_dir(&working_directory_path) - .map_err(|e| format!("Failed to create {:?}: {:?}", working_directory_path, e))?; - // Moving validators between the same VC is unlikely to be useful and probably indicates a user // error. if src_vc_url == dest_vc_url { @@ -509,47 +472,6 @@ async fn sleep_with_retry_message(pubkey: &PublicKeyBytes, path: Option<&str>) { sleep(UPLOAD_RETRY_WAIT).await } -pub fn backup_validator>( - validator_specification: &ValidatorSpecification, - working_directory_path: P, - dest_vc_url: &SensitiveUrl, - dest_vc_token_path: P, -) { - use crate::validators::import_validators::{ - CMD, VALIDATORS_FILE_FLAG, VALIDATOR_CLIENT_TOKEN_FLAG, VALIDATOR_CLIENT_URL_FLAG, - }; - - let validator_specification_path = working_directory_path - .as_ref() - .join(VALIDATOR_SPECIFICATION_FILE); - if let Err(e) = write_to_json_file(&validator_specification_path, &validator_specification) { - eprintln!( - "A validator was removed from the source validator client but it could not be \ - saved to disk after an upload failure. The validator may need to be recovered \ - from a backup or mnemonic. Error was {:?}", - e - ); - } - - eprintln!( - "It may be possible to recover this validator by running the following command: \n\n\ - lighthouse {} {} {} --{} {:?} --{} {} --{} {:?} \n\n\ - The {:?} directory contains a backup of the validator that was unable to be uploaded. \ - That backup contains the unencrypted validator secret key and should not be shared with \ - anyone. If the recovery command (above) succeeds, it is safe to remove that directory.", - crate::CMD, - crate::validators::CMD, - CMD, - VALIDATORS_FILE_FLAG, - validator_specification_path.as_os_str(), - VALIDATOR_CLIENT_URL_FLAG, - dest_vc_url.full, - VALIDATOR_CLIENT_TOKEN_FLAG, - dest_vc_token_path.as_ref().as_os_str(), - working_directory_path.as_ref().as_os_str(), - ) -} - // The tests use crypto and are too slow in debug. #[cfg(not(debug_assertions))] #[cfg(test)] @@ -616,7 +538,6 @@ mod test { fs::write(&dest_vc_token_path, &dest_vc.api_token).unwrap(); let move_config = MoveConfig { - working_directory_path: self.dir.path().into(), src_vc_url: src_vc.url.clone(), src_vc_token_path, dest_vc_url: dest_vc.url.clone(), From 64decbc65a9576f29a01babda0450285a98d11a8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 10:06:04 +1000 Subject: [PATCH 055/138] Handle status from import --- validator_manager/src/validators/common.rs | 24 ++++----- .../src/validators/import_validators.rs | 47 +++++++++++++++++- .../src/validators/move_validators.rs | 49 ++++++++++++++++++- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index d433ec8089a..d3f3d3f231d 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -3,7 +3,7 @@ use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::{ lighthouse_vc::{ http_client::ValidatorClientHttpClient, - std_types::{ImportKeystoresRequest, SingleKeystoreResponse}, + std_types::{ImportKeystoreStatus, ImportKeystoresRequest, SingleKeystoreResponse, Status}, types::UpdateFeeRecipientRequest, }, SensitiveUrl, @@ -31,6 +31,7 @@ pub enum UploadError { DuplicateValidator(PublicKeyBytes), FailedToListKeys(eth2::Error), KeyUploadFailed(eth2::Error), + IncorrectStatusCount(usize), FeeRecipientUpdateFailed(eth2::Error), PatchValidatorFailed(eth2::Error), } @@ -52,7 +53,7 @@ impl ValidatorSpecification { self, http_client: &ValidatorClientHttpClient, ignore_duplicates: bool, - ) -> Result<(), UploadError> { + ) -> Result, UploadError> { let ValidatorSpecification { voting_keystore, voting_keystore_password, @@ -98,14 +99,15 @@ impl ValidatorSpecification { } }; - if let Err(e) = http_client.post_keystores(&request).await { - // Return here *without* writing the deposit JSON file. This might help prevent - // users from submitting duplicate deposits or deposits for validators that weren't - // initialized on a VC. - // - // Next the the user runs with the --ignore-duplicates flag there should be a new, - // complete deposit JSON file created. - return Err(UploadError::KeyUploadFailed(e)); + let mut statuses = http_client + .post_keystores(&request) + .await + .map_err(UploadError::KeyUploadFailed)? + .data; + + let status = statuses.pop().ok_or(UploadError::IncorrectStatusCount(0))?; + if !statuses.is_empty() { + return Err(UploadError::IncorrectStatusCount(statuses.len() + 1)); } if let Some(fee_recipient) = fee_recipient { @@ -132,7 +134,7 @@ impl ValidatorSpecification { .map_err(UploadError::PatchValidatorFailed)?; } - Ok(()) + Ok(status) } } diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index f1ad622502a..74228b1e1e0 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -1,7 +1,7 @@ use super::common::*; use crate::DumpConfig; use clap::{App, Arg, ArgMatches}; -use eth2::SensitiveUrl; +use eth2::{lighthouse_vc::std_types::ImportKeystoreStatus, SensitiveUrl}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; @@ -134,7 +134,38 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { for (i, validator) in validators.into_iter().enumerate() { match validator.upload(&http_client, ignore_duplicates).await { - Ok(()) => eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count), + Ok(status) => { + match status.status { + ImportKeystoreStatus::Imported => { + eprintln!("Uploaded keystore {} of {} to the VC", i + 1, count) + } + ImportKeystoreStatus::Duplicate => { + if ignore_duplicates { + eprintln!("Re-uploaded keystore {} of {} to the VC", i + 1, count) + } else { + eprintln!( + "Keystore {} of {} was uploaded to the VC, but it was a duplicate. \ + Exiting now, use --{} to allow duplicates.", + i + 1, count, IGNORE_DUPLICATES_FLAG + ); + return Err(DETECTED_DUPLICATE_MESSAGE.to_string()); + } + } + ImportKeystoreStatus::Error => { + eprintln!( + "Upload of keystore {} of {} failed with message: {:?}. \ + A potential solution is run this command again \ + using the --{} flag, however care should be taken to ensure \ + that there are no duplicate deposits submitted.", + i + 1, + count, + status.message, + IGNORE_DUPLICATES_FLAG + ); + return Err(format!("Upload failed with {:?}", status.message)); + } + } + } e @ Err(UploadError::InvalidPublicKey) => { eprintln!("Validator {} has an invalid public key", i); return Err(format!("{:?}", e)); @@ -168,6 +199,18 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { ); return Err(format!("{:?}", e)); } + Err(UploadError::IncorrectStatusCount(count)) => { + eprintln!( + "Keystore was uploaded, however the validator client returned an invalid response. \ + A potential solution is run this command again using the --{} flag, however care \ + should be taken to ensure that there are no duplicate deposits submitted.", + IGNORE_DUPLICATES_FLAG + ); + return Err(format!( + "Invalid status count in import response: {}", + count + )); + } Err(UploadError::FeeRecipientUpdateFailed(e)) => { eprintln!( "Failed to set fee recipient for validator {}. This value may need \ diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 64225db6e46..f9576260dda 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -3,7 +3,10 @@ use crate::DumpConfig; use clap::{App, Arg, ArgMatches}; use eth2::{ lighthouse_vc::{ - std_types::{DeleteKeystoreStatus, DeleteKeystoresRequest, InterchangeJsonStr, Status}, + std_types::{ + DeleteKeystoreStatus, DeleteKeystoresRequest, ImportKeystoreStatus, InterchangeJsonStr, + Status, + }, types::{ExportKeystoresResponse, SingleExportKeystoresResponse}, }, SensitiveUrl, @@ -399,7 +402,37 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { .upload(&dest_http_client, ignore_duplicates) .await { - Ok(()) => break, + Ok(status) => { + match status.status { + ImportKeystoreStatus::Imported => { + eprintln!("Moved keystore {} of {}", i + 1, count); + break; + } + ImportKeystoreStatus::Duplicate => { + eprintln!("Moved duplicate keystore {} of {} to the VC", i + 1, count); + break; + } + ImportKeystoreStatus::Error => { + eprintln!( + "Upload of keystore {} of {} failed with message: {:?}. \ + A potential solution is run this command again \ + using the --{} flag, however care should be taken to ensure \ + that there are no duplicate deposits submitted.", + i + 1, + count, + status.message, + IGNORE_DUPLICATES_FLAG + ); + // Retry uploading this validator. + sleep_with_retry_message( + &pubkey_to_move, + keystore_derivation_path.as_deref(), + ) + .await; + return Err(format!("Upload failed with {:?}", status.message)); + } + } + } e @ Err(UploadError::InvalidPublicKey) => { eprintln!("Validator {} has an invalid public key", i); return Err(format!("{:?}", e)); @@ -429,6 +462,18 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { sleep_with_retry_message(&pubkey_to_move, keystore_derivation_path.as_deref()) .await; } + Err(UploadError::IncorrectStatusCount(count)) => { + eprintln!( + "Keystore was uploaded, however the validator client returned an invalid response. \ + A potential solution is run this command again using the --{} flag, however care \ + should be taken to ensure that there are no duplicate deposits submitted.", + IGNORE_DUPLICATES_FLAG + ); + return Err(format!( + "Invalid status count in import response: {}", + count + )); + } Err(UploadError::FeeRecipientUpdateFailed(e)) => { eprintln!( "Failed to set fee recipient for validator {}. This value may need \ From 0c786c0ee241f2c798a1dc347ca8c4f23cb825fc Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 10:26:55 +1000 Subject: [PATCH 056/138] Add tests for duplicates --- .../src/validators/move_validators.rs | 123 +++++++++++++++--- 1 file changed, 107 insertions(+), 16 deletions(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index f9576260dda..f12c22f0cdd 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -393,7 +393,8 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { }; // We might as well just ignore validators that already exist on the destination machine, - // there doesn't appear to be much harm just adding them again. + // there doesn't appear to be much harm just adding them again and removing them from the + // source VC is an improvement. let ignore_duplicates = true; loop { @@ -531,7 +532,9 @@ mod test { const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; struct TestBuilder { - import_builder: Option, + src_import_builder: Option, + dest_import_builder: Option, + duplicates: usize, dir: TempDir, } @@ -539,7 +542,9 @@ mod test { async fn new() -> Self { let dir = tempdir().unwrap(); Self { - import_builder: None, + src_import_builder: None, + dest_import_builder: None, + duplicates: 0, dir, } } @@ -549,7 +554,21 @@ mod test { .await .create_validators(count, first_index) .await; - self.import_builder = Some(builder); + self.src_import_builder = Some(builder); + self + } + + async fn with_dest_validators(mut self, count: u32, first_index: u32) -> Self { + let builder = ImportTestBuilder::new() + .await + .create_validators(count, first_index) + .await; + self.dest_import_builder = Some(builder); + self + } + + fn register_duplicates(mut self, num_duplicates: usize) -> Self { + self.duplicates = num_duplicates; self } @@ -557,7 +576,7 @@ mod test { where F: Fn(&[PublicKeyBytes]) -> Validators, { - let src_vc = if let Some(import_builder) = self.import_builder { + let src_vc = if let Some(import_builder) = self.src_import_builder { let import_test_result = import_builder.run_test().await; assert!(import_test_result.result.is_ok()); import_test_result.vc @@ -578,10 +597,21 @@ mod test { .collect(); let validators = gen_validators_enum(&src_vc_initial_pubkeys); - let dest_vc = ApiTester::new().await; + let dest_vc = if let Some(import_builder) = self.dest_import_builder { + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + import_test_result.vc + } else { + ApiTester::new().await + }; let dest_vc_token_path = self.dir.path().join(DEST_VC_TOKEN_FILE_NAME); fs::write(&dest_vc_token_path, &dest_vc.api_token).unwrap(); + let (dest_vc_client, dest_vc_initial_keystores) = + vc_http_client(dest_vc.url.clone(), &dest_vc_token_path) + .await + .unwrap(); + let move_config = MoveConfig { src_vc_url: src_vc.url.clone(), src_vc_token_path, @@ -595,17 +625,26 @@ mod test { let result = run(move_config).await; - let (_dest_vc_client, dest_vc_keystores) = - vc_http_client(dest_vc.url.clone(), &dest_vc_token_path) - .await - .unwrap(); let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; + let dest_vc_final_keystores = dest_vc_client.get_keystores().await.unwrap().data; match validators { Validators::All => { - assert!(src_vc_final_keystores.is_empty()); + assert!( + src_vc_final_keystores.is_empty(), + "all keystores should be removed from source vc" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + src_vc_initial_keystores.len() + - self.duplicates, + "the correct count of keystores should have been moved to the dest" + ); for initial_keystore in &src_vc_initial_keystores { - assert!(dest_vc_keystores.contains(initial_keystore)) + assert!( + dest_vc_final_keystores.contains(initial_keystore), + "the source keystore should be present at the dest" + ) } } Validators::Some(pubkeys) => { @@ -614,16 +653,27 @@ mod test { src_vc_initial_keystores .len() .checked_sub(pubkeys.len()) - .unwrap() + .unwrap(), + "the correct count of validators should have been removed from the src" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + pubkeys.len() - self.duplicates, + "the correct count of keystores should have been moved to the dest" ); - assert_eq!(dest_vc_keystores.len(), pubkeys.len()); for pubkey in pubkeys { let initial_keystore = src_vc_initial_keystores .iter() .find(|k| k.validating_pubkey == pubkey) .unwrap(); - assert!(!src_vc_final_keystores.contains(initial_keystore)); - assert!(dest_vc_keystores.contains(initial_keystore)); + assert!( + !src_vc_final_keystores.contains(initial_keystore), + "the keystore should not be present at the source" + ); + assert!( + dest_vc_final_keystores.contains(initial_keystore), + "the keystore should be present at the dest" + ); } } } @@ -678,6 +728,47 @@ mod test { .assert_ok(); } + #[tokio::test] + async fn one_validator_to_non_empty_dest() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .with_dest_validators(1, 10) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validators_move_all_where_one_is_a_duplicate() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .with_dest_validators(1, 1) + .await + .register_duplicates(1) + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validators_move_one_where_one_is_a_duplicate() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .with_dest_validators(2, 0) + .await + .register_duplicates(1) + .run_test(|pubkeys| Validators::Some(pubkeys[0..1].to_vec())) + .await + .assert_ok(); + } + #[tokio::test] async fn three_validators_move_all() { TestBuilder::new() From 7f89f1efa7c4c9b28d2265f9d173bfe25edfbc3b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 11:29:31 +1000 Subject: [PATCH 057/138] Add `move` tests, "builder-proposals" takes bool --- lighthouse/tests/validator_manager.rs | 102 +++++++++++++++++- .../src/validators/create_validators.rs | 15 ++- validator_manager/src/validators/mod.rs | 4 + .../src/validators/move_validators.rs | 15 ++- 4 files changed, 122 insertions(+), 14 deletions(-) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index a0737032d43..3ce64fe70ef 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -8,11 +8,16 @@ use std::str::FromStr; use tempfile::{tempdir, TempDir}; use types::*; use validator_manager::validators::{ - create_validators::CreateConfig, import_validators::ImportConfig, + create_validators::CreateConfig, + import_validators::ImportConfig, + move_validators::{MoveConfig, Validators}, }; const EXAMPLE_ETH1_ADDRESS: &str = "0x00000000219ab540356cBB839Cbe05303d7705Fa"; +const EXAMPLE_PUBKEY_0: &str = "0x933ad9491b62059dd065b560d256d8957a8c402cc6e8d8ee7290ae11e8f7329267a8811c397529dac52ae1342ba58c95"; +const EXAMPLE_PUBKEY_1: &str = "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c"; + struct CommandLineTest { cmd: Command, config_path: PathBuf, @@ -93,6 +98,12 @@ impl CommandLineTest { } } +impl CommandLineTest { + fn validators_move() -> Self { + Self::default().flag("validators", None).flag("move", None) + } +} + #[test] pub fn validator_create_without_output_path() { CommandLineTest::validators_create().assert_failed(); @@ -114,7 +125,7 @@ pub fn validator_create_defaults() { disable_deposits: false, specify_voting_keystore_password: false, eth1_withdrawal_address: None, - builder_proposals: false, + builder_proposals: None, fee_recipient: None, gas_limit: None, bn_url: None, @@ -134,7 +145,7 @@ pub fn validator_create_misc_flags() { .flag("--stdin-inputs", None) .flag("--specify-voting-keystore-password", None) .flag("--eth1-withdrawal-address", Some(EXAMPLE_ETH1_ADDRESS)) - .flag("--builder-proposals", None) + .flag("--builder-proposals", Some("true")) .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) .flag("--gas-limit", Some("1337")) .flag("--beacon-node", Some("http://localhost:1001")) @@ -149,7 +160,7 @@ pub fn validator_create_misc_flags() { disable_deposits: false, specify_voting_keystore_password: true, eth1_withdrawal_address: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), - builder_proposals: true, + builder_proposals: Some(true), fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), gas_limit: Some(1337), bn_url: Some(SensitiveUrl::parse("http://localhost:1001").unwrap()), @@ -164,8 +175,10 @@ pub fn validator_create_disable_deposits() { .flag("--output-path", Some("./meow")) .flag("--count", Some("1")) .flag("--disable-deposits", None) + .flag("--builder-proposals", Some("false")) .assert_success(|config| { assert_eq!(config.disable_deposits, true); + assert_eq!(config.builder_proposals, Some(false)); }); } @@ -215,3 +228,84 @@ pub fn validator_import_missing_validators_file() { .flag("--validator-client-token", Some("./token.json")) .assert_failed(); } + +#[test] +pub fn validator_move_defaults() { + CommandLineTest::validators_move() + .flag("--src-validator-client-url", Some("http://localhost:1")) + .flag("--src-validator-client-token", Some("./1.json")) + .flag("--dest-validator-client-url", Some("http://localhost:2")) + .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--validators", Some("all")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), + dest_vc_token_path: PathBuf::from("./2.json"), + validators: Validators::All, + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_move_misc_flags_0() { + CommandLineTest::validators_move() + .flag("--src-validator-client-url", Some("http://localhost:1")) + .flag("--src-validator-client-token", Some("./1.json")) + .flag("--dest-validator-client-url", Some("http://localhost:2")) + .flag("--dest-validator-client-token", Some("./2.json")) + .flag( + "--validators", + Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), + ) + .flag("--builder-proposals", Some("true")) + .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) + .flag("--gas-limit", Some("1337")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), + dest_vc_token_path: PathBuf::from("./2.json"), + validators: Validators::Some(vec![ + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(), + ]), + builder_proposals: Some(true), + fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), + gas_limit: Some(1337), + }; + assert_eq!(expected, config); + }); +} + +#[test] +pub fn validator_move_misc_flags_1() { + CommandLineTest::validators_move() + .flag("--src-validator-client-url", Some("http://localhost:1")) + .flag("--src-validator-client-token", Some("./1.json")) + .flag("--dest-validator-client-url", Some("http://localhost:2")) + .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) + .flag("--builder-proposals", Some("false")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), + dest_vc_token_path: PathBuf::from("./2.json"), + validators: Validators::Some(vec![ + PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap() + ]), + builder_proposals: Some(false), + fee_recipient: None, + gas_limit: None, + }; + assert_eq!(expected, config); + }); +} diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 6d8d1b5a420..01c1bc71e5b 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -158,7 +158,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { "When provided, all created validators will attempt to create \ blocks via builder rather than the local EL.", ) - .required(false), + .required(false) + .possible_values(&["true", "false"]) + .takes_value(true), ) .arg( Arg::with_name(BEACON_NODE_FLAG) @@ -188,7 +190,7 @@ pub struct CreateConfig { pub disable_deposits: bool, pub specify_voting_keystore_password: bool, pub eth1_withdrawal_address: Option
, - pub builder_proposals: bool, + pub builder_proposals: Option, pub fee_recipient: Option
, pub gas_limit: Option, pub bn_url: Option, @@ -211,7 +213,7 @@ impl CreateConfig { matches, ETH1_WITHDRAWAL_ADDRESS_FLAG, )?, - builder_proposals: matches.is_present(BUILDER_PROPOSALS_FLAG), + builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, bn_url: clap_utils::parse_optional(matches, BEACON_NODE_FLAG)?, @@ -433,8 +435,11 @@ impl ValidatorsAndDeposits { slashing_protection: None, fee_recipient, gas_limit, - builder_proposals: Some(builder_proposals), - enabled: Some(true), + builder_proposals, + // Allow the VC to choose a default "enabled" state. Since "enabled" is not part of + // the standard API, leaving this as `None` means we are not forced to use the + // non-standard API. + enabled: None, }; eprintln!( diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs index 4e502647da6..eeb0a029622 100644 --- a/validator_manager/src/validators/mod.rs +++ b/validator_manager/src/validators/mod.rs @@ -14,6 +14,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .about("Provides commands for managing validators in a Lighthouse Validator Client.") .subcommand(create_validators::cli_app()) .subcommand(import_validators::cli_app()) + .subcommand(move_validators::cli_app()) } pub async fn cli_run<'a, T: EthSpec>( @@ -28,6 +29,9 @@ pub async fn cli_run<'a, T: EthSpec>( (import_validators::CMD, Some(matches)) => { import_validators::cli_run(matches, dump_config).await } + (move_validators::CMD, Some(matches)) => { + move_validators::cli_run(matches, dump_config).await + } (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index f12c22f0cdd..9ccb0286e6c 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -124,7 +124,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { "When provided, all created validators will attempt to create \ blocks via builder rather than the local EL.", ) - .required(false), + .required(false) + .possible_values(&["true", "false"]) + .takes_value(true), ) } @@ -156,7 +158,7 @@ pub struct MoveConfig { pub dest_vc_url: SensitiveUrl, pub dest_vc_token_path: PathBuf, pub validators: Validators, - pub builder_proposals: bool, + pub builder_proposals: Option, pub fee_recipient: Option
, pub gas_limit: Option, } @@ -175,7 +177,7 @@ impl MoveConfig { DEST_VALIDATOR_CLIENT_TOKEN_FLAG, )?, validators: clap_utils::parse_required(matches, VALIDATORS_FLAG)?, - builder_proposals: matches.is_present(BUILDER_PROPOSALS_FLAG), + builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, }) @@ -388,8 +390,11 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { slashing_protection: Some(InterchangeJsonStr(slashing_protection)), fee_recipient, gas_limit, - builder_proposals: Some(builder_proposals), - enabled: Some(true), + builder_proposals, + // Allow the VC to choose a default "enabled" state. Since "enabled" is not part of + // the standard API, leaving this as `None` means we are not forced to use the + // non-standard API. + enabled: None, }; // We might as well just ignore validators that already exist on the destination machine, From e5476cd6b70f6329f9d7d2a54778156c9a75df83 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 11:46:58 +1000 Subject: [PATCH 058/138] Fix tests for windows --- lighthouse/tests/validator_manager.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 3ce64fe70ef..eeca1b8084d 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -121,7 +121,7 @@ pub fn validator_create_defaults() { count: 1, deposit_gwei: MainnetEthSpec::default_spec().max_effective_balance, mnemonic_path: None, - stdin_inputs: false, + stdin_inputs: cfg!(windows) || false, disable_deposits: false, specify_voting_keystore_password: false, eth1_withdrawal_address: None, @@ -156,7 +156,7 @@ pub fn validator_create_misc_flags() { count: 9, deposit_gwei: 42, mnemonic_path: Some(PathBuf::from("./woof")), - stdin_inputs: true, + stdin_inputs: cfg!(windows) || true, disable_deposits: false, specify_voting_keystore_password: true, eth1_withdrawal_address: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), From 89b233449c34598bf3f38d79dd897945c9764698 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 12:10:21 +1000 Subject: [PATCH 059/138] Use common version of regex across workspace --- beacon_node/builder_client/Cargo.toml | 2 +- beacon_node/lighthouse_network/Cargo.toml | 2 +- common/account_utils/Cargo.toml | 2 +- common/lighthouse_version/Cargo.toml | 2 +- common/monitoring_api/Cargo.toml | 2 +- consensus/types/Cargo.toml | 2 +- testing/node_test_rig/Cargo.toml | 2 +- validator_manager/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index c4d21c59ab8..48ac0300c98 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -9,4 +9,4 @@ reqwest = { version = "0.11.0", features = ["json","stream"] } sensitive_url = { path = "../../common/sensitive_url" } eth2 = { path = "../../common/eth2" } serde = { version = "1.0.116", features = ["derive"] } -serde_json = "1.0.58" \ No newline at end of file +serde_json = "1.0.58" diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index c6ba5305088..39aa6e629c7 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -34,7 +34,7 @@ tiny-keccak = "2.0.2" task_executor = { path = "../../common/task_executor" } rand = "0.8.5" directory = { path = "../../common/directory" } -regex = "1.5.5" +regex = "1.5.4" strum = { version = "0.24.0", features = ["derive"] } superstruct = "0.5.0" prometheus-client = "0.16.0" diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index ccff88ceef3..be042b70082 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -18,6 +18,6 @@ serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } types = { path = "../../consensus/types" } validator_dir = { path = "../validator_dir" } -regex = "1.5.5" +regex = "1.5.4" rpassword = "5.0.0" directory = { path = "../directory" } diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 96ac266476c..51d2d44ddc7 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -11,4 +11,4 @@ git-version = "0.3.4" target_info = "0.1.0" [dev-dependencies] -regex = "1.5.5" +regex = "1.5.4" diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index bfb5e720423..9d84da63472 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -19,5 +19,5 @@ lighthouse_metrics = { path = "../lighthouse_metrics" } slog = "2.5.2" store = { path = "../../beacon_node/store" } lazy_static = "1.4.0" -regex = "1.5.5" +regex = "1.5.4" sensitive_url = { path = "../sensitive_url" } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 68fdbf7990d..a7a34db8ce5 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -39,7 +39,7 @@ derivative = "2.1.1" rusqlite = { version = "0.25.3", features = ["bundled"], optional = true } arbitrary = { version = "1.0", features = ["derive"], optional = true } eth2_serde_utils = "0.1.1" -regex = "1.5.5" +regex = "1.5.4" lazy_static = "1.4.0" parking_lot = "0.12.0" itertools = "0.10.0" diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 2c9bd5939f8..ea5d005c16a 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -13,4 +13,4 @@ eth2 = { path = "../../common/eth2" } validator_client = { path = "../../validator_client" } validator_dir = { path = "../../common/validator_dir", features = ["insecure_keys"] } sensitive_url = { path = "../../common/sensitive_url" } -execution_layer = { path = "../../beacon_node/execution_layer" } \ No newline at end of file +execution_layer = { path = "../../beacon_node/execution_layer" } diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index e32f35bb7be..e1f493b5bf9 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -25,6 +25,6 @@ tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } [dev-dependencies] tempfile = "3.1.0" -regex = "1.6.0" +regex = "1.5.4" eth2_network_config = { path = "../common/eth2_network_config" } validator_client = { path = "../validator_client" } From e8da2810ee87c3af5eade2b2a4624dac0a07c165 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 12:19:08 +1000 Subject: [PATCH 060/138] Revert "Use common version of regex across workspace" This reverts commit 7f74036465fbf1aec89b056363177c074b7ab978. --- beacon_node/builder_client/Cargo.toml | 2 +- beacon_node/lighthouse_network/Cargo.toml | 2 +- common/account_utils/Cargo.toml | 2 +- common/lighthouse_version/Cargo.toml | 2 +- common/monitoring_api/Cargo.toml | 2 +- consensus/types/Cargo.toml | 2 +- testing/node_test_rig/Cargo.toml | 2 +- validator_manager/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/beacon_node/builder_client/Cargo.toml b/beacon_node/builder_client/Cargo.toml index 48ac0300c98..c4d21c59ab8 100644 --- a/beacon_node/builder_client/Cargo.toml +++ b/beacon_node/builder_client/Cargo.toml @@ -9,4 +9,4 @@ reqwest = { version = "0.11.0", features = ["json","stream"] } sensitive_url = { path = "../../common/sensitive_url" } eth2 = { path = "../../common/eth2" } serde = { version = "1.0.116", features = ["derive"] } -serde_json = "1.0.58" +serde_json = "1.0.58" \ No newline at end of file diff --git a/beacon_node/lighthouse_network/Cargo.toml b/beacon_node/lighthouse_network/Cargo.toml index 39aa6e629c7..c6ba5305088 100644 --- a/beacon_node/lighthouse_network/Cargo.toml +++ b/beacon_node/lighthouse_network/Cargo.toml @@ -34,7 +34,7 @@ tiny-keccak = "2.0.2" task_executor = { path = "../../common/task_executor" } rand = "0.8.5" directory = { path = "../../common/directory" } -regex = "1.5.4" +regex = "1.5.5" strum = { version = "0.24.0", features = ["derive"] } superstruct = "0.5.0" prometheus-client = "0.16.0" diff --git a/common/account_utils/Cargo.toml b/common/account_utils/Cargo.toml index be042b70082..ccff88ceef3 100644 --- a/common/account_utils/Cargo.toml +++ b/common/account_utils/Cargo.toml @@ -18,6 +18,6 @@ serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } types = { path = "../../consensus/types" } validator_dir = { path = "../validator_dir" } -regex = "1.5.4" +regex = "1.5.5" rpassword = "5.0.0" directory = { path = "../directory" } diff --git a/common/lighthouse_version/Cargo.toml b/common/lighthouse_version/Cargo.toml index 51d2d44ddc7..96ac266476c 100644 --- a/common/lighthouse_version/Cargo.toml +++ b/common/lighthouse_version/Cargo.toml @@ -11,4 +11,4 @@ git-version = "0.3.4" target_info = "0.1.0" [dev-dependencies] -regex = "1.5.4" +regex = "1.5.5" diff --git a/common/monitoring_api/Cargo.toml b/common/monitoring_api/Cargo.toml index 9d84da63472..bfb5e720423 100644 --- a/common/monitoring_api/Cargo.toml +++ b/common/monitoring_api/Cargo.toml @@ -19,5 +19,5 @@ lighthouse_metrics = { path = "../lighthouse_metrics" } slog = "2.5.2" store = { path = "../../beacon_node/store" } lazy_static = "1.4.0" -regex = "1.5.4" +regex = "1.5.5" sensitive_url = { path = "../sensitive_url" } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index a7a34db8ce5..68fdbf7990d 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -39,7 +39,7 @@ derivative = "2.1.1" rusqlite = { version = "0.25.3", features = ["bundled"], optional = true } arbitrary = { version = "1.0", features = ["derive"], optional = true } eth2_serde_utils = "0.1.1" -regex = "1.5.4" +regex = "1.5.5" lazy_static = "1.4.0" parking_lot = "0.12.0" itertools = "0.10.0" diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index ea5d005c16a..2c9bd5939f8 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -13,4 +13,4 @@ eth2 = { path = "../../common/eth2" } validator_client = { path = "../../validator_client" } validator_dir = { path = "../../common/validator_dir", features = ["insecure_keys"] } sensitive_url = { path = "../../common/sensitive_url" } -execution_layer = { path = "../../beacon_node/execution_layer" } +execution_layer = { path = "../../beacon_node/execution_layer" } \ No newline at end of file diff --git a/validator_manager/Cargo.toml b/validator_manager/Cargo.toml index e1f493b5bf9..e32f35bb7be 100644 --- a/validator_manager/Cargo.toml +++ b/validator_manager/Cargo.toml @@ -25,6 +25,6 @@ tokio = { version = "1.14.0", features = ["time", "rt-multi-thread", "macros"] } [dev-dependencies] tempfile = "3.1.0" -regex = "1.5.4" +regex = "1.6.0" eth2_network_config = { path = "../common/eth2_network_config" } validator_client = { path = "../validator_client" } From 1f8f12541b064054e8b9cd6563798b69d1e54f1d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 15:47:18 +1000 Subject: [PATCH 061/138] Add keystore export flag to VC --- lighthouse/tests/validator_client.rs | 13 +++++++++++++ validator_client/src/cli.rs | 12 ++++++++++++ validator_client/src/config.rs | 4 ++++ 3 files changed, 29 insertions(+) diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index a9b76c27548..d56f3a28752 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -311,6 +311,19 @@ fn http_allow_origin_all_flag() { .run() .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); } +#[test] +fn http_allow_keystore_export_default() { + CommandLineTest::new() + .run() + .with_config(|config| assert!(!config.http_api.allow_keystore_export)); +} +#[test] +fn http_allow_keystore_export_present() { + CommandLineTest::new() + .flag("http-allow-keystore-export", None) + .run() + .with_config(|config| assert!(config.http_api.allow_keystore_export)); +} // Tests for Metrics flags. #[test] diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 5c7205a4ae4..916dc113b76 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -188,6 +188,18 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { address of this server (e.g., http://localhost:5062).") .takes_value(true), ) + .arg( + Arg::with_name("http-allow-keystore-export") + .long("http-allow-keystore-export") + .value_name("ORIGIN") + .help("If present, allow access to the DELETE /lighthouse/keystores HTTP \ + API method, which allows exporting keystores and passwords to HTTP API \ + consumers who have access to the API token. This method is useful for \ + exporting validators, however it should be used with caution since it \ + exposes private key data to authorized users.") + .required(false) + .takes_value(false), + ) /* Prometheus metrics HTTP server related arguments */ .arg( Arg::with_name("metrics") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index 22472f75124..fd10b2de1dd 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -255,6 +255,10 @@ impl Config { config.http_api.allow_origin = Some(allow_origin.to_string()); } + if cli_args.is_present("http-allow-keystore-export") { + config.http_api.allow_keystore_export = true; + } + /* * Prometheus metrics HTTP server */ From be8463770fd52eab1e94ed2259dc14662e44eb02 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 25 Aug 2022 16:42:55 +1000 Subject: [PATCH 062/138] Add count to `move`, fix tests --- common/eth2/src/lighthouse_vc/std_types.rs | 2 +- lighthouse/tests/validator_manager.rs | 27 ++- .../src/validators/create_validators.rs | 8 +- .../src/validators/move_validators.rs | 196 +++++++++++++----- 4 files changed, 172 insertions(+), 61 deletions(-) diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index 101d71c3b3d..077850b0302 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -28,7 +28,7 @@ pub struct ListKeystoresResponse { pub data: Vec, } -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Hash)] pub struct SingleKeystoreResponse { pub validating_pubkey: PublicKeyBytes, pub derivation_path: Option, diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index eeca1b8084d..6a0abebe7e9 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -272,7 +272,7 @@ pub fn validator_move_misc_flags_0() { src_vc_token_path: PathBuf::from("./1.json"), dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), dest_vc_token_path: PathBuf::from("./2.json"), - validators: Validators::Some(vec![ + validators: Validators::Specific(vec![ PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap(), PublicKeyBytes::from_str(EXAMPLE_PUBKEY_1).unwrap(), ]), @@ -299,7 +299,7 @@ pub fn validator_move_misc_flags_1() { src_vc_token_path: PathBuf::from("./1.json"), dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), dest_vc_token_path: PathBuf::from("./2.json"), - validators: Validators::Some(vec![ + validators: Validators::Specific(vec![ PublicKeyBytes::from_str(EXAMPLE_PUBKEY_0).unwrap() ]), builder_proposals: Some(false), @@ -309,3 +309,26 @@ pub fn validator_move_misc_flags_1() { assert_eq!(expected, config); }); } + +#[test] +pub fn validator_move_count() { + CommandLineTest::validators_move() + .flag("--src-validator-client-url", Some("http://localhost:1")) + .flag("--src-validator-client-token", Some("./1.json")) + .flag("--dest-validator-client-url", Some("http://localhost:2")) + .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--validators", Some("42")) + .assert_success(|config| { + let expected = MoveConfig { + src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), + src_vc_token_path: PathBuf::from("./1.json"), + dest_vc_url: SensitiveUrl::parse("http://localhost:2").unwrap(), + dest_vc_token_path: PathBuf::from("./2.json"), + validators: Validators::Count(42), + builder_proposals: None, + fee_recipient: None, + gas_limit: None, + }; + assert_eq!(expected, config); + }); +} diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/validators/create_validators.rs index 01c1bc71e5b..0692d337d40 100644 --- a/validator_manager/src/validators/create_validators.rs +++ b/validator_manager/src/validators/create_validators.rs @@ -560,7 +560,7 @@ pub mod tests { disable_deposits: false, specify_voting_keystore_password: false, eth1_withdrawal_address: None, - builder_proposals: false, + builder_proposals: None, fee_recipient: None, gas_limit: None, bn_url: None, @@ -610,8 +610,8 @@ pub mod tests { assert!(validator.slashing_protection.is_none()); assert_eq!(validator.fee_recipient, config.fee_recipient); assert_eq!(validator.gas_limit, config.gas_limit); - assert_eq!(validator.builder_proposals, Some(config.builder_proposals)); - assert_eq!(validator.enabled, Some(true)); + assert_eq!(validator.builder_proposals, config.builder_proposals); + assert_eq!(validator.enabled, None); } let deposits_path = output_dir.path().join(DEPOSITS_FILENAME); @@ -759,7 +759,7 @@ pub mod tests { TestBuilder::default() .mutate_config(|config| { config.deposit_gwei = 42; - config.builder_proposals = true; + config.builder_proposals = Some(true); config.gas_limit = Some(1337); }) .run_test() diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 9ccb0286e6c..1d5b89c7c7f 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -133,7 +133,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub enum Validators { All, - Some(Vec), + Count(usize), + Specific(Vec), } impl FromStr for Validators { @@ -142,11 +143,16 @@ impl FromStr for Validators { fn from_str(s: &str) -> Result { match s { "all" => Ok(Validators::All), - other => other + pubkeys if pubkeys.starts_with("0x") => pubkeys .split(',') .map(PublicKeyBytes::from_str) .collect::>() - .map(Validators::Some), + .map(Validators::Specific), + other => usize::from_str(other) + .map_err(|_| { + format!("Expected \"all\", a list of 0x-prefixed pubkeys or an integer") + }) + .map(Validators::Count), } } } @@ -228,7 +234,26 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { let pubkeys_to_move = match validators { Validators::All => src_keystores.iter().map(|v| v.validating_pubkey).collect(), - Validators::Some(request_pubkeys) => { + Validators::Count(count) => { + let mut viable_pubkeys: Vec<_> = src_keystores + .iter() + .filter(|v| !v.readonly.unwrap_or(true)) + .map(|v| v.validating_pubkey) + .collect(); + viable_pubkeys.sort_unstable_by_key(|pubkey| pubkey.serialize()); + viable_pubkeys + .get(0..count) + .ok_or_else(|| { + format!( + "Cannot move {} keystores since source validator client only has {} \ + keystores which are able to be moved (not read-only).", + count, + viable_pubkeys.len() + ) + })? + .to_vec() + } + Validators::Specific(request_pubkeys) => { let request_pubkeys_set: HashSet<_> = request_pubkeys.iter().collect(); let src_pubkeys_set: HashSet<_> = src_keystores.iter().map(|v| &v.validating_pubkey).collect(); @@ -623,62 +648,88 @@ mod test { dest_vc_url: dest_vc.url.clone(), dest_vc_token_path: dest_vc_token_path.clone(), validators: validators.clone(), - builder_proposals: false, + builder_proposals: None, fee_recipient: None, gas_limit: None, }; let result = run(move_config).await; - let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; - let dest_vc_final_keystores = dest_vc_client.get_keystores().await.unwrap().data; + if result.is_ok() { + let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; + let dest_vc_final_keystores = dest_vc_client.get_keystores().await.unwrap().data; - match validators { - Validators::All => { - assert!( - src_vc_final_keystores.is_empty(), - "all keystores should be removed from source vc" - ); - assert_eq!( - dest_vc_final_keystores.len(), - dest_vc_initial_keystores.len() + src_vc_initial_keystores.len() - - self.duplicates, - "the correct count of keystores should have been moved to the dest" - ); - for initial_keystore in &src_vc_initial_keystores { + match validators { + Validators::All => { assert!( - dest_vc_final_keystores.contains(initial_keystore), - "the source keystore should be present at the dest" - ) + src_vc_final_keystores.is_empty(), + "all keystores should be removed from source vc" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + src_vc_initial_keystores.len() + - self.duplicates, + "the correct count of keystores should have been moved to the dest" + ); + for initial_keystore in &src_vc_initial_keystores { + assert!( + dest_vc_final_keystores.contains(initial_keystore), + "the source keystore should be present at the dest" + ) + } } - } - Validators::Some(pubkeys) => { - assert_eq!( - src_vc_final_keystores.len(), - src_vc_initial_keystores - .len() - .checked_sub(pubkeys.len()) - .unwrap(), - "the correct count of validators should have been removed from the src" - ); - assert_eq!( - dest_vc_final_keystores.len(), - dest_vc_initial_keystores.len() + pubkeys.len() - self.duplicates, - "the correct count of keystores should have been moved to the dest" - ); - for pubkey in pubkeys { - let initial_keystore = src_vc_initial_keystores - .iter() - .find(|k| k.validating_pubkey == pubkey) - .unwrap(); - assert!( - !src_vc_final_keystores.contains(initial_keystore), - "the keystore should not be present at the source" + Validators::Count(count) => { + assert_eq!( + src_vc_final_keystores.len(), + src_vc_initial_keystores.len() - count, + "keystores should be removed from source vc" ); - assert!( - dest_vc_final_keystores.contains(initial_keystore), - "the keystore should be present at the dest" + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + count - self.duplicates, + "the correct count of keystores should have been moved to the dest" ); + let moved_keystores: Vec<_> = { + let initial_set: HashSet<_> = src_vc_initial_keystores.iter().collect(); + let final_set: HashSet<_> = src_vc_final_keystores.iter().collect(); + initial_set.difference(&final_set).cloned().collect() + }; + assert_eq!(moved_keystores.len(), count); + for moved_keystore in &moved_keystores { + assert!( + dest_vc_final_keystores.contains(moved_keystore), + "the moved keystore should be present at the dest" + ) + } + } + Validators::Specific(pubkeys) => { + assert_eq!( + src_vc_final_keystores.len(), + src_vc_initial_keystores + .len() + .checked_sub(pubkeys.len()) + .unwrap(), + "the correct count of validators should have been removed from the src" + ); + assert_eq!( + dest_vc_final_keystores.len(), + dest_vc_initial_keystores.len() + pubkeys.len() - self.duplicates, + "the correct count of keystores should have been moved to the dest" + ); + for pubkey in pubkeys { + let initial_keystore = src_vc_initial_keystores + .iter() + .find(|k| k.validating_pubkey == pubkey) + .unwrap(); + assert!( + !src_vc_final_keystores.contains(initial_keystore), + "the keystore should not be present at the source" + ); + assert!( + dest_vc_final_keystores.contains(initial_keystore), + "the keystore should be present at the dest" + ); + } } } } @@ -697,6 +748,10 @@ mod test { assert_eq!(self.result, Ok(())) } + fn assert_err(self) { + assert!(self.result.is_err()) + } + fn assert_err_is(self, msg: String) { assert_eq!(self.result, Err(msg)) } @@ -728,7 +783,7 @@ mod test { .await .with_src_validators(1, 0) .await - .run_test(|pubkeys| Validators::Some(pubkeys.to_vec())) + .run_test(|pubkeys| Validators::Specific(pubkeys.to_vec())) .await .assert_ok(); } @@ -769,7 +824,7 @@ mod test { .with_dest_validators(2, 0) .await .register_duplicates(1) - .run_test(|pubkeys| Validators::Some(pubkeys[0..1].to_vec())) + .run_test(|pubkeys| Validators::Specific(pubkeys[0..1].to_vec())) .await .assert_ok(); } @@ -791,7 +846,7 @@ mod test { .await .with_src_validators(3, 0) .await - .run_test(|pubkeys| Validators::Some(pubkeys[0..1].to_vec())) + .run_test(|pubkeys| Validators::Specific(pubkeys[0..1].to_vec())) .await .assert_ok(); } @@ -802,7 +857,7 @@ mod test { .await .with_src_validators(3, 0) .await - .run_test(|pubkeys| Validators::Some(pubkeys[0..2].to_vec())) + .run_test(|pubkeys| Validators::Specific(pubkeys[0..2].to_vec())) .await .assert_ok(); } @@ -813,8 +868,41 @@ mod test { .await .with_src_validators(3, 42) .await - .run_test(|pubkeys| Validators::Some(pubkeys.to_vec())) + .run_test(|pubkeys| Validators::Specific(pubkeys.to_vec())) .await .assert_ok(); } + + #[tokio::test] + async fn three_validators_move_one_by_count() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|_| Validators::Count(1)) + .await + .assert_ok(); + } + + #[tokio::test] + async fn three_validators_move_two_by_count() { + TestBuilder::new() + .await + .with_src_validators(3, 0) + .await + .run_test(|_| Validators::Count(2)) + .await + .assert_ok(); + } + + #[tokio::test] + async fn one_validators_move_two_by_count() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .run_test(|_| Validators::Count(2)) + .await + .assert_err(); + } } From 2abde0b666fa5e0efa12ec3fa07c2d13cda5b0d8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 15:30:02 +1000 Subject: [PATCH 063/138] Allow VC to create password files via HTTP API --- account_manager/src/validator/import.rs | 8 +- .../src/validator_definitions.rs | 20 +++- common/validator_dir/src/builder.rs | 17 ++- common/validator_dir/src/lib.rs | 4 +- common/validator_dir/src/validator_dir.rs | 6 +- lighthouse/tests/validator_client.rs | 13 +++ validator_client/src/cli.rs | 10 ++ validator_client/src/config.rs | 4 + .../src/http_api/create_validator.rs | 22 +++- validator_client/src/http_api/keystores.rs | 20 +++- validator_client/src/http_api/mod.rs | 101 ++++++++++++++---- validator_client/src/http_api/test_utils.rs | 2 + validator_client/src/lib.rs | 1 + validator_client/src/validator_store.rs | 6 +- .../src/validators/move_validators.rs | 2 +- 15 files changed, 189 insertions(+), 47 deletions(-) diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index c581866a258..b9cc85e8d85 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -4,8 +4,8 @@ use account_utils::{ eth2_keystore::Keystore, read_password_from_user, validator_definitions::{ - recursively_find_voting_keystores, ValidatorDefinition, ValidatorDefinitions, - CONFIG_FILENAME, + recursively_find_voting_keystores, PasswordStorage, ValidatorDefinition, + ValidatorDefinitions, CONFIG_FILENAME, }, ZeroizeString, }; @@ -277,7 +277,9 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let suggested_fee_recipient = None; let validator_def = ValidatorDefinition::new_keystore_with_password( &dest_keystore, - password_opt, + password_opt + .map(PasswordStorage::ValidatorDefinitions) + .unwrap_or(PasswordStorage::None), graffiti, suggested_fee_recipient, None, diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 579195299ce..c0addb0568f 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -49,6 +49,16 @@ pub enum Error { KeystoreWithoutPassword, } +/// Defines how a password for a validator keystore will be persisted. +pub enum PasswordStorage { + /// Store the password in the `validator_definitions.yml` file. + ValidatorDefinitions(ZeroizeString), + /// Store the password in a separate, dedicated file (likely in the "secrets" directory). + File(PathBuf), + /// Don't store the password at all. + None, +} + #[derive(Clone, PartialEq, Serialize, Deserialize, Hash, Eq)] pub struct Web3SignerDefinition { pub url: String, @@ -151,7 +161,7 @@ impl ValidatorDefinition { /// This function does not check the password against the keystore. pub fn new_keystore_with_password>( voting_keystore_path: P, - voting_keystore_password: Option, + voting_keystore_password_storage: PasswordStorage, graffiti: Option, suggested_fee_recipient: Option
, gas_limit: Option, @@ -161,6 +171,12 @@ impl ValidatorDefinition { let keystore = Keystore::from_json_file(&voting_keystore_path).map_err(Error::UnableToOpenKeystore)?; let voting_public_key = keystore.public_key().ok_or(Error::InvalidKeystorePubkey)?; + let (voting_keystore_password_path, voting_keystore_password) = + match voting_keystore_password_storage { + PasswordStorage::ValidatorDefinitions(password) => (None, Some(password)), + PasswordStorage::File(path) => (Some(path), None), + PasswordStorage::None => (None, None), + }; Ok(ValidatorDefinition { enabled: true, @@ -172,7 +188,7 @@ impl ValidatorDefinition { builder_proposals, signing_definition: SigningDefinition::LocalKeystore { voting_keystore_path, - voting_keystore_password_path: None, + voting_keystore_password_path, voting_keystore_password, }, }) diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 596c918b3f4..a7732a7798e 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -78,6 +78,13 @@ impl<'a> Builder<'a> { self } + /// Optionally supply a directory in which to store the passwords for the validator keystores. + /// If `None` is provided, do not store the password. + pub fn password_dir_opt(mut self, password_dir_opt: Option) -> Self { + self.password_dir = password_dir_opt; + self + } + /// Build the `ValidatorDir` use the given `keystore` which can be unlocked with `password`. /// /// The builder will not necessarily check that `password` can unlock `keystore`. @@ -234,7 +241,7 @@ impl<'a> Builder<'a> { if self.store_withdrawal_keystore { // Write the withdrawal password to file. write_password_to_file( - password_dir.join(withdrawal_keypair.pk.as_hex_string()), + keystore_password_path(&password_dir, &withdrawal_keystore), withdrawal_password.as_bytes(), )?; @@ -250,7 +257,7 @@ impl<'a> Builder<'a> { if let Some(password_dir) = self.password_dir.as_ref() { // Write the voting password to file. write_password_to_file( - password_dir.join(format!("0x{}", voting_keystore.pubkey())), + keystore_password_path(&password_dir, &voting_keystore), voting_password.as_bytes(), )?; } @@ -262,6 +269,12 @@ impl<'a> Builder<'a> { } } +pub fn keystore_password_path>(password_dir: P, keystore: &Keystore) -> PathBuf { + password_dir + .as_ref() + .join(format!("0x{}", keystore.pubkey())) +} + /// Writes a JSON keystore to file. fn write_keystore_to_file(path: PathBuf, keystore: &Keystore) -> Result<(), Error> { if path.exists() { diff --git a/common/validator_dir/src/lib.rs b/common/validator_dir/src/lib.rs index a39d322834b..4aa0d590a16 100644 --- a/common/validator_dir/src/lib.rs +++ b/common/validator_dir/src/lib.rs @@ -15,6 +15,6 @@ pub use crate::validator_dir::{ ETH1_DEPOSIT_TX_HASH_FILE, }; pub use builder::{ - Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, - WITHDRAWAL_KEYSTORE_FILE, + keystore_password_path, Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, + VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, }; diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index cb1ddde24a4..24b317dcfe3 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -1,5 +1,5 @@ use crate::builder::{ - ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, + keystore_password_path, ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, }; use deposit_contract::decode_eth1_tx_data; @@ -219,9 +219,7 @@ pub fn unlock_keypair>( ) .map_err(Error::UnableToReadKeystore)?; - let password_path = password_dir - .as_ref() - .join(format!("0x{}", keystore.pubkey())); + let password_path = keystore_password_path(password_dir, &keystore); let password: PlainText = read(&password_path) .map_err(|_| Error::UnableToReadPassword(password_path))? .into(); diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index d56f3a28752..4b1e34bce78 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -324,6 +324,19 @@ fn http_allow_keystore_export_present() { .run() .with_config(|config| assert!(config.http_api.allow_keystore_export)); } +#[test] +fn http_store_keystore_passwords_in_secrets_dir_default() { + CommandLineTest::new() + .run() + .with_config(|config| assert!(!config.http_api.store_passwords_in_secrets_dir)); +} +#[test] +fn http_store_keystore_passwords_in_secrets_dir_present() { + CommandLineTest::new() + .flag("http-store-passwords-in-secrets-dir", None) + .run() + .with_config(|config| assert!(config.http_api.store_passwords_in_secrets_dir)); +} // Tests for Metrics flags. #[test] diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 916dc113b76..e0b9d0246c7 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -200,6 +200,16 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .required(false) .takes_value(false), ) + .arg( + Arg::with_name("http-store-passwords-in-secrets-dir") + .long("http-store-passwords-in-secrets-dir") + .value_name("ORIGIN") + .help("If present, any validators created via the HTTP will have keystore \ + passwords stored in the secrets-dir rather than the validator \ + definitions file.") + .required(false) + .takes_value(false), + ) /* Prometheus metrics HTTP server related arguments */ .arg( Arg::with_name("metrics") diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index fd10b2de1dd..fe9bfb8d9ab 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -259,6 +259,10 @@ impl Config { config.http_api.allow_keystore_export = true; } + if cli_args.is_present("http-store-passwords-in-secrets-dir") { + config.http_api.store_passwords_in_secrets_dir = true; + } + /* * Prometheus metrics HTTP server */ diff --git a/validator_client/src/http_api/create_validator.rs b/validator_client/src/http_api/create_validator.rs index a32ccce6279..61f18c04ef1 100644 --- a/validator_client/src/http_api/create_validator.rs +++ b/validator_client/src/http_api/create_validator.rs @@ -1,15 +1,15 @@ use crate::ValidatorStore; -use account_utils::validator_definitions::ValidatorDefinition; +use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use account_utils::{ eth2_wallet::{bip39::Mnemonic, WalletBuilder}, random_mnemonic, random_password, ZeroizeString, }; use eth2::lighthouse_vc::types::{self as api_types}; use slot_clock::SlotClock; -use std::path::Path; +use std::path::{Path, PathBuf}; use types::ChainSpec; use types::EthSpec; -use validator_dir::Builder as ValidatorDirBuilder; +use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; /// Create some validator EIP-2335 keystores and store them on disk. Then, enroll the validators in /// this validator client. @@ -27,6 +27,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, key_derivation_path_offset: Option, validator_requests: &[api_types::ValidatorRequest], validator_dir: P, + secrets_dir: Option, validator_store: &ValidatorStore, spec: &ChainSpec, ) -> Result<(Vec, Mnemonic), warp::Rejection> { @@ -95,7 +96,20 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, )) })?; + let voting_password_storage = if let Some(secrets_dir) = &secrets_dir { + let password_path = keystore_password_path(secrets_dir, &keystores.voting); + if password_path.exists() { + return Err(warp_utils::reject::custom_server_error( + "Duplicate keystore password path".to_string(), + )); + } + PasswordStorage::File(password_path) + } else { + PasswordStorage::ValidatorDefinitions(voting_password_string.clone()) + }; + let validator_dir = ValidatorDirBuilder::new(validator_dir.as_ref().into()) + .password_dir_opt(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) .create_eth1_tx_data(request.deposit_gwei, spec) @@ -136,7 +150,7 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, validator_store .add_validator_keystore( voting_keystore_path, - voting_password_string, + voting_password_storage, request.enable, request.graffiti.clone(), request.suggested_fee_recipient, diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/src/http_api/keystores.rs index 982b5f0171c..51e85837531 100644 --- a/validator_client/src/http_api/keystores.rs +++ b/validator_client/src/http_api/keystores.rs @@ -3,7 +3,7 @@ use crate::{ initialized_validators::Error, signing_method::SigningMethod, InitializedValidators, ValidatorStore, }; -use account_utils::ZeroizeString; +use account_utils::{validator_definitions::PasswordStorage, ZeroizeString}; use eth2::lighthouse_vc::{ std_types::{ DeleteKeystoreStatus, DeleteKeystoresRequest, DeleteKeystoresResponse, @@ -20,7 +20,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio::runtime::Handle; use types::{EthSpec, PublicKeyBytes}; -use validator_dir::Builder as ValidatorDirBuilder; +use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use warp::Rejection; use warp_utils::reject::{custom_bad_request, custom_server_error}; @@ -61,6 +61,7 @@ pub fn list( pub fn import( request: ImportKeystoresRequest, validator_dir: PathBuf, + secrets_dir: Option, validator_store: Arc>, task_executor: TaskExecutor, log: Logger, @@ -131,6 +132,7 @@ pub fn import( keystore, password, validator_dir.clone(), + secrets_dir.clone(), &validator_store, handle, ) { @@ -161,6 +163,7 @@ fn import_single_keystore( keystore: Keystore, password: ZeroizeString, validator_dir_path: PathBuf, + secrets_dir: Option, validator_store: &ValidatorStore, handle: Handle, ) -> Result { @@ -182,6 +185,16 @@ fn import_single_keystore( } } + let password_storage = if let Some(secrets_dir) = &secrets_dir { + let password_path = keystore_password_path(secrets_dir, &keystore); + if password_path.exists() { + return Ok(ImportKeystoreStatus::Duplicate); + } + PasswordStorage::File(password_path) + } else { + PasswordStorage::ValidatorDefinitions(password.clone()) + }; + // Check that the password is correct. // In future we should re-structure to avoid the double decryption here. It's not as simple // as removing this check because `add_validator_keystore` will break if provided with an @@ -192,6 +205,7 @@ fn import_single_keystore( .map_err(|e| format!("incorrect password: {:?}", e))?; let validator_dir = ValidatorDirBuilder::new(validator_dir_path) + .password_dir_opt(secrets_dir) .voting_keystore(keystore, password.as_ref()) .store_withdrawal_keystore(false) .build() @@ -204,7 +218,7 @@ fn import_single_keystore( handle .block_on(validator_store.add_validator_keystore( voting_keystore_path, - password, + password_storage, true, None, None, diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index a25f9ea271c..657f3bc008e 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -9,7 +9,9 @@ pub mod test_utils; use crate::ValidatorStore; use account_utils::{ mnemonic_from_phrase, - validator_definitions::{SigningDefinition, ValidatorDefinition, Web3SignerDefinition}, + validator_definitions::{ + PasswordStorage, SigningDefinition, ValidatorDefinition, Web3SignerDefinition, + }, }; pub use api_secret::ApiSecret; use create_validator::{create_validators_mnemonic, create_validators_web3signer}; @@ -28,7 +30,7 @@ use std::path::PathBuf; use std::sync::Arc; use task_executor::TaskExecutor; use types::{ChainSpec, ConfigAndPreset, EthSpec}; -use validator_dir::Builder as ValidatorDirBuilder; +use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; use warp::{ http::{ header::{HeaderValue, CONTENT_TYPE}, @@ -64,6 +66,7 @@ pub struct Context { pub api_secret: ApiSecret, pub validator_store: Option>>, pub validator_dir: Option, + pub secrets_dir: Option, pub spec: ChainSpec, pub config: Config, pub log: Logger, @@ -78,6 +81,7 @@ pub struct Config { pub listen_port: u16, pub allow_origin: Option, pub allow_keystore_export: bool, + pub store_passwords_in_secrets_dir: bool, } impl Default for Config { @@ -88,6 +92,7 @@ impl Default for Config { listen_port: 5062, allow_origin: None, allow_keystore_export: false, + store_passwords_in_secrets_dir: false, } } } @@ -113,6 +118,7 @@ pub fn serve( ) -> Result<(SocketAddr, impl Future), Error> { let config = &ctx.config; let allow_keystore_export = config.allow_keystore_export; + let store_passwords_in_secrets_dir = config.store_passwords_in_secrets_dir; let log = ctx.log.clone(); // Configure CORS. @@ -179,6 +185,17 @@ pub fn serve( }) }); + let inner_secrets_dir = ctx.secrets_dir.clone(); + let secrets_dir_filter = warp::any().map(move || inner_secrets_dir.clone()).and_then( + |secrets_dir: Option<_>| async move { + secrets_dir.ok_or_else(|| { + warp_utils::reject::custom_not_found( + "secrets_dir directory is not initialized.".to_string(), + ) + }) + }, + ); + let inner_ctx = ctx.clone(); let log_filter = warp::any().map(move || inner_ctx.log.clone()); @@ -290,18 +307,21 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) + .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(spec_filter.clone()) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( - |body: Vec, - validator_dir: PathBuf, - validator_store: Arc>, - spec: Arc, - signer, - task_executor: TaskExecutor| { + move |body: Vec, + validator_dir: PathBuf, + secrets_dir: PathBuf, + validator_store: Arc>, + spec: Arc, + signer, + task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let (validators, mnemonic) = handle.block_on(create_validators_mnemonic( @@ -309,6 +329,7 @@ pub fn serve( None, &body, &validator_dir, + secrets_dir, &validator_store, &spec, ))?; @@ -333,18 +354,21 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) + .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(spec_filter) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( - |body: api_types::CreateValidatorsMnemonicRequest, - validator_dir: PathBuf, - validator_store: Arc>, - spec: Arc, - signer, - task_executor: TaskExecutor| { + move |body: api_types::CreateValidatorsMnemonicRequest, + validator_dir: PathBuf, + secrets_dir: PathBuf, + validator_store: Arc>, + spec: Arc, + signer, + task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); if let Some(handle) = task_executor.handle() { let mnemonic = mnemonic_from_phrase(body.mnemonic.as_str()).map_err(|e| { @@ -359,6 +383,7 @@ pub fn serve( Some(body.key_derivation_path_offset), &body.validators, &validator_dir, + secrets_dir, &validator_store, &spec, ))?; @@ -379,15 +404,17 @@ pub fn serve( .and(warp::path::end()) .and(warp::body::json()) .and(validator_dir_filter.clone()) + .and(secrets_dir_filter.clone()) .and(validator_store_filter.clone()) .and(signer.clone()) .and(task_executor_filter.clone()) .and_then( - |body: api_types::KeystoreValidatorsPostRequest, - validator_dir: PathBuf, - validator_store: Arc>, - signer, - task_executor: TaskExecutor| { + move |body: api_types::KeystoreValidatorsPostRequest, + validator_dir: PathBuf, + secrets_dir: PathBuf, + validator_store: Arc>, + signer, + task_executor: TaskExecutor| { blocking_signed_json_task(signer, move || { // Check to ensure the password is correct. let keypair = body @@ -400,7 +427,21 @@ pub fn serve( )) })?; + let secrets_dir = store_passwords_in_secrets_dir.then(|| secrets_dir); + let password_storage = if let Some(secrets_dir) = &secrets_dir { + let password_path = keystore_password_path(secrets_dir, &body.keystore); + if password_path.exists() { + return Err(warp_utils::reject::custom_server_error( + "Duplicate keystore password path".to_string(), + )); + } + PasswordStorage::File(password_path) + } else { + PasswordStorage::ValidatorDefinitions(body.password.clone()) + }; + let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) + .password_dir_opt(secrets_dir) .voting_keystore(body.keystore.clone(), body.password.as_ref()) .store_withdrawal_keystore(false) .build() @@ -414,7 +455,6 @@ pub fn serve( // Drop validator dir so that `add_validator_keystore` can re-lock the keystore. let voting_keystore_path = validator_dir.voting_keystore_path(); drop(validator_dir); - let voting_password = body.password.clone(); let graffiti = body.graffiti.clone(); let suggested_fee_recipient = body.suggested_fee_recipient; let gas_limit = body.gas_limit; @@ -425,7 +465,7 @@ pub fn serve( handle .block_on(validator_store.add_validator_keystore( voting_keystore_path, - voting_password, + password_storage, body.enable, graffiti, suggested_fee_recipient, @@ -850,13 +890,28 @@ pub fn serve( .and(warp::body::json()) .and(signer.clone()) .and(validator_dir_filter) + .and(secrets_dir_filter) .and(validator_store_filter.clone()) .and(task_executor_filter.clone()) .and(log_filter.clone()) .and_then( - |request, signer, validator_dir, validator_store, task_executor, log| { + move |request, + signer, + validator_dir, + secrets_dir, + validator_store, + task_executor, + log| { + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); blocking_signed_json_task(signer, move || { - keystores::import(request, validator_dir, validator_store, task_executor, log) + keystores::import( + request, + validator_dir, + secrets_dir, + validator_store, + task_executor, + log, + ) }) }, ); diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 862c83a21f0..6b84b4e32fa 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -121,6 +121,7 @@ impl ApiTester { task_executor: test_runtime.task_executor.clone(), api_secret, validator_dir: Some(validator_dir.path().into()), + secrets_dir: Some(secrets_dir.path().into()), validator_store: Some(validator_store.clone()), spec: E::default_spec(), config: HttpConfig { @@ -129,6 +130,7 @@ impl ApiTester { listen_port: 0, allow_origin: None, allow_keystore_export: true, + store_passwords_in_secrets_dir: false, }, log, _phantom: PhantomData, diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 9db4cc03155..b6956bc6a6d 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -520,6 +520,7 @@ impl ProductionValidatorClient { api_secret, validator_store: Some(self.validator_store.clone()), validator_dir: Some(self.config.validator_dir.clone()), + secrets_dir: Some(self.config.secrets_dir.clone()), spec: self.context.eth2_config.spec.clone(), config: self.config.http_api.clone(), log: log.clone(), diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 292b49ac3a5..0a9b35dc672 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -5,7 +5,7 @@ use crate::{ signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod}, Config, }; -use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; +use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use parking_lot::{Mutex, RwLock}; use slashing_protection::{ interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, @@ -161,7 +161,7 @@ impl ValidatorStore { pub async fn add_validator_keystore>( &self, voting_keystore_path: P, - password: ZeroizeString, + password_storage: PasswordStorage, enable: bool, graffiti: Option, suggested_fee_recipient: Option
, @@ -170,7 +170,7 @@ impl ValidatorStore { ) -> Result { let mut validator_def = ValidatorDefinition::new_keystore_with_password( voting_keystore_path, - Some(password), + password_storage, graffiti.map(Into::into), suggested_fee_recipient, gas_limit, diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 1d5b89c7c7f..d1041f3d878 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -150,7 +150,7 @@ impl FromStr for Validators { .map(Validators::Specific), other => usize::from_str(other) .map_err(|_| { - format!("Expected \"all\", a list of 0x-prefixed pubkeys or an integer") + "Expected \"all\", a list of 0x-prefixed pubkeys or an integer".to_string() }) .map(Validators::Count), } From 47eaa50c0603de6ec5c453675d52ffe455a49ee8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 18:28:33 +1000 Subject: [PATCH 064/138] Shorten "validator-client" to "vc" --- lighthouse/tests/validator_manager.rs | 38 ++++++++-------- .../src/validators/import_validators.rs | 18 ++++---- .../src/validators/move_validators.rs | 44 ++++++++----------- 3 files changed, 47 insertions(+), 53 deletions(-) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 6a0abebe7e9..73ea58db29d 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -186,7 +186,7 @@ pub fn validator_create_disable_deposits() { pub fn validator_import_defaults() { CommandLineTest::validators_import() .flag("--validators-file", Some("./vals.json")) - .flag("--validator-client-token", Some("./token.json")) + .flag("--vc-token", Some("./token.json")) .assert_success(|config| { let expected = ImportConfig { validators_file_path: PathBuf::from("./vals.json"), @@ -202,7 +202,7 @@ pub fn validator_import_defaults() { pub fn validator_import_misc_flags() { CommandLineTest::validators_import() .flag("--validators-file", Some("./vals.json")) - .flag("--validator-client-token", Some("./token.json")) + .flag("--vc-token", Some("./token.json")) .flag("--ignore-duplicates", None) .assert_success(|config| { let expected = ImportConfig { @@ -225,17 +225,17 @@ pub fn validator_import_missing_token() { #[test] pub fn validator_import_missing_validators_file() { CommandLineTest::validators_import() - .flag("--validator-client-token", Some("./token.json")) + .flag("--vc-token", Some("./token.json")) .assert_failed(); } #[test] pub fn validator_move_defaults() { CommandLineTest::validators_move() - .flag("--src-validator-client-url", Some("http://localhost:1")) - .flag("--src-validator-client-token", Some("./1.json")) - .flag("--dest-validator-client-url", Some("http://localhost:2")) - .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) .flag("--validators", Some("all")) .assert_success(|config| { let expected = MoveConfig { @@ -255,10 +255,10 @@ pub fn validator_move_defaults() { #[test] pub fn validator_move_misc_flags_0() { CommandLineTest::validators_move() - .flag("--src-validator-client-url", Some("http://localhost:1")) - .flag("--src-validator-client-token", Some("./1.json")) - .flag("--dest-validator-client-url", Some("http://localhost:2")) - .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) .flag( "--validators", Some(&format!("{},{}", EXAMPLE_PUBKEY_0, EXAMPLE_PUBKEY_1)), @@ -287,10 +287,10 @@ pub fn validator_move_misc_flags_0() { #[test] pub fn validator_move_misc_flags_1() { CommandLineTest::validators_move() - .flag("--src-validator-client-url", Some("http://localhost:1")) - .flag("--src-validator-client-token", Some("./1.json")) - .flag("--dest-validator-client-url", Some("http://localhost:2")) - .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) .flag("--validators", Some(&format!("{}", EXAMPLE_PUBKEY_0))) .flag("--builder-proposals", Some("false")) .assert_success(|config| { @@ -313,10 +313,10 @@ pub fn validator_move_misc_flags_1() { #[test] pub fn validator_move_count() { CommandLineTest::validators_move() - .flag("--src-validator-client-url", Some("http://localhost:1")) - .flag("--src-validator-client-token", Some("./1.json")) - .flag("--dest-validator-client-url", Some("http://localhost:2")) - .flag("--dest-validator-client-token", Some("./2.json")) + .flag("--src-vc-url", Some("http://localhost:1")) + .flag("--src-vc-token", Some("./1.json")) + .flag("--dest-vc-url", Some("http://localhost:2")) + .flag("--dest-vc-token", Some("./2.json")) .flag("--validators", Some("42")) .assert_success(|config| { let expected = MoveConfig { diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/validators/import_validators.rs index 74228b1e1e0..8861309dd78 100644 --- a/validator_manager/src/validators/import_validators.rs +++ b/validator_manager/src/validators/import_validators.rs @@ -8,8 +8,8 @@ use std::path::PathBuf; pub const CMD: &str = "import"; pub const VALIDATORS_FILE_FLAG: &str = "validators-file"; -pub const VALIDATOR_CLIENT_URL_FLAG: &str = "validator-client-url"; -pub const VALIDATOR_CLIENT_TOKEN_FLAG: &str = "validator-client-token"; +pub const VC_URL_FLAG: &str = "vc-url"; +pub const VC_TOKEN_FLAG: &str = "vc-token"; pub const DETECTED_DUPLICATE_MESSAGE: &str = "Duplicate validator detected!"; @@ -33,8 +33,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true), ) .arg( - Arg::with_name(VALIDATOR_CLIENT_URL_FLAG) - .long(VALIDATOR_CLIENT_URL_FLAG) + Arg::with_name(VC_URL_FLAG) + .long(VC_URL_FLAG) .value_name("HTTP_ADDRESS") .help( "A HTTP(S) address of a validator client using the keymanager-API. \ @@ -42,12 +42,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { no changes are made to the validator client.", ) .default_value("http://localhost:5062") - .requires(VALIDATOR_CLIENT_TOKEN_FLAG) + .requires(VC_TOKEN_FLAG) .takes_value(true), ) .arg( - Arg::with_name(VALIDATOR_CLIENT_TOKEN_FLAG) - .long(VALIDATOR_CLIENT_TOKEN_FLAG) + Arg::with_name(VC_TOKEN_FLAG) + .long(VC_TOKEN_FLAG) .value_name("PATH") .help("The file containing a token required by the validator client.") .takes_value(true), @@ -79,8 +79,8 @@ impl ImportConfig { fn from_cli(matches: &ArgMatches) -> Result { Ok(Self { validators_file_path: clap_utils::parse_required(matches, VALIDATORS_FILE_FLAG)?, - vc_url: clap_utils::parse_required(matches, VALIDATOR_CLIENT_URL_FLAG)?, - vc_token_path: clap_utils::parse_required(matches, VALIDATOR_CLIENT_TOKEN_FLAG)?, + vc_url: clap_utils::parse_required(matches, VC_URL_FLAG)?, + vc_token_path: clap_utils::parse_required(matches, VC_TOKEN_FLAG)?, ignore_duplicates: matches.is_present(IGNORE_DUPLICATES_FLAG), }) } diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index d1041f3d878..86ae9289247 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -23,10 +23,10 @@ pub const MOVE_DIR_NAME: &str = "lighthouse-validator-move"; pub const VALIDATOR_SPECIFICATION_FILE: &str = "validator-specification.json"; pub const CMD: &str = "move"; -pub const SRC_VALIDATOR_CLIENT_URL_FLAG: &str = "src-validator-client-url"; -pub const SRC_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "src-validator-client-token"; -pub const DEST_VALIDATOR_CLIENT_URL_FLAG: &str = "dest-validator-client-url"; -pub const DEST_VALIDATOR_CLIENT_TOKEN_FLAG: &str = "dest-validator-client-token"; +pub const SRC_VC_URL_FLAG: &str = "src-vc-url"; +pub const SRC_VC_TOKEN_FLAG: &str = "src-vc-token"; +pub const DEST_VC_URL_FLAG: &str = "dest-vc-url"; +pub const DEST_VC_TOKEN_FLAG: &str = "dest-vc-token"; pub const VALIDATORS_FLAG: &str = "validators"; pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; @@ -44,8 +44,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { command.", ) .arg( - Arg::with_name(SRC_VALIDATOR_CLIENT_URL_FLAG) - .long(SRC_VALIDATOR_CLIENT_URL_FLAG) + Arg::with_name(SRC_VC_URL_FLAG) + .long(SRC_VC_URL_FLAG) .value_name("HTTP_ADDRESS") .help( "A HTTP(S) address of a validator client using the keymanager-API. \ @@ -53,19 +53,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { that are to be moved.", ) .required(true) - .requires(SRC_VALIDATOR_CLIENT_TOKEN_FLAG) + .requires(SRC_VC_TOKEN_FLAG) .takes_value(true), ) .arg( - Arg::with_name(SRC_VALIDATOR_CLIENT_TOKEN_FLAG) - .long(SRC_VALIDATOR_CLIENT_TOKEN_FLAG) + Arg::with_name(SRC_VC_TOKEN_FLAG) + .long(SRC_VC_TOKEN_FLAG) .value_name("PATH") .help("The file containing a token required by the source validator client.") .takes_value(true), ) .arg( - Arg::with_name(DEST_VALIDATOR_CLIENT_URL_FLAG) - .long(DEST_VALIDATOR_CLIENT_URL_FLAG) + Arg::with_name(DEST_VC_URL_FLAG) + .long(DEST_VC_URL_FLAG) .value_name("HTTP_ADDRESS") .help( "A HTTP(S) address of a validator client using the keymanager-API. \ @@ -73,12 +73,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { added as they are removed from the \"source\" validator client.", ) .required(true) - .requires(DEST_VALIDATOR_CLIENT_TOKEN_FLAG) + .requires(DEST_VC_TOKEN_FLAG) .takes_value(true), ) .arg( - Arg::with_name(DEST_VALIDATOR_CLIENT_TOKEN_FLAG) - .long(DEST_VALIDATOR_CLIENT_TOKEN_FLAG) + Arg::with_name(DEST_VC_TOKEN_FLAG) + .long(DEST_VC_TOKEN_FLAG) .value_name("PATH") .help("The file containing a token required by the destination validator client.") .takes_value(true), @@ -172,16 +172,10 @@ pub struct MoveConfig { impl MoveConfig { fn from_cli(matches: &ArgMatches) -> Result { Ok(Self { - src_vc_url: clap_utils::parse_required(matches, SRC_VALIDATOR_CLIENT_URL_FLAG)?, - src_vc_token_path: clap_utils::parse_required( - matches, - SRC_VALIDATOR_CLIENT_TOKEN_FLAG, - )?, - dest_vc_url: clap_utils::parse_required(matches, DEST_VALIDATOR_CLIENT_URL_FLAG)?, - dest_vc_token_path: clap_utils::parse_required( - matches, - DEST_VALIDATOR_CLIENT_TOKEN_FLAG, - )?, + src_vc_url: clap_utils::parse_required(matches, SRC_VC_URL_FLAG)?, + src_vc_token_path: clap_utils::parse_required(matches, SRC_VC_TOKEN_FLAG)?, + dest_vc_url: clap_utils::parse_required(matches, DEST_VC_URL_FLAG)?, + dest_vc_token_path: clap_utils::parse_required(matches, DEST_VC_TOKEN_FLAG)?, validators: clap_utils::parse_required(matches, VALIDATORS_FLAG)?, builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, @@ -219,7 +213,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { if src_vc_url == dest_vc_url { return Err(format!( "--{} and --{} must be different", - SRC_VALIDATOR_CLIENT_URL_FLAG, DEST_VALIDATOR_CLIENT_URL_FLAG + SRC_VC_URL_FLAG, DEST_VC_URL_FLAG )); } From d1aff84f837666f845c62a4ef3bef77767ff3f57 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 18:28:54 +1000 Subject: [PATCH 065/138] Handle a missing command --- validator_manager/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 0571cd7f4ce..c5f1a731c75 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -64,6 +64,7 @@ pub fn run<'a, T: EthSpec>( (validators::CMD, Some(matches)) => { validators::cli_run::(matches, &spec, dump_config).await } + ("", _) => Err("No command supplied. See --help.".to_string()), (unknown, _) => Err(format!( "{} is not a valid {} command. See --help.", unknown, CMD From fdd0311507430c217176f0492ba1364fe384e716 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 18:34:46 +1000 Subject: [PATCH 066/138] Strip newlines from VC token --- validator_manager/src/validators/common.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index d3f3d3f231d..c57bbc4f65a 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -1,4 +1,4 @@ -use account_utils::ZeroizeString; +use account_utils::{strip_off_newlines, ZeroizeString}; use eth2::lighthouse_vc::std_types::{InterchangeJsonStr, KeystoreJsonStr}; use eth2::{ lighthouse_vc::{ @@ -309,7 +309,7 @@ pub async fn vc_http_client>( let token_path = token_path.as_ref(); let token_bytes = fs::read(&token_path).map_err(|e| format!("Failed to read {:?}: {:?}", token_path, e))?; - let token_string = String::from_utf8(token_bytes) + let token_string = String::from_utf8(strip_off_newlines(token_bytes)) .map_err(|e| format!("Failed to parse {:?} as utf8: {:?}", token_path, e))?; let http_client = ValidatorClientHttpClient::new(url.clone(), token_string).map_err(|e| { format!( From b2887f8b555b02e580425d6e6f0392c90002d89b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 19:06:55 +1000 Subject: [PATCH 067/138] Ensure password dir exists in val dir builder --- Cargo.lock | 1 + common/validator_dir/Cargo.toml | 1 + common/validator_dir/src/builder.rs | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 27cc1d89dbe..7be6912ea44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7345,6 +7345,7 @@ dependencies = [ "bls", "deposit_contract", "derivative", + "directory", "eth2_keystore", "filesystem", "hex", diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 0eba4cf2327..091140568ab 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -20,6 +20,7 @@ tree_hash = "0.4.1" hex = "0.4.2" derivative = "2.1.1" lockfile = { path = "../lockfile" } +directory = { path = "../directory" } [dev-dependencies] tempfile = "3.1.0" diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index a7732a7798e..4ab9764f841 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -1,6 +1,7 @@ use crate::{Error as DirError, ValidatorDir}; use bls::get_withdrawal_credentials; use deposit_contract::{encode_eth1_tx_data, Error as DepositError}; +use directory::ensure_dir_exists; use eth2_keystore::{Error as KeystoreError, Keystore, KeystoreBuilder, PlainText}; use filesystem::create_with_600_perms; use rand::{distributions::Alphanumeric, Rng}; @@ -41,6 +42,7 @@ pub enum Error { #[cfg(feature = "insecure_keys")] InsecureKeysError(String), MissingPasswordDir, + UnableToCreatePasswordDir(String), } impl From for Error { @@ -160,6 +162,10 @@ impl<'a> Builder<'a> { create_dir_all(&dir).map_err(Error::UnableToCreateDir)?; } + if let Some(password_dir) = &self.password_dir { + ensure_dir_exists(password_dir).map_err(Error::UnableToCreatePasswordDir)?; + } + // The withdrawal keystore must be initialized in order to store it or create an eth1 // deposit. if (self.store_withdrawal_keystore || self.deposit_info.is_some()) From 4ccb7cbf4da171743913b813a5181d0f3b5b1f1e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 19:07:13 +1000 Subject: [PATCH 068/138] Exit early on keystore upload error --- validator_manager/src/validators/common.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index c57bbc4f65a..517dca87554 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -110,6 +110,11 @@ impl ValidatorSpecification { return Err(UploadError::IncorrectStatusCount(statuses.len() + 1)); } + // Exit early if there's an error uploading. + if status.status == ImportKeystoreStatus::Error { + return Ok(status); + } + if let Some(fee_recipient) = fee_recipient { http_client .post_fee_recipient( From 07196804e026085ce9dc0e913ecabca8a4f8030b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 19:23:39 +1000 Subject: [PATCH 069/138] Remove naughty return Err --- validator_manager/src/validators/move_validators.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index 86ae9289247..e71b3cf7730 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -454,7 +454,6 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { keystore_derivation_path.as_deref(), ) .await; - return Err(format!("Upload failed with {:?}", status.message)); } } } From da2a8badc73aaef2023f8404dd9a7c46090a0882 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 29 Aug 2022 22:11:05 +1000 Subject: [PATCH 070/138] Start adding docs --- book/src/validator-manager-move.md | 95 ++++++++++++++++++++++++++++++ book/src/validator-manager.md | 40 +++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 book/src/validator-manager-move.md create mode 100644 book/src/validator-manager.md diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md new file mode 100644 index 00000000000..41a7dc2ab18 --- /dev/null +++ b/book/src/validator-manager-move.md @@ -0,0 +1,95 @@ +# Moving Validators + +This document describes the steps to move validators between two validator clients (VCs) which are +able to SSH between each other. This guides assumes some experience with the Linux command line. + +There will be two VCs in this example: + +- The *source* VC which contains the validators/keystores to be moved. +- The *destination* VC which is to take the validators/keystores from the source. + +This example will assume the source VC is accessible at `src-host` and the destination VC is +accessible at `dest-host`. Replace these values with your own. + +### 1. Configure the Source VC + +The source VC needs to have the following flags: + +- `--http` +- `--unencrypted-http-transport` +- `--http-address 127.0.0.1` +- `--http-port 5062` +- `--http-allow-keystore-export` + +Therefore, the source VC command might look like: + +```bash +lighthouse \ + vc \ + --http \ + --unencrypted-http-transport \ + --http-address 127.0.0.1 \ + --http-port 5062 \ + --http-allow-keystore-export +``` + +### 2. Configure the Destination VC + +The destination VC needs to have the same flags as the source VC. + +Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have +the import validator keystore passwords stored in separate files rather than in the +`valdiator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. + +### 3. Configure SSH + +For this example to work, the `dest-host` host must be able to SSH to the `src-host` host. This +configuration is out-of-scope of this article, however it probably involves adding a public key to +the `.ssh/authorized_keys` file on the `dest-host` host. + +You will know this is complete when you can SSH to the `dest-host` from your PC and then run `ssh +src-host` successfully. + +### 4. Obtain the Source API Token + +The VC API is protected by an *API token*. This is stored in a file on each of the hosts. Since +we'll be running our command on the destination host, it will need to have the API token for the +source host on its file-system. + +On the **source host**, find the location of the `api-token.txt` file and copy the contents. The +location of the file varies, but it is located in the "validator directory" of your data directory, +alongside validator keystores. For example: `~/.lighthouse/mainnet/validators/api-token.txt`. + +Copy the contents of that file into a new file on the **destination host** at `~/src-token.txt`. The +API token should be similar to `api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123`. + + +### 4. Create an SSH Tunnel + +In one terminal window, SSH to the **destination host** and establish a reverse-SSH connection +between the **desination host** and the **source host**. + +```bash +ssh dest-host +ssh -L 6062:localhost:5062 src-host +``` + +It's important that you leave this session open throughout the rest of this tutorial. If you close +this terminal window then the connection between the destination and source host will be lost. + +---- + +### N. Setup a `screen` Session + +We will use the GNU `screen` application to run multiple, persistent sessions on the destination +host. If you don't like screen feel free to use an equivalent (e.g., `tmux`). + +On the **destination host** run: + +``` +screen +# press "enter" when prompted +``` + +Create a new screen by pressing `Ctrl+A` then `c`. Switch back to the previous screen (`0`) by +pressing `Ctrl+A` and then `"` (use the up/down arrows and enter to select a screen). diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md new file mode 100644 index 00000000000..0b12beffde8 --- /dev/null +++ b/book/src/validator-manager.md @@ -0,0 +1,40 @@ +# Validator Manager + +[Ethereum Launchpad]: https://launchpad.ethereum.org/en/ +[Import Validators]: #import-validators + +## Introduction + +The `lighthouse validator-manager` tool provides utilities for managing validators on a running +Lighthouse Validator Client. The validator manager performs operations via the HTTP API of the +validator client (VC). Due to limitations of the +[keymanager-APIs](https://ethereum.github.io/keymanager-APIs/), only Lighthouse VCs are fully +supported by this command. + +The validator manager tool is similar to the `lighthouse account-manager` tool, except the latter +creates files that will be read by the VC next time it starts rather than making instant changes to +a live VC. The validator manager is generally superior to the account manager for the following +(non-exhaustive) reasons: + +- The validator manager generates deposit files compatible with the [Ethereum Launchpad](). +- Changes made with the validator manager do not require downtime for the VC. +- The "key cache" is preserved whenever a validator is added with the validator manager, this + prevents long waits at start up when a new validator is added. + +## Commands + +### Create Validators + +The `lighthouse validator-manager validators create` command accepts a mnemonic and produces a JSON +file containing validator keystores that can be imported with the [Import +Validators]() command. + +For users that want to add validators to a VC from an existing mnemonic, this is the first half the +process which generates a *validator specifications* JSON file for the new validators. The second +half of the process is to upload those validator specifications to a VC (see [Import Validators](). +The command is split into two steps so that security-conscious validators can generate the validator +specifications on an "air-gapped" computer which is not connected to the Internet. Then, the +validator specifications file can be transferred to a VC for import. This means that the VC never +has access to the mnemonic, just the keystores with which it is concerned. + +### Import Validators From 9539ca2165eba7f57e6e923384f2c726d896cf0a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 30 Aug 2022 06:02:18 +1000 Subject: [PATCH 071/138] Add "done" message --- validator_manager/src/validators/move_validators.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/validators/move_validators.rs index e71b3cf7730..cb9519d9591 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/validators/move_validators.rs @@ -526,6 +526,8 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } } + eprintln!("Done."); + Ok(()) } From 4d790027b724c6931d2d546fe482517531e3fc59 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 30 Aug 2022 09:16:45 +1000 Subject: [PATCH 072/138] Add dodgy progress to docs --- book/src/validator-manager-move.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 41a7dc2ab18..4f44abcb421 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -77,6 +77,20 @@ ssh -L 6062:localhost:5062 src-host It's important that you leave this session open throughout the rest of this tutorial. If you close this terminal window then the connection between the destination and source host will be lost. +### 5. Move + +``` +lighthouse \ + validator-manager \ + validators \ + move \ + --src-vc-url http://localhost:6062 \ + --src-vc-token ~/src-token.txt \ + --dest-vc-url http://localhost:5062 \ + --dest-vc-token ~/dest-token.txt \ + --validators all \ +``` + ---- ### N. Setup a `screen` Session From 6af351a07563902ca19134da0f5daa4878ba857e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 30 Aug 2022 11:35:28 +1000 Subject: [PATCH 073/138] Update docs --- book/src/validator-manager-move.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 4f44abcb421..5103b2a0d73 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -1,7 +1,8 @@ # Moving Validators This document describes the steps to move validators between two validator clients (VCs) which are -able to SSH between each other. This guides assumes some experience with the Linux command line. +able to SSH between each other. This guides assumes experience with the Linux command line and SSH +connections. There will be two VCs in this example: @@ -79,6 +80,9 @@ this terminal window then the connection between the destination and source host ### 5. Move +With the SSH tunnel established between the `dest-host` and `src-host`, from the **destination +host** run the command to move the validators: + ``` lighthouse \ validator-manager \ @@ -87,10 +91,12 @@ lighthouse \ --src-vc-url http://localhost:6062 \ --src-vc-token ~/src-token.txt \ --dest-vc-url http://localhost:5062 \ - --dest-vc-token ~/dest-token.txt \ + --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ --validators all \ ``` + + ---- ### N. Setup a `screen` Session From 9dfea42513cd0d396843c135a404541d29e0b44b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 30 Aug 2022 15:31:48 +1000 Subject: [PATCH 074/138] Add more to docs --- book/src/validator-manager-move.md | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 5103b2a0d73..cbfb7118df4 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -36,7 +36,16 @@ lighthouse \ ### 2. Configure the Destination VC -The destination VC needs to have the same flags as the source VC. +The destination VC needs to have the following flags: + +```bash +lighthouse \ + vc \ + --http \ + --unencrypted-http-transport \ + --http-address 127.0.0.1 \ + --http-port 5062 \ +``` Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have the import validator keystore passwords stored in separate files rather than in the @@ -95,21 +104,4 @@ lighthouse \ --validators all \ ``` - - ----- - -### N. Setup a `screen` Session - -We will use the GNU `screen` application to run multiple, persistent sessions on the destination -host. If you don't like screen feel free to use an equivalent (e.g., `tmux`). - -On the **destination host** run: - -``` -screen -# press "enter" when prompted -``` - -Create a new screen by pressing `Ctrl+A` then `c`. Switch back to the previous screen (`0`) by -pressing `Ctrl+A` and then `"` (use the up/down arrows and enter to select a screen). +TODO From 2abb4a7703d4b80f93835859aec6827ce7aba7c7 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 5 Sep 2022 11:35:58 +1000 Subject: [PATCH 075/138] Add password migrate command --- .../src/validator_definitions.rs | 39 ++++++++++++++++++- common/validator_dir/src/lib.rs | 4 +- validator_client/src/lib.rs | 5 +++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index c0addb0568f..d3237078191 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -16,7 +16,7 @@ use std::fs::{self, File}; use std::io; use std::path::{Path, PathBuf}; use types::{graffiti::GraffitiString, Address, PublicKey}; -use validator_dir::VOTING_KEYSTORE_FILE; +use validator_dir::{write_password_to_file, VOTING_KEYSTORE_FILE}; /// The file name for the serialized `ValidatorDefinitions` struct. pub const CONFIG_FILENAME: &str = "validator_definitions.yml"; @@ -47,6 +47,7 @@ pub enum Error { UnableToCreateValidatorDir(PathBuf), UnableToReadKeystorePassword(String), KeystoreWithoutPassword, + UnableToCreatePassword(validator_dir::BuilderError), } /// Defines how a password for a validator keystore will be persisted. @@ -350,6 +351,42 @@ impl ValidatorDefinitions { Ok(new_defs_count) } + // TODO(paul): remove this + pub fn migrate_passwords_to_secrets_dir>( + &mut self, + validators_dir: P, + secrets_dir: P, + ) -> Result<(), Error> { + for def in &mut self.0 { + match &mut def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + voting_keystore_password_path, + voting_keystore_password, + } => { + if voting_keystore_password_path.is_some() { + continue; + } + + let keystore = Keystore::from_json_file(&voting_keystore_path) + .map_err(Error::UnableToOpenKeystore)?; + + if let Some(password) = voting_keystore_password { + let password_path = default_keystore_password_path(&keystore, &secrets_dir); + if !password_path.exists() { + write_password_to_file(&password_path, password.as_ref()) + .map_err(Error::UnableToCreatePassword)?; + *voting_keystore_password_path = Some(password_path); + *voting_keystore_password = None; + } + } + } + SigningDefinition::Web3Signer { .. } => (), + } + } + self.save(validators_dir) + } + /// Encodes `self` as a YAML string and atomically writes it to the `CONFIG_FILENAME` file in /// the `validators_dir` directory. /// diff --git a/common/validator_dir/src/lib.rs b/common/validator_dir/src/lib.rs index 4aa0d590a16..df0fc81b9bb 100644 --- a/common/validator_dir/src/lib.rs +++ b/common/validator_dir/src/lib.rs @@ -15,6 +15,6 @@ pub use crate::validator_dir::{ ETH1_DEPOSIT_TX_HASH_FILE, }; pub use builder::{ - keystore_password_path, Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, - VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, + keystore_password_path, write_password_to_file, Builder, Error as BuilderError, + ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, }; diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index b6956bc6a6d..f6dca6e14fb 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -162,6 +162,11 @@ impl ProductionValidatorClient { let mut validator_defs = ValidatorDefinitions::open_or_create(&config.validator_dir) .map_err(|e| format!("Unable to open or create validator definitions: {:?}", e))?; + // TODO(paul): remove this + validator_defs + .migrate_passwords_to_secrets_dir(&config.validator_dir, &config.secrets_dir) + .map_err(|e| format!("Unable to migrate passwords: {:?}", e))?; + if !config.disable_auto_discover { let new_validators = validator_defs .discover_local_keystores(&config.validator_dir, &config.secrets_dir, &log) From c60b9c6ace085712ffa6677cb8fe214479671632 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 5 Sep 2022 11:36:08 +1000 Subject: [PATCH 076/138] Revert "Add password migrate command" This reverts commit 36477a004f2c8afac4c0b8e8fa8c4473dc74175b. --- .../src/validator_definitions.rs | 39 +------------------ common/validator_dir/src/lib.rs | 4 +- validator_client/src/lib.rs | 5 --- 3 files changed, 3 insertions(+), 45 deletions(-) diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index d3237078191..c0addb0568f 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -16,7 +16,7 @@ use std::fs::{self, File}; use std::io; use std::path::{Path, PathBuf}; use types::{graffiti::GraffitiString, Address, PublicKey}; -use validator_dir::{write_password_to_file, VOTING_KEYSTORE_FILE}; +use validator_dir::VOTING_KEYSTORE_FILE; /// The file name for the serialized `ValidatorDefinitions` struct. pub const CONFIG_FILENAME: &str = "validator_definitions.yml"; @@ -47,7 +47,6 @@ pub enum Error { UnableToCreateValidatorDir(PathBuf), UnableToReadKeystorePassword(String), KeystoreWithoutPassword, - UnableToCreatePassword(validator_dir::BuilderError), } /// Defines how a password for a validator keystore will be persisted. @@ -351,42 +350,6 @@ impl ValidatorDefinitions { Ok(new_defs_count) } - // TODO(paul): remove this - pub fn migrate_passwords_to_secrets_dir>( - &mut self, - validators_dir: P, - secrets_dir: P, - ) -> Result<(), Error> { - for def in &mut self.0 { - match &mut def.signing_definition { - SigningDefinition::LocalKeystore { - voting_keystore_path, - voting_keystore_password_path, - voting_keystore_password, - } => { - if voting_keystore_password_path.is_some() { - continue; - } - - let keystore = Keystore::from_json_file(&voting_keystore_path) - .map_err(Error::UnableToOpenKeystore)?; - - if let Some(password) = voting_keystore_password { - let password_path = default_keystore_password_path(&keystore, &secrets_dir); - if !password_path.exists() { - write_password_to_file(&password_path, password.as_ref()) - .map_err(Error::UnableToCreatePassword)?; - *voting_keystore_password_path = Some(password_path); - *voting_keystore_password = None; - } - } - } - SigningDefinition::Web3Signer { .. } => (), - } - } - self.save(validators_dir) - } - /// Encodes `self` as a YAML string and atomically writes it to the `CONFIG_FILENAME` file in /// the `validators_dir` directory. /// diff --git a/common/validator_dir/src/lib.rs b/common/validator_dir/src/lib.rs index df0fc81b9bb..4aa0d590a16 100644 --- a/common/validator_dir/src/lib.rs +++ b/common/validator_dir/src/lib.rs @@ -15,6 +15,6 @@ pub use crate::validator_dir::{ ETH1_DEPOSIT_TX_HASH_FILE, }; pub use builder::{ - keystore_password_path, write_password_to_file, Builder, Error as BuilderError, - ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, + keystore_password_path, Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, + VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE, }; diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index f6dca6e14fb..b6956bc6a6d 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -162,11 +162,6 @@ impl ProductionValidatorClient { let mut validator_defs = ValidatorDefinitions::open_or_create(&config.validator_dir) .map_err(|e| format!("Unable to open or create validator definitions: {:?}", e))?; - // TODO(paul): remove this - validator_defs - .migrate_passwords_to_secrets_dir(&config.validator_dir, &config.secrets_dir) - .map_err(|e| format!("Unable to migrate passwords: {:?}", e))?; - if !config.disable_auto_discover { let new_validators = validator_defs .discover_local_keystores(&config.validator_dir, &config.secrets_dir, &log) From 6569a5c93e4576fab5cbb08a2b7299cf35497282 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 5 Sep 2022 16:39:25 +1000 Subject: [PATCH 077/138] Remove comments from deposit `amount` --- validator_manager/src/validators/common.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index 517dca87554..9d587899fec 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -165,7 +165,6 @@ pub struct StandardDepositDataJson { pub pubkey: PublicKeyBytes, #[serde(with = "hash256_without_0x_prefix")] pub withdrawal_credentials: Hash256, - #[serde(with = "eth2_serde_utils::quoted_u64")] pub amount: u64, #[serde(with = "signature_bytes_without_0x_prefix")] pub signature: SignatureBytes, From dbe2fa4c1ba4a301be0b2bf6525a1565bd30ae3b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 5 Sep 2022 16:49:52 +1000 Subject: [PATCH 078/138] Add comment about `amount` field --- validator_manager/src/validators/common.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index 9d587899fec..3f375a10f10 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -165,6 +165,8 @@ pub struct StandardDepositDataJson { pub pubkey: PublicKeyBytes, #[serde(with = "hash256_without_0x_prefix")] pub withdrawal_credentials: Hash256, + /// The `amount` field is *not* quoted (i.e., a string) like most other `u64` fields in the + /// consensus specs, it's a simple integer. pub amount: u64, #[serde(with = "signature_bytes_without_0x_prefix")] pub signature: SignatureBytes, From c63fece35cf26ebe8551c04830f5bc0473860abf Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 14 Nov 2022 17:44:39 +1100 Subject: [PATCH 079/138] Appease clippy --- common/validator_dir/src/builder.rs | 4 ++-- validator_client/src/http_api/mod.rs | 2 +- validator_manager/src/validators/common.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/validator_dir/src/builder.rs b/common/validator_dir/src/builder.rs index 4ab9764f841..22888e9d4d1 100644 --- a/common/validator_dir/src/builder.rs +++ b/common/validator_dir/src/builder.rs @@ -247,7 +247,7 @@ impl<'a> Builder<'a> { if self.store_withdrawal_keystore { // Write the withdrawal password to file. write_password_to_file( - keystore_password_path(&password_dir, &withdrawal_keystore), + keystore_password_path(password_dir, &withdrawal_keystore), withdrawal_password.as_bytes(), )?; @@ -263,7 +263,7 @@ impl<'a> Builder<'a> { if let Some(password_dir) = self.password_dir.as_ref() { // Write the voting password to file. write_password_to_file( - keystore_password_path(&password_dir, &voting_keystore), + keystore_password_path(password_dir, &voting_keystore), voting_password.as_bytes(), )?; } diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index 657f3bc008e..aba1b658a06 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -427,7 +427,7 @@ pub fn serve( )) })?; - let secrets_dir = store_passwords_in_secrets_dir.then(|| secrets_dir); + let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); let password_storage = if let Some(secrets_dir) = &secrets_dir { let password_path = keystore_password_path(secrets_dir, &body.keystore); if password_path.exists() { diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/validators/common.rs index 3f375a10f10..6addb46a3b8 100644 --- a/validator_manager/src/validators/common.rs +++ b/validator_manager/src/validators/common.rs @@ -284,7 +284,7 @@ mod bytes_4_without_0x_prefix { where S: serde::Serializer, { - let hex_string = &hex::encode(&bytes); + let hex_string = &hex::encode(bytes); serializer.serialize_str(hex_string) } @@ -314,7 +314,7 @@ pub async fn vc_http_client>( ) -> Result<(ValidatorClientHttpClient, Vec), String> { let token_path = token_path.as_ref(); let token_bytes = - fs::read(&token_path).map_err(|e| format!("Failed to read {:?}: {:?}", token_path, e))?; + fs::read(token_path).map_err(|e| format!("Failed to read {:?}: {:?}", token_path, e))?; let token_string = String::from_utf8(strip_off_newlines(token_bytes)) .map_err(|e| format!("Failed to parse {:?} as utf8: {:?}", token_path, e))?; let http_client = ValidatorClientHttpClient::new(url.clone(), token_string).map_err(|e| { From 53e492eef91791525ad7d75311328b42e7062911 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 09:32:15 +1100 Subject: [PATCH 080/138] Fix Cargo.lock --- Cargo.lock | 908 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 557 insertions(+), 351 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1041c91d1d9..3466c936a32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,16 +113,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] @@ -132,6 +132,15 @@ name = "amcl" version = "0.3.0" source = "git+https://github.com/sigp/milagro_bls?tag=v1.4.2#16655aa033175a90c10ef02aa144e2835de23aec" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -143,24 +152,24 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "arbitrary" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7924531f38b1970ff630f03eb20a2fde69db5c590c93b0f3482e95dcc5fd60" +checksum = "29d47fbf90d5149a107494b15a7dc8d69b351be2db3bb9691740e88ec17fd880" dependencies = [ "derive_arbitrary", ] [[package]] name = "arc-swap" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" +checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" [[package]] name = "arrayref" @@ -203,9 +212,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -287,9 +296,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" +checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43" dependencies = [ "async-trait", "axum-core", @@ -299,7 +308,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.2", + "itoa 1.0.4", "matchit", "memchr", "mime", @@ -318,9 +327,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b" +checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc" dependencies = [ "async-trait", "bytes", @@ -355,15 +364,15 @@ checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64ct" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" [[package]] name = "beacon-api-client" @@ -537,7 +546,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -552,9 +561,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -664,15 +673,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byte-slice-cast" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" [[package]] name = "byteorder" @@ -682,9 +691,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" dependencies = [ "serde", ] @@ -733,9 +742,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cexpr" @@ -779,14 +788,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", "time 0.1.44", + "wasm-bindgen", "winapi", ] @@ -801,9 +812,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -873,7 +884,7 @@ dependencies = [ "slot_clock", "store", "task_executor", - "time 0.3.11", + "time 0.3.16", "timer", "tokio", "types", @@ -881,13 +892,23 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.48" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" dependencies = [ "cc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "compare_fields" version = "0.2.0" @@ -1027,26 +1048,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" dependencies = [ "autocfg 1.1.0", "cfg-if", "crossbeam-utils", "memoffset", - "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -1057,12 +1076,12 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2b443d17d49dad5ef0ede301c3179cc923b8822f3393b4d2c28c269dd4a122" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1130,11 +1149,11 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.2" +version = "3.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" +checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" dependencies = [ - "nix 0.24.2", + "nix 0.25.0", "winapi", ] @@ -1159,11 +1178,55 @@ checksum = "4033478fbf70d6acf2655ac70da91ee65852d69daf7a67bf7a2f518fb47aafcf" dependencies = [ "byteorder", "digest 0.9.0", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", "zeroize", ] +[[package]] +name = "cxx" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "darling" version = "0.13.4" @@ -1251,9 +1314,9 @@ checksum = "b72465f46d518f6015d9cf07f7f3013a95dd6b9c2747c3d65ae0cce43929d14f" [[package]] name = "delay_map" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6716ce9729be9628979ae1ff63e8bc8b7ad53b5472a2633bf079607a55328d36" +checksum = "9c4d75d3abfe4830dcbf9bcb1b926954e121669f74dd1ca7aa0183b1755d83f6" dependencies = [ "futures", "tokio-util 0.6.10", @@ -1268,7 +1331,7 @@ dependencies = [ "hex", "reqwest", "serde_json", - "sha2 0.10.2", + "sha2 0.10.6", "tree_hash", "types", ] @@ -1296,9 +1359,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a577516173adb681466d517d39bd468293bc2c2a16439375ef0f35bba45f3d" +checksum = "4903dff04948f22033ca30232ab8eca2c3fc4c913a8b6a34ee5199699814817f" dependencies = [ "proc-macro2", "quote", @@ -1329,11 +1392,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle", ] @@ -1424,9 +1487,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5caaa75cbd2b960ff1e5392d2cfb1f44717fffe12fc1f32b7b5d1267f99732a6" +checksum = "f8a6eee2d5d0d113f015688310da018bd1d864d86bd567c8fca9c266889e1bfa" [[package]] name = "ecdsa" @@ -1496,9 +1559,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "elliptic-curve" @@ -1509,12 +1572,12 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.3", + "digest 0.10.5", "ff", "generic-array", "group", "pkcs8", - "rand_core 0.6.3", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1545,7 +1608,7 @@ dependencies = [ "rand 0.8.5", "rlp", "serde", - "sha3 0.10.1", + "sha3 0.10.6", "zeroize", ] @@ -1573,9 +1636,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" dependencies = [ "atty", "humantime", @@ -1704,7 +1767,7 @@ dependencies = [ "lazy_static", "ring", "rustc-hex", - "sha2 0.10.2", + "sha2 0.10.6", "wasm-bindgen-test", ] @@ -1731,7 +1794,7 @@ dependencies = [ "hex", "num-bigint-dig", "ring", - "sha2 0.10.2", + "sha2 0.10.6", "zeroize", ] @@ -1860,9 +1923,9 @@ dependencies = [ [[package]] name = "ethabi" -version = "17.1.0" +version = "17.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f186de076b3e77b8e6d73c99d1b52edc2a229e604f4b5eb6992c06c11d79d537" +checksum = "e4966fba78396ff92db3b817ee71143eccd98acf0f876b8d600e585a670c5d1b" dependencies = [ "ethereum-types 0.13.1", "hex", @@ -1870,7 +1933,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha3 0.10.1", + "sha3 0.10.6", "thiserror", "uint", ] @@ -1961,7 +2024,7 @@ dependencies = [ "bytes", "chrono", "elliptic-curve", - "ethabi 17.1.0", + "ethabi 17.2.0", "fastrlp", "generic-array", "hex", @@ -1992,7 +2055,7 @@ dependencies = [ "futures-core", "futures-timer", "futures-util", - "getrandom 0.2.7", + "getrandom 0.2.8", "hashers", "hex", "http", @@ -2128,9 +2191,9 @@ dependencies = [ [[package]] name = "fastrlp-derive" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fa41ebc231af281098b11ad4a4f6182ec9096902afffe948034a20d4e1385a" +checksum = "d9e9158c1d8f0a7a716c9191562eaabba70268ba64972ef4871ce8d66fd08872" dependencies = [ "bytes", "proc-macro2", @@ -2140,11 +2203,11 @@ dependencies = [ [[package]] name = "ff" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", ] @@ -2239,11 +2302,10 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -2271,9 +2333,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -2286,9 +2348,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -2296,15 +2358,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -2314,15 +2376,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", @@ -2331,26 +2393,26 @@ dependencies = [ [[package]] name = "futures-rustls" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01fe9932a224b72b45336d96040aa86386d674a31d0af27d800ea7bc8ca97fe" +checksum = "d2411eed028cdf8c8034eaf21f9915f956b6c3abec4d4c7949ee67f0721127bd" dependencies = [ "futures-io", - "rustls 0.20.6", + "rustls 0.20.7", "webpki 0.22.0", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-timer" @@ -2360,9 +2422,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -2387,9 +2449,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -2431,9 +2493,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "js-sys", @@ -2488,20 +2550,20 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "group" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -2512,7 +2574,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", "tracing", ] @@ -2560,9 +2622,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64", "bitflags", @@ -2571,7 +2633,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.10.0", + "sha1", ] [[package]] @@ -2645,7 +2707,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -2678,7 +2740,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa 1.0.4", ] [[package]] @@ -2764,9 +2826,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -2782,9 +2844,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064" dependencies = [ "bytes", "futures-channel", @@ -2795,7 +2857,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa 1.0.4", "pin-project-lite 0.2.9", "socket2", "tokio", @@ -2812,7 +2874,7 @@ checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ "http", "hyper", - "rustls 0.20.6", + "rustls 0.20.7", "tokio", "tokio-rustls 0.23.4", ] @@ -2830,6 +2892,30 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2847,6 +2933,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "if-addrs" version = "0.6.7" @@ -2906,7 +3002,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" dependencies = [ - "parity-scale-codec 3.1.5", + "parity-scale-codec 3.2.1", ] [[package]] @@ -2998,9 +3094,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -3013,15 +3109,15 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -3064,8 +3160,8 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.2", - "sha3 0.10.1", + "sha2 0.10.6", + "sha3 0.10.6", ] [[package]] @@ -3100,7 +3196,7 @@ dependencies = [ "clap_utils", "deposit_contract", "directory", - "env_logger 0.9.0", + "env_logger 0.9.1", "environment", "eth1_test_rig", "eth2", @@ -3150,9 +3246,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libflate" @@ -3186,9 +3282,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "libmdbx" @@ -3214,7 +3310,7 @@ dependencies = [ "bytes", "futures", "futures-timer", - "getrandom 0.2.7", + "getrandom 0.2.8", "instant", "lazy_static", "libp2p-core", @@ -3264,7 +3360,7 @@ dependencies = [ "prost-build", "rand 0.8.5", "rw-stream-sink", - "sha2 0.10.2", + "sha2 0.10.6", "smallvec", "thiserror", "unsigned-varint 0.7.1", @@ -3308,7 +3404,7 @@ dependencies = [ "prost-build", "rand 0.7.3", "regex", - "sha2 0.10.2", + "sha2 0.10.6", "smallvec", "unsigned-varint 0.7.1", "wasm-timer", @@ -3381,7 +3477,7 @@ dependencies = [ "prost", "prost-build", "rand 0.8.5", - "sha2 0.10.2", + "sha2 0.10.6", "snow", "static_assertions", "x25519-dalek", @@ -3427,9 +3523,9 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f02622b9dd150011b4eeec387f8bd013189a2f27da08ba363e7c6e606d77a48" +checksum = "a0eddc4497a8b5a506013c40e8189864f9c3a00db2b25671f428ae9007f3ba32" dependencies = [ "heck", "quote", @@ -3568,7 +3664,7 @@ dependencies = [ "clap_utils", "database_manager", "directory", - "env_logger 0.9.0", + "env_logger 0.9.1", "environment", "eth1", "eth2", @@ -3634,7 +3730,7 @@ dependencies = [ "regex", "serde", "serde_derive", - "sha2 0.10.2", + "sha2 0.10.6", "slog", "slog-async", "slog-term", @@ -3663,6 +3759,15 @@ dependencies = [ "target_info", ] +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3692,9 +3797,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -3895,23 +4000,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -3960,14 +4065,14 @@ dependencies = [ [[package]] name = "multihash" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3db354f401db558759dfc1e568d010a5d4146f4d3f637be1275ec4a3cf09689" +checksum = "1c346cf9999c631f002d8f977c4eaeaa0e6386f16007202308d0b3757522c2cc" dependencies = [ "core2", - "digest 0.10.3", + "digest 0.10.5", "multihash-derive", - "sha2 0.10.2", + "sha2 0.10.6", "unsigned-varint 0.7.1", ] @@ -4100,10 +4205,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" dependencies = [ + "autocfg 1.1.0", "bitflags", "cfg-if", "libc", @@ -4155,6 +4261,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -4245,9 +4361,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "oneshot_broadcast" @@ -4270,9 +4386,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.41" +version = "0.10.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" dependencies = [ "bitflags", "cfg-if", @@ -4311,9 +4427,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.75" +version = "0.9.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a" dependencies = [ "autocfg 1.1.0", "cc", @@ -4346,6 +4462,12 @@ dependencies = [ "types", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "p256" version = "0.11.1" @@ -4354,7 +4476,7 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa", "elliptic-curve", - "sha2 0.10.2", + "sha2 0.10.6", ] [[package]] @@ -4373,9 +4495,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.1.5" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9182e4a71cae089267ab03e67c99368db7cd877baf50f931e5d6d4b71e195ac0" +checksum = "366e44391a8af4cfd6002ef6ba072bae071a96aafca98d7d448a34c5dca38b6a" dependencies = [ "arrayvec", "bitvec 1.0.1", @@ -4427,7 +4549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -4446,22 +4568,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] name = "paste" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pbkdf2" @@ -4498,15 +4620,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" +checksum = "dbc7bc69c062e492337d74d59b120c274fd3d261b6bf6d3207d499b4b379c41a" dependencies = [ "thiserror", "ucd-trie", @@ -4534,18 +4656,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -4582,9 +4704,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "platforms" @@ -4594,9 +4716,9 @@ checksum = "e8d0eef3571242013a0d5dc84861c3ae4a652e56e12adf8bdc26ff5f8cb34c94" [[package]] name = "plotters" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716b4eeb6c4a1d3ecc956f75b43ec2e8e8ba80026413e70a3f41fd3313d3492b" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" dependencies = [ "num-traits", "plotters-backend", @@ -4613,9 +4735,9 @@ checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" [[package]] name = "plotters-svg" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0918736323d1baff32ee0eade54984f6f201ad7e97d5cfb5d6ab4a358529615" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" dependencies = [ "plotters-backend", ] @@ -4677,10 +4799,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -4717,9 +4840,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.42" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] @@ -4738,9 +4861,9 @@ dependencies = [ [[package]] name = "prometheus" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cface98dfa6d645ea4c789839f176e4b072265d085bfcc48eaa8d137f58d3c39" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" dependencies = [ "cfg-if", "fnv", @@ -4753,12 +4876,12 @@ dependencies = [ [[package]] name = "prometheus-client" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c473049631c233933d6286c88bbb7be30e62ec534cf99a9ae0079211f7fa603" +checksum = "83cd1b99916654a69008fd66b4f9397fbe08e6e51dfe23d4417acf5d3b8cb87c" dependencies = [ "dtoa", - "itoa 1.0.2", + "itoa 1.0.4", "parking_lot 0.12.1", "prometheus-client-derive-text-encode", ] @@ -4854,9 +4977,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.27.1" +version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psutil" @@ -4919,9 +5042,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -4980,7 +5103,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5000,7 +5123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5014,11 +5137,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", ] [[package]] @@ -5036,7 +5159,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5078,7 +5201,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] @@ -5120,9 +5243,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ "base64", "bytes", @@ -5137,13 +5260,13 @@ dependencies = [ "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "native-tls", + "once_cell", "percent-encoding", "pin-project-lite 0.2.9", - "rustls 0.20.6", + "rustls 0.20.7", "rustls-pemfile", "serde", "serde_json", @@ -5151,7 +5274,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.23.4", - "tokio-util 0.7.3", + "tokio-util 0.7.4", "tower-service", "url", "wasm-bindgen", @@ -5205,9 +5328,9 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "rlp" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ "bytes", "rustc-hex", @@ -5302,7 +5425,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.12", + "semver 1.0.14", ] [[package]] @@ -5320,9 +5443,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -5332,18 +5455,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ "base64", ] [[package]] name = "rustversion" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "rw-stream-sink" @@ -5358,9 +5481,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "safe_arith" @@ -5397,7 +5520,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -5411,9 +5534,9 @@ dependencies = [ [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" @@ -5421,6 +5544,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + [[package]] name = "scrypt" version = "0.7.0" @@ -5487,9 +5616,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -5528,9 +5657,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "semver-parser" @@ -5563,9 +5692,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.140" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] @@ -5592,9 +5721,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.140" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -5603,20 +5732,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.4", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ "proc-macro2", "quote", @@ -5630,7 +5759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa 1.0.4", "ryu", "serde", ] @@ -5690,7 +5819,18 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.5", ] [[package]] @@ -5708,13 +5848,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -5731,11 +5871,11 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881bf8156c87b6301fc5ca6b27f11eeb2761224c7081e69b409d5a1951a70c86" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", "keccak", ] @@ -5765,12 +5905,12 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb766570a2825fa972bceff0d195727876a9cdf2460ab2e52d455dc2de47fd9" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.3", - "rand_core 0.6.3", + "digest 0.10.5", + "rand_core 0.6.4", ] [[package]] @@ -5782,7 +5922,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.11", + "time 0.3.16", ] [[package]] @@ -5790,7 +5930,7 @@ name = "simulator" version = "0.2.0" dependencies = [ "clap", - "env_logger 0.9.0", + "env_logger 0.9.1", "eth1", "eth1_test_rig", "execution_layer", @@ -5908,7 +6048,7 @@ dependencies = [ "serde", "serde_json", "slog", - "time 0.3.11", + "time 0.3.16", ] [[package]] @@ -5953,7 +6093,7 @@ dependencies = [ "slog", "term", "thread_local", - "time 0.3.11", + "time 0.3.16", ] [[package]] @@ -5992,9 +6132,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "snap" @@ -6012,18 +6152,18 @@ dependencies = [ "blake2", "chacha20poly1305", "curve25519-dalek 4.0.0-pre.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "ring", "rustc_version 0.4.0", - "sha2 0.10.2", + "sha2 0.10.6", "subtle", ] [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -6094,7 +6234,7 @@ dependencies = [ "beacon_chain", "bls", "derivative", - "env_logger 0.9.0", + "env_logger 0.9.1", "eth2_hashing", "eth2_ssz", "eth2_ssz_derive", @@ -6179,9 +6319,9 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck", "proc-macro2", @@ -6221,9 +6361,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", @@ -6250,9 +6390,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.26.8" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ddf41e393a9133c81d5f0974195366bd57082deac6e0eb02ed39b8341c2bb6" +checksum = "c375d5fd899e32847b8566e10598d6e9f1d9b55ec6de3cdf9e7da4bdc51371bc" dependencies = [ "cfg-if", "core-foundation-sys", @@ -6375,18 +6515,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -6424,21 +6564,32 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.4", "libc", "num_threads", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +dependencies = [ + "time-core", +] [[package]] name = "timer" @@ -6506,9 +6657,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg 1.1.0", "bytes", @@ -6516,7 +6667,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite 0.2.9", "signal-hook-registry", @@ -6573,21 +6723,21 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls 0.20.6", + "rustls 0.20.7", "tokio", "webpki 0.22.0", ] [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", "pin-project-lite 0.2.9", "tokio", - "tokio-util 0.7.3", + "tokio-util 0.7.4", ] [[package]] @@ -6611,7 +6761,7 @@ checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" dependencies = [ "futures-util", "log", - "rustls 0.20.6", + "rustls 0.20.7", "tokio", "tokio-rustls 0.23.4", "tungstenite 0.17.3", @@ -6637,9 +6787,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -6695,9 +6845,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -6707,9 +6857,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", @@ -6720,9 +6870,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", @@ -6731,9 +6881,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", "valuable", @@ -6762,12 +6912,12 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "ansi_term", "matchers", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", @@ -6834,7 +6984,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna", + "idna 0.2.3", "ipnet", "lazy_static", "log", @@ -6904,7 +7054,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "rustls 0.20.6", + "rustls 0.20.7", "sha-1 0.10.0", "thiserror", "url", @@ -6979,15 +7129,15 @@ dependencies = [ [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" +checksum = "a45526d29728d135c2900b0d30573fe3ee79fceb12ef534c7bb30e810a91b601" dependencies = [ "arbitrary", "byteorder", @@ -7019,30 +7169,30 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" @@ -7086,13 +7236,12 @@ version = "0.1.0" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna", - "matches", + "idna 0.3.0", "percent-encoding", ] @@ -7108,7 +7257,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.8", "serde", ] @@ -7329,9 +7478,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -7339,9 +7488,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -7354,9 +7503,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if", "js-sys", @@ -7366,9 +7515,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7376,9 +7525,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -7389,15 +7538,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wasm-bindgen-test" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513df541345bb9fcc07417775f3d51bbb677daf307d8035c0afafd87dc2e6599" +checksum = "09d2fff962180c3fadf677438054b1db62bee4aa32af26a45388af07d1287e1d" dependencies = [ "console_error_panic_hook", "js-sys", @@ -7409,9 +7558,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6150d36a03e90a3cf6c12650be10626a9902d70c5270fd47d7a47e5389a10d56" +checksum = "4683da3dfc016f704c9f82cf401520c4f1cb3ee440f7f52b3d6ac29506a49ca7" dependencies = [ "proc-macro2", "quote", @@ -7434,9 +7583,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -7458,7 +7607,7 @@ dependencies = [ "futures-timer", "headers", "hex", - "idna", + "idna 0.2.3", "jsonrpc-core", "log", "once_cell", @@ -7536,22 +7685,22 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.4" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" dependencies = [ "webpki 0.22.0", ] [[package]] name = "which" -version = "4.2.5" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] @@ -7615,43 +7764,100 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "winreg" version = "0.7.0" @@ -7740,9 +7946,9 @@ dependencies = [ [[package]] name = "yamux" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0608f53c1dc0bad505d03a34bbd49fbf2ad7b51eb036123e896365532745a1" +checksum = "e5d9ba232399af1783a58d8eb26f6b5006fbefe2dc9ef36bd283324792d03ea5" dependencies = [ "futures", "log", From 06f7efcc58a10c9d247bf8a7904a6eec718de00a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 09:32:22 +1100 Subject: [PATCH 081/138] Fix clippy error --- validator_manager/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index c5f1a731c75..8c6f66662e8 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -43,10 +43,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } /// Run the account manager, returning an error if the operation did not succeed. -pub fn run<'a, T: EthSpec>( - matches: &'a ArgMatches<'a>, - mut env: Environment, -) -> Result<(), String> { +pub fn run<'a, T: EthSpec>(matches: &'a ArgMatches<'a>, env: Environment) -> Result<(), String> { let context = env.core_context(); let spec = context.eth2_config.spec.clone(); let dump_config = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? From 0ece17ed322d01f6ac326ade503fbb2965d84cd6 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 10:35:51 +1100 Subject: [PATCH 082/138] Remove `validators` subcommand --- .../src/{validators => }/common.rs | 0 .../src/{validators => }/create_validators.rs | 0 .../src/{validators => }/import_validators.rs | 0 validator_manager/src/lib.rs | 21 +++++++--- .../src/{validators => }/move_validators.rs | 2 +- validator_manager/src/validators/mod.rs | 40 ------------------- 6 files changed, 17 insertions(+), 46 deletions(-) rename validator_manager/src/{validators => }/common.rs (100%) rename validator_manager/src/{validators => }/create_validators.rs (100%) rename validator_manager/src/{validators => }/import_validators.rs (100%) rename validator_manager/src/{validators => }/move_validators.rs (99%) delete mode 100644 validator_manager/src/validators/mod.rs diff --git a/validator_manager/src/validators/common.rs b/validator_manager/src/common.rs similarity index 100% rename from validator_manager/src/validators/common.rs rename to validator_manager/src/common.rs diff --git a/validator_manager/src/validators/create_validators.rs b/validator_manager/src/create_validators.rs similarity index 100% rename from validator_manager/src/validators/create_validators.rs rename to validator_manager/src/create_validators.rs diff --git a/validator_manager/src/validators/import_validators.rs b/validator_manager/src/import_validators.rs similarity index 100% rename from validator_manager/src/validators/import_validators.rs rename to validator_manager/src/import_validators.rs diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index 8c6f66662e8..e5033035c1f 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -1,12 +1,15 @@ use clap::App; use clap::ArgMatches; +use common::write_to_json_file; use environment::Environment; use serde::Serialize; use std::path::PathBuf; use types::EthSpec; -use validators::common::write_to_json_file; -pub mod validators; +pub mod common; +pub mod create_validators; +pub mod import_validators; +pub mod move_validators; pub const CMD: &str = "validator_manager"; @@ -39,7 +42,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .visible_aliases(&["vm", "validator-manager", CMD]) .about("Utilities for managing a Lighthouse validator client via the HTTP API.") - .subcommand(validators::cli_app()) + .subcommand(create_validators::cli_app()) + .subcommand(import_validators::cli_app()) + .subcommand(move_validators::cli_app()) } /// Run the account manager, returning an error if the operation did not succeed. @@ -58,8 +63,14 @@ pub fn run<'a, T: EthSpec>(matches: &'a ArgMatches<'a>, env: Environment) -> .block_on_dangerous( async { match matches.subcommand() { - (validators::CMD, Some(matches)) => { - validators::cli_run::(matches, &spec, dump_config).await + (create_validators::CMD, Some(matches)) => { + create_validators::cli_run::(matches, &spec, dump_config).await + } + (import_validators::CMD, Some(matches)) => { + import_validators::cli_run(matches, dump_config).await + } + (move_validators::CMD, Some(matches)) => { + move_validators::cli_run(matches, dump_config).await } ("", _) => Err("No command supplied. See --help.".to_string()), (unknown, _) => Err(format!( diff --git a/validator_manager/src/validators/move_validators.rs b/validator_manager/src/move_validators.rs similarity index 99% rename from validator_manager/src/validators/move_validators.rs rename to validator_manager/src/move_validators.rs index cb9519d9591..1bd1466d6ba 100644 --- a/validator_manager/src/validators/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -234,7 +234,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { .filter(|v| !v.readonly.unwrap_or(true)) .map(|v| v.validating_pubkey) .collect(); - viable_pubkeys.sort_unstable_by_key(|pubkey| pubkey.serialize()); + viable_pubkeys.sort_unstable_by_key(PublicKeyBytes::serialize); viable_pubkeys .get(0..count) .ok_or_else(|| { diff --git a/validator_manager/src/validators/mod.rs b/validator_manager/src/validators/mod.rs deleted file mode 100644 index eeb0a029622..00000000000 --- a/validator_manager/src/validators/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -pub mod common; -pub mod create_validators; -pub mod import_validators; -pub mod move_validators; - -use crate::DumpConfig; -use clap::{App, ArgMatches}; -use types::{ChainSpec, EthSpec}; - -pub const CMD: &str = "validators"; - -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_validators::cli_app()) - .subcommand(import_validators::cli_app()) - .subcommand(move_validators::cli_app()) -} - -pub async fn cli_run<'a, T: EthSpec>( - matches: &'a ArgMatches<'a>, - spec: &ChainSpec, - dump_config: DumpConfig, -) -> Result<(), String> { - match matches.subcommand() { - (create_validators::CMD, Some(matches)) => { - create_validators::cli_run::(matches, spec, dump_config).await - } - (import_validators::CMD, Some(matches)) => { - import_validators::cli_run(matches, dump_config).await - } - (move_validators::CMD, Some(matches)) => { - move_validators::cli_run(matches, dump_config).await - } - (unknown, _) => Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )), - } -} From b5c463eaacf6ce9d1250a26fc38ac339c15fba3b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 10:44:00 +1100 Subject: [PATCH 083/138] Fix cli tests --- lighthouse/tests/validator_manager.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 73ea58db29d..0723fa5bfc3 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -7,7 +7,7 @@ use std::process::{Command, Stdio}; use std::str::FromStr; use tempfile::{tempdir, TempDir}; use types::*; -use validator_manager::validators::{ +use validator_manager::{ create_validators::CreateConfig, import_validators::ImportConfig, move_validators::{MoveConfig, Validators}, @@ -84,23 +84,19 @@ impl CommandLineTest { impl CommandLineTest { fn validators_create() -> Self { - Self::default() - .flag("validators", None) - .flag("create", None) + Self::default().flag("create", None) } } impl CommandLineTest { fn validators_import() -> Self { - Self::default() - .flag("validators", None) - .flag("import", None) + Self::default().flag("import", None) } } impl CommandLineTest { fn validators_move() -> Self { - Self::default().flag("validators", None).flag("move", None) + Self::default().flag("move", None) } } From af6affac4671f21071342ca8bea3acd4f7da452f Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 10:50:31 +1100 Subject: [PATCH 084/138] Fix compile errors --- validator_manager/src/import_validators.rs | 2 +- validator_manager/src/move_validators.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 8861309dd78..92af5ed673c 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -237,7 +237,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { #[cfg(test)] pub mod tests { use super::*; - use crate::validators::create_validators::tests::TestBuilder as CreateTestBuilder; + use crate::create_validators::tests::TestBuilder as CreateTestBuilder; use std::fs; use tempfile::{tempdir, TempDir}; use validator_client::http_api::test_utils::ApiTester; diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 1bd1466d6ba..592f6e0dd95 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -548,7 +548,7 @@ async fn sleep_with_retry_message(pubkey: &PublicKeyBytes, path: Option<&str>) { #[cfg(test)] mod test { use super::*; - use crate::validators::import_validators::tests::TestBuilder as ImportTestBuilder; + use crate::import_validators::tests::TestBuilder as ImportTestBuilder; use std::fs; use tempfile::{tempdir, TempDir}; use validator_client::http_api::test_utils::ApiTester; From 0fe8958768845c9b69b3706d63f1e20eea3eba91 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 15:27:53 +1100 Subject: [PATCH 085/138] Add test for moving validators back again --- validator_manager/src/move_validators.rs | 74 +++++++++++++++++++----- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 592f6e0dd95..ab1db3c2cd9 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -561,6 +561,7 @@ mod test { dest_import_builder: Option, duplicates: usize, dir: TempDir, + move_back_again: bool, } impl TestBuilder { @@ -571,9 +572,15 @@ mod test { dest_import_builder: None, duplicates: 0, dir, + move_back_again: false, } } + fn move_back_again(mut self) -> Self { + self.move_back_again = true; + self + } + async fn with_src_validators(mut self, count: u32, first_index: u32) -> Self { let builder = ImportTestBuilder::new() .await @@ -597,18 +604,15 @@ mod test { self } - async fn run_test(self, gen_validators_enum: F) -> TestResult + async fn move_validators( + &self, + gen_validators_enum: F, + src_vc: &ApiTester, + dest_vc: &ApiTester, + ) -> Result<(), String> where F: Fn(&[PublicKeyBytes]) -> Validators, { - let src_vc = if let Some(import_builder) = self.src_import_builder { - let import_test_result = import_builder.run_test().await; - assert!(import_test_result.result.is_ok()); - import_test_result.vc - } else { - ApiTester::new().await - }; - let src_vc_token_path = self.dir.path().join(SRC_VC_TOKEN_FILE_NAME); fs::write(&src_vc_token_path, &src_vc.api_token).unwrap(); let (src_vc_client, src_vc_initial_keystores) = @@ -622,13 +626,6 @@ mod test { .collect(); let validators = gen_validators_enum(&src_vc_initial_pubkeys); - let dest_vc = if let Some(import_builder) = self.dest_import_builder { - let import_test_result = import_builder.run_test().await; - assert!(import_test_result.result.is_ok()); - import_test_result.vc - } else { - ApiTester::new().await - }; let dest_vc_token_path = self.dir.path().join(DEST_VC_TOKEN_FILE_NAME); fs::write(&dest_vc_token_path, &dest_vc.api_token).unwrap(); @@ -729,6 +726,39 @@ mod test { } } + result + } + + async fn run_test(mut self, gen_validators_enum: F) -> TestResult + where + F: Fn(&[PublicKeyBytes]) -> Validators + Copy, + { + let src_vc = if let Some(import_builder) = self.src_import_builder.take() { + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + import_test_result.vc + } else { + ApiTester::new().await + }; + + let dest_vc = if let Some(import_builder) = self.dest_import_builder.take() { + let import_test_result = import_builder.run_test().await; + assert!(import_test_result.result.is_ok()); + import_test_result.vc + } else { + ApiTester::new().await + }; + + let result = self + .move_validators(gen_validators_enum, &src_vc, &dest_vc) + .await; + + if self.move_back_again { + self.move_validators(gen_validators_enum, &dest_vc, &src_vc) + .await + .unwrap(); + } + TestResult { result } } } @@ -900,4 +930,16 @@ mod test { .await .assert_err(); } + + #[tokio::test] + async fn two_validator_move_all_and_back_again() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .move_back_again() + .run_test(|_| Validators::All) + .await + .assert_ok(); + } } From 86c3bcd3613a056c269277969753ec033da3330b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 1 Dec 2022 20:32:07 +1100 Subject: [PATCH 086/138] Add misc changes --- common/eth2/src/lighthouse_vc/http_client.rs | 2 +- validator_manager/src/move_validators.rs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index a2e3e3f6ff9..e34d362b04c 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -487,7 +487,7 @@ impl ValidatorClientHttpClient { .await } - /// `DELETE eth/v1/keystores` + /// `DELETE lighthouse/keystores` pub async fn delete_lighthouse_keystores( &self, req: &DeleteKeystoresRequest, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index ab1db3c2cd9..b5766f53c64 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -31,6 +31,7 @@ pub const VALIDATORS_FLAG: &str = "validators"; pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; +pub const SKIP_WARNING_PROMPTS: &str = "skip-warning-prompts"; const NO_VALIDATORS_MSG: &str = "No validators present on source validator client"; @@ -128,6 +129,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .possible_values(&["true", "false"]) .takes_value(true), ) + .arg( + Arg::with_name(SKIP_WARNING_PROMPTS) + .long(SKIP_WARNING_PROMPTS) + .help( + "Don't display interactive warning prompts. Only provide \ + this flag if you know what the prompts say and you know \ + what you're doing.", + ) + .required(false) + .takes_value(false), + ) } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] @@ -167,6 +179,7 @@ pub struct MoveConfig { pub builder_proposals: Option, pub fee_recipient: Option
, pub gas_limit: Option, + pub skip_warning_prompts: bool, } impl MoveConfig { @@ -180,6 +193,7 @@ impl MoveConfig { builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, + skip_warning_prompts: matches.is_present(SKIP_WARNING_PROMPTS), }) } } @@ -206,6 +220,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { builder_proposals, fee_recipient, gas_limit, + skip_warning_prompts } = config; // Moving validators between the same VC is unlikely to be useful and probably indicates a user From bbcf7b857ef194677269a0eb2520250a6ff9eff8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 6 Dec 2022 10:28:37 +1100 Subject: [PATCH 087/138] Revert "Add misc changes" This reverts commit 86c3bcd3613a056c269277969753ec033da3330b. --- common/eth2/src/lighthouse_vc/http_client.rs | 2 +- validator_manager/src/move_validators.rs | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/common/eth2/src/lighthouse_vc/http_client.rs b/common/eth2/src/lighthouse_vc/http_client.rs index e34d362b04c..a2e3e3f6ff9 100644 --- a/common/eth2/src/lighthouse_vc/http_client.rs +++ b/common/eth2/src/lighthouse_vc/http_client.rs @@ -487,7 +487,7 @@ impl ValidatorClientHttpClient { .await } - /// `DELETE lighthouse/keystores` + /// `DELETE eth/v1/keystores` pub async fn delete_lighthouse_keystores( &self, req: &DeleteKeystoresRequest, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index b5766f53c64..ab1db3c2cd9 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -31,7 +31,6 @@ pub const VALIDATORS_FLAG: &str = "validators"; pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; -pub const SKIP_WARNING_PROMPTS: &str = "skip-warning-prompts"; const NO_VALIDATORS_MSG: &str = "No validators present on source validator client"; @@ -129,17 +128,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .possible_values(&["true", "false"]) .takes_value(true), ) - .arg( - Arg::with_name(SKIP_WARNING_PROMPTS) - .long(SKIP_WARNING_PROMPTS) - .help( - "Don't display interactive warning prompts. Only provide \ - this flag if you know what the prompts say and you know \ - what you're doing.", - ) - .required(false) - .takes_value(false), - ) } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] @@ -179,7 +167,6 @@ pub struct MoveConfig { pub builder_proposals: Option, pub fee_recipient: Option
, pub gas_limit: Option, - pub skip_warning_prompts: bool, } impl MoveConfig { @@ -193,7 +180,6 @@ impl MoveConfig { builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, - skip_warning_prompts: matches.is_present(SKIP_WARNING_PROMPTS), }) } } @@ -220,7 +206,6 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { builder_proposals, fee_recipient, gas_limit, - skip_warning_prompts } = config; // Moving validators between the same VC is unlikely to be useful and probably indicates a user From 6bef2beba41219a0ecb185c5fe415b906b5ff8f1 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 14 Dec 2022 11:29:12 +1100 Subject: [PATCH 088/138] Allow export without password --- validator_client/src/http_api/keystores.rs | 2 +- validator_client/src/initialized_validators.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/validator_client/src/http_api/keystores.rs b/validator_client/src/http_api/keystores.rs index 51e85837531..c2d9b4d67f4 100644 --- a/validator_client/src/http_api/keystores.rs +++ b/validator_client/src/http_api/keystores.rs @@ -333,7 +333,7 @@ fn delete_single_keystore( Ok(Some(keystore_and_password)) => Ok(SingleExportKeystoresResponse { status: Status::ok(DeleteKeystoreStatus::Deleted), validating_keystore: Some(KeystoreJsonStr(keystore_and_password.keystore)), - validating_keystore_password: Some(keystore_and_password.password), + validating_keystore_password: keystore_and_password.password, }), Ok(None) => Ok(SingleExportKeystoresResponse { status: Status::ok(DeleteKeystoreStatus::Deleted), diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 6213304a44c..2e9f83de0e4 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -45,7 +45,7 @@ const USE_STDIN: bool = false; pub struct KeystoreAndPassword { pub keystore: Keystore, - pub password: ZeroizeString, + pub password: Option, } #[derive(Debug)] @@ -103,7 +103,6 @@ pub enum Error { /// Unable to apply an action to a validator. InvalidActionOnValidator, UnableToReadValidatorPassword(String), - MissingKeystorePassword, UnableToReadKeystoreFile(eth2_keystore::Error), } @@ -561,10 +560,11 @@ impl InitializedValidators { .. } if is_local_keystore => { let password = match (voting_keystore_password, voting_keystore_password_path) { - (Some(password), _) => password.clone(), + (Some(password), _) => Some(password.clone()), (_, Some(path)) => read_password_string(path) + .map(Option::Some) .map_err(Error::UnableToReadValidatorPassword)?, - (None, None) => return Err(Error::MissingKeystorePassword), + (None, None) => None, }; let keystore = Keystore::from_json_file(voting_keystore_path) .map_err(Error::UnableToReadKeystoreFile)?; From 0abb6a3d60d11f03d851c4c91d2b7e11a938532c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 14 Dec 2022 11:55:12 +1100 Subject: [PATCH 089/138] Read password from user in move command --- validator_manager/src/common.rs | 1 + validator_manager/src/create_validators.rs | 1 - validator_manager/src/move_validators.rs | 50 ++++++++++++++++++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs index 6addb46a3b8..be4e8ae0093 100644 --- a/validator_manager/src/common.rs +++ b/validator_manager/src/common.rs @@ -15,6 +15,7 @@ use tree_hash::TreeHash; use types::*; pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; +pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; /// When the `ethereum/staking-deposit-cli` tool generates deposit data JSON, it adds a /// `deposit_cli_version` to protect the web-based "Launchpad" tool against a breaking change that diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index 0692d337d40..2704a1a271d 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -19,7 +19,6 @@ pub const OUTPUT_PATH_FLAG: &str = "output-path"; pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; pub const DISABLE_DEPOSITS_FLAG: &str = "disable-deposits"; pub const COUNT_FLAG: &str = "count"; -pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; pub const FIRST_INDEX_FLAG: &str = "first-index"; pub const MNEMONIC_FLAG: &str = "mnemonic-path"; pub const SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index ab1db3c2cd9..a3684039655 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -1,5 +1,6 @@ use super::common::*; use crate::DumpConfig; +use account_utils::read_password_from_user; use clap::{App, Arg, ArgMatches}; use eth2::{ lighthouse_vc::{ @@ -167,6 +168,7 @@ pub struct MoveConfig { pub builder_proposals: Option, pub fee_recipient: Option
, pub gas_limit: Option, + pub stdin_inputs: bool, } impl MoveConfig { @@ -180,6 +182,7 @@ impl MoveConfig { builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, + stdin_inputs: cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG), }) } } @@ -206,6 +209,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { builder_proposals, fee_recipient, gas_limit, + stdin_inputs, } = config; // Moving validators between the same VC is unlikely to be useful and probably indicates a user @@ -346,10 +350,48 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { validating_keystore_password, } => match (validating_keystore, validating_keystore_password) { (Some(keystore), Some(password)) => (keystore, password), - (keystore_opt, password_opt) => { + (Some(keystore), None) => { + eprintln!( + "Validator {:?} requires a password, please provide it to continue with \ + moving validators. If the provided password is incorrect the user will \ + be asked to provide another password. \ + Failing to provide the correct password now will \ + result in the keystore being deleted from the src VC \ + without being transfered to the dest VC. \ + The dest VC will store this password on its filesystem and it will not be \ + required next time the dest VC starts. \ + It is strongly recommend to provide a password now rather than exiting.", + pubkey_to_move + ); + + // Read the password from the user, retrying if the password is incorrect. + loop { + match read_password_from_user(stdin_inputs) { + Ok(password) => { + if let Err(e) = keystore.decrypt_keypair(password.as_ref()) { + eprintln!("Failed to decrypt keystore: {:?}", e); + } else { + break (keystore, password); + } + } + Err(e) => { + eprintln!( + "Retrying after error: {:?}. If this error persists the user will need to \ + manually recover their keystore for validator {:?} from the mnemonic." + , + e, pubkey_to_move + ); + } + } + + // Add a sleep here to prevent spamming the console. + sleep(Duration::from_secs(1)).await; + } + } + (None, password_opt) => { eprintln!( "Validator {:?} was not moved since the validator client did \ - not return both a keystore and password. It is likely that the \ + not return a keystore. It is likely that the \ validator has been deleted from the source validator client \ without being moved to the destination validator client. \ This validator will most likely need to be manually recovered \ @@ -357,8 +399,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { pubkey_to_move ); return Err(format!( - "VC returned deleted but keystore {}, password {}", - keystore_opt.is_some(), + "VC returned deleted but keystore not present (password {})", password_opt.is_some() )); } @@ -643,6 +684,7 @@ mod test { builder_proposals: None, fee_recipient: None, gas_limit: None, + stdin_inputs: false, }; let result = run(move_config).await; From afed3fb713aa09258e030744cb76445b5fda0d3b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 14 Dec 2022 12:50:47 +1100 Subject: [PATCH 090/138] Add test for supplying password --- .../src/initialized_validators.rs | 29 ++++++++ validator_manager/src/import_validators.rs | 2 +- validator_manager/src/move_validators.rs | 74 +++++++++++++++++-- 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 2e9f83de0e4..bd9053b18a2 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -1177,4 +1177,33 @@ impl InitializedValidators { val.index = Some(index); } } + + /// Deletes any passwords store in the validator definitions file and + /// returns a map of pubkey to deleted password. + pub fn delete_passwords_from_validator_definitions( + &mut self, + ) -> Result, Error> { + let mut passwords = HashMap::default(); + + for def in self.definitions.as_mut_slice() { + match &mut def.signing_definition { + SigningDefinition::LocalKeystore { + ref mut voting_keystore_password, + .. + } => { + if let Some(password) = voting_keystore_password.take() { + passwords.insert(def.voting_public_key.clone(), password); + } + } + // Remote signers don't have passwords. + SigningDefinition::Web3Signer { .. } => (), + }; + } + + self.definitions + .save(&self.validators_dir) + .map_err(Error::UnableToSaveDefinitions)?; + + Ok(passwords) + } } diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 92af5ed673c..c9e077eb858 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -246,7 +246,7 @@ pub mod tests { pub struct TestBuilder { import_config: ImportConfig, - vc: ApiTester, + pub vc: ApiTester, /// Holds the temp directory owned by the `CreateTestBuilder` so it doesn't get cleaned-up /// before we can read it. create_dir: Option, diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index a3684039655..67b559f75a5 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -1,6 +1,6 @@ use super::common::*; use crate::DumpConfig; -use account_utils::read_password_from_user; +use account_utils::{read_password_from_user, ZeroizeString}; use clap::{App, Arg, ArgMatches}; use eth2::{ lighthouse_vc::{ @@ -37,6 +37,30 @@ const NO_VALIDATORS_MSG: &str = "No validators present on source validator clien const UPLOAD_RETRY_WAIT: Duration = Duration::from_secs(5); +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum PasswordSource { + /// Reads the password from the user via the terminal. + Interactive { stdin_inputs: bool }, + /// This variant is panic-y and should only be used during testing. + Testing(HashMap>), +} + +impl PasswordSource { + fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result { + match self { + PasswordSource::Interactive { stdin_inputs } => read_password_from_user(*stdin_inputs), + // This path with panic if the password list is empty. Since the + // password prompt will just keep retrying on a failed password, the + // panic helps us break the loop if we misconfigure the test. + PasswordSource::Testing(passwords) => Ok(passwords + .get_mut(pubkey) + .expect("pubkey should be known") + .remove(0) + .into()), + } + } +} + pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .about( @@ -168,7 +192,7 @@ pub struct MoveConfig { pub builder_proposals: Option, pub fee_recipient: Option
, pub gas_limit: Option, - pub stdin_inputs: bool, + pub password_source: PasswordSource, } impl MoveConfig { @@ -182,7 +206,9 @@ impl MoveConfig { builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, - stdin_inputs: cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG), + password_source: PasswordSource::Interactive { + stdin_inputs: cfg!(windows) || matches.is_present(STDIN_INPUTS_FLAG), + }, }) } } @@ -209,7 +235,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { builder_proposals, fee_recipient, gas_limit, - stdin_inputs, + mut password_source, } = config; // Moving validators between the same VC is unlikely to be useful and probably indicates a user @@ -366,7 +392,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { // Read the password from the user, retrying if the password is incorrect. loop { - match read_password_from_user(stdin_inputs) { + match password_source.read_password(&pubkey_to_move) { Ok(password) => { if let Err(e) = keystore.decrypt_keypair(password.as_ref()) { eprintln!("Failed to decrypt keystore: {:?}", e); @@ -603,6 +629,7 @@ mod test { duplicates: usize, dir: TempDir, move_back_again: bool, + passwords: HashMap>, } impl TestBuilder { @@ -614,6 +641,7 @@ mod test { duplicates: 0, dir, move_back_again: false, + passwords: <_>::default(), } } @@ -645,6 +673,28 @@ mod test { self } + fn remove_passwords_from_src_vc(mut self) -> Self { + let passwords = self + .src_import_builder + .as_ref() + .expect("src validators must be created before passwords can be removed") + .vc + .initialized_validators + .write() + .delete_passwords_from_validator_definitions() + .unwrap(); + self.passwords = passwords + .into_iter() + .map(|(pubkey, password)| { + ( + PublicKeyBytes::from(&pubkey), + vec![password.as_str().to_string()], + ) + }) + .collect(); + self + } + async fn move_validators( &self, gen_validators_enum: F, @@ -684,7 +734,7 @@ mod test { builder_proposals: None, fee_recipient: None, gas_limit: None, - stdin_inputs: false, + password_source: PasswordSource::Testing(self.passwords.clone()), }; let result = run(move_config).await; @@ -984,4 +1034,16 @@ mod test { .await .assert_ok(); } + + #[tokio::test] + async fn one_validator_move_all_passwords_removed() { + TestBuilder::new() + .await + .with_src_validators(1, 0) + .await + .remove_passwords_from_src_vc() + .run_test(|_| Validators::All) + .await + .assert_ok(); + } } From d096a07a2144ae39962c3d82ba8183c4bbe76cf8 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 14 Dec 2022 16:18:19 +1100 Subject: [PATCH 091/138] Add more password tests --- validator_manager/src/move_validators.rs | 111 ++++++++++++++++++----- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 67b559f75a5..57ce8a0ee14 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -48,7 +48,10 @@ pub enum PasswordSource { impl PasswordSource { fn read_password(&mut self, pubkey: &PublicKeyBytes) -> Result { match self { - PasswordSource::Interactive { stdin_inputs } => read_password_from_user(*stdin_inputs), + PasswordSource::Interactive { stdin_inputs } => { + eprintln!("Please enter a password for keystore {:?}:", pubkey); + read_password_from_user(*stdin_inputs) + } // This path with panic if the password list is empty. Since the // password prompt will just keep retrying on a failed password, the // panic helps us break the loop if we misconfigure the test. @@ -378,14 +381,15 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { (Some(keystore), Some(password)) => (keystore, password), (Some(keystore), None) => { eprintln!( - "Validator {:?} requires a password, please provide it to continue with \ - moving validators. If the provided password is incorrect the user will \ + "Validator {:?} requires a password, please provide it to continue \ + moving validators. \ + The dest VC will store this password on its filesystem and the password \ + will not be required next time the dest VC starts. \ + If the provided password is incorrect the user will \ be asked to provide another password. \ Failing to provide the correct password now will \ result in the keystore being deleted from the src VC \ without being transfered to the dest VC. \ - The dest VC will store this password on its filesystem and it will not be \ - required next time the dest VC starts. \ It is strongly recommend to provide a password now rather than exiting.", pubkey_to_move ); @@ -629,6 +633,8 @@ mod test { duplicates: usize, dir: TempDir, move_back_again: bool, + remove_passwords_from_src_vc: bool, + mutate_passwords: Option>)>>, passwords: HashMap>, } @@ -641,6 +647,8 @@ mod test { duplicates: 0, dir, move_back_again: false, + remove_passwords_from_src_vc: false, + mutate_passwords: None, passwords: <_>::default(), } } @@ -674,24 +682,15 @@ mod test { } fn remove_passwords_from_src_vc(mut self) -> Self { - let passwords = self - .src_import_builder - .as_ref() - .expect("src validators must be created before passwords can be removed") - .vc - .initialized_validators - .write() - .delete_passwords_from_validator_definitions() - .unwrap(); - self.passwords = passwords - .into_iter() - .map(|(pubkey, password)| { - ( - PublicKeyBytes::from(&pubkey), - vec![password.as_str().to_string()], - ) - }) - .collect(); + self.remove_passwords_from_src_vc = true; + self + } + + fn mutate_passwords>) + 'static>( + mut self, + func: F, + ) -> Self { + self.mutate_passwords = Some(Box::new(func)); self } @@ -841,6 +840,28 @@ mod test { ApiTester::new().await }; + if self.remove_passwords_from_src_vc { + let passwords = src_vc + .initialized_validators + .write() + .delete_passwords_from_validator_definitions() + .unwrap(); + + self.passwords = passwords + .into_iter() + .map(|(pubkey, password)| { + ( + PublicKeyBytes::from(&pubkey), + vec![password.as_str().to_string()], + ) + }) + .collect(); + + if let Some(func) = self.mutate_passwords.take() { + func(&mut self.passwords) + } + } + let result = self .move_validators(gen_validators_enum, &src_vc, &dest_vc) .await; @@ -1036,12 +1057,52 @@ mod test { } #[tokio::test] - async fn one_validator_move_all_passwords_removed() { + async fn two_validator_move_all_passwords_removed() { TestBuilder::new() .await - .with_src_validators(1, 0) + .with_src_validators(2, 0) + .await + .remove_passwords_from_src_vc() + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + /// This test simulates a src VC that doesn't know the keystore passwords + /// and provide the wrong password before providing the correct password. + #[tokio::test] + async fn two_validator_move_all_passwords_removed_failed_password_attempt() { + TestBuilder::new() + .await + .with_src_validators(2, 0) + .await + .remove_passwords_from_src_vc() + .mutate_passwords(|passwords| { + passwords.iter_mut().for_each(|(_, passwords)| { + passwords.insert(0, "wrong-password".to_string()); + passwords.push("wrong-password".to_string()); + }) + }) + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + /// This test simulates a src VC that doesn't know the keystore passwords + /// and we have not provided the correct password. + #[should_panic] + #[tokio::test] + async fn two_validator_move_all_passwords_removed_without_correct_password() { + TestBuilder::new() + .await + .with_src_validators(2, 0) .await .remove_passwords_from_src_vc() + .mutate_passwords(|passwords| { + passwords + .iter_mut() + .for_each(|(_, passwords)| *passwords = vec!["wrong-password".to_string()]) + }) .run_test(|_| Validators::All) .await .assert_ok(); From 8ca7f321e3000a8e0cf07b7b72609dc401fd5862 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 15 Dec 2022 09:32:07 +1100 Subject: [PATCH 092/138] Add stdin-inputs flag to move command --- validator_manager/src/move_validators.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 57ce8a0ee14..54a6773fb1a 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -156,6 +156,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .possible_values(&["true", "false"]) .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."), + ) } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] From f27908a3e37ffa9fcc0c11424e9d098b45984661 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 13 Mar 2023 13:59:56 +1100 Subject: [PATCH 093/138] Update docs as per removal of `validators` --- book/src/validator-manager-move.md | 1 - book/src/validator-manager.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index cbfb7118df4..67cd4b44aa1 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -95,7 +95,6 @@ host** run the command to move the validators: ``` lighthouse \ validator-manager \ - validators \ move \ --src-vc-url http://localhost:6062 \ --src-vc-token ~/src-token.txt \ diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index 0b12beffde8..1b8b1f1a038 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -25,7 +25,7 @@ a live VC. The validator manager is generally superior to the account manager fo ### Create Validators -The `lighthouse validator-manager validators create` command accepts a mnemonic and produces a JSON +The `lighthouse validator-manager create` command accepts a mnemonic and produces a JSON file containing validator keystores that can be imported with the [Import Validators]() command. From 9c76d4506d1850cf32f3b1899f3defbf153f11d4 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 27 Mar 2023 11:35:07 +1100 Subject: [PATCH 094/138] Clarify help message --- validator_manager/src/move_validators.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 54a6773fb1a..91c011f31e3 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -116,9 +116,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long(VALIDATORS_FLAG) .value_name("STRING") .help( - "One or more validator public keys (as 0x-prefixed hex) to be moved from \ - the source to destination validator clients. Alternatively, use \"all\" to \ - move all the validators from the source validator client.", + "The validators to be moved. Either (a) a list of \ + 0x-prefixed validator pubkeys, (b) an integer count of \ + validators or (c) the word \"all\".", ) .required(true) .takes_value(true), From 1b225e944e1d6b350079a172cf3a426781ececff Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 27 Mar 2023 11:39:27 +1100 Subject: [PATCH 095/138] Tidy message --- validator_manager/src/move_validators.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 91c011f31e3..c6803ff4510 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -116,9 +116,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long(VALIDATORS_FLAG) .value_name("STRING") .help( - "The validators to be moved. Either (a) a list of \ - 0x-prefixed validator pubkeys, (b) an integer count of \ - validators or (c) the word \"all\".", + "The validators to be moved. Either a list of 0x-prefixed \ + validator pubkeys, an integer count of validators or the \ + word \"all\".", ) .required(true) .takes_value(true), From 707d2a258b9586145c3f75a1c8d232efc664b8f2 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 27 Mar 2023 11:56:41 +1100 Subject: [PATCH 096/138] Further improve help message --- validator_manager/src/move_validators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index c6803ff4510..9608eafa53c 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -118,7 +118,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help( "The validators to be moved. Either a list of 0x-prefixed \ validator pubkeys, an integer count of validators or the \ - word \"all\".", + keyword \"all\".", ) .required(true) .takes_value(true), From ad65cf65f3a90501e37ab79e537de6f11e9de804 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 10 Jul 2023 17:51:44 +1000 Subject: [PATCH 097/138] Fix clippy lints --- validator_client/src/http_api/test_utils.rs | 14 +++++++++++--- validator_client/src/http_api/tests.rs | 3 +++ validator_manager/src/common.rs | 1 + validator_manager/src/lib.rs | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index 23a37bbc97e..fc9083ea010 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -135,6 +135,7 @@ impl ApiTester { store_passwords_in_secrets_dir: false, }, log, + sse_logging_components: None, slot_clock, _phantom: PhantomData, }); @@ -146,7 +147,7 @@ impl ApiTester { }; let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); - tokio::spawn(async { server.await }); + tokio::spawn(server); let url = SensitiveUrl::parse(&format!( "http://{}:{}", @@ -504,7 +505,7 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None) + .patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None, None) .await .unwrap(); @@ -546,7 +547,13 @@ impl ApiTester { let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index]; self.client - .patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None) + .patch_lighthouse_validators( + &validator.voting_pubkey, + None, + Some(gas_limit), + None, + None, + ) .await .unwrap(); @@ -573,6 +580,7 @@ impl ApiTester { None, None, Some(builder_proposals), + None, ) .await .unwrap(); diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index dbb9d4d620c..d310a70324f 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -126,6 +126,7 @@ impl ApiTester { task_executor: executor, api_secret, validator_dir: Some(validator_dir.path().into()), + secrets_dir: Some(secrets_dir.path().into()), validator_store: Some(validator_store.clone()), graffiti_file: None, graffiti_flag: Some(Graffiti::default()), @@ -135,6 +136,8 @@ impl ApiTester { listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), listen_port: 0, allow_origin: None, + allow_keystore_export: true, + store_passwords_in_secrets_dir: false, }, sse_logging_components: None, log, diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs index cc302ea20fc..7bfcc2e66d8 100644 --- a/validator_manager/src/common.rs +++ b/validator_manager/src/common.rs @@ -135,6 +135,7 @@ impl ValidatorSpecification { enabled, gas_limit, builder_proposals, + None, // Grafitti field is not maintained between validator moves. ) .await .map_err(UploadError::PatchValidatorFailed)?; diff --git a/validator_manager/src/lib.rs b/validator_manager/src/lib.rs index e5033035c1f..6889ee79d2c 100644 --- a/validator_manager/src/lib.rs +++ b/validator_manager/src/lib.rs @@ -50,7 +50,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /// Run the account manager, returning an error if the operation did not succeed. pub fn run<'a, T: EthSpec>(matches: &'a ArgMatches<'a>, env: Environment) -> Result<(), String> { let context = env.core_context(); - let spec = context.eth2_config.spec.clone(); + let spec = context.eth2_config.spec; let dump_config = clap_utils::parse_optional(matches, DUMP_CONFIGS_FLAG)? .map(DumpConfig::Enabled) .unwrap_or_else(|| DumpConfig::Disabled); From 8d3aa4419b6fee42f05c45e882d7c3e6b590758a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 10 Jul 2023 18:30:46 +1000 Subject: [PATCH 098/138] Use async tests --- validator_client/src/http_api/tests.rs | 706 ++++++++++++------------- 1 file changed, 326 insertions(+), 380 deletions(-) diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index d310a70324f..affe7a99999 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -31,7 +31,7 @@ use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use task_executor::TaskExecutor; +use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::runtime::Runtime; use tokio::sync::oneshot; @@ -48,9 +48,8 @@ struct ApiTester { validator_store: Arc>, url: SensitiveUrl, slot_clock: TestingSlotClock, - _server_shutdown: oneshot::Sender<()>, _validator_dir: TempDir, - _runtime_shutdown: exit_future::Signal, + _test_runtime: TestRuntime, } // Builds a runtime to be used in the testing configuration. @@ -64,7 +63,7 @@ fn build_runtime() -> Arc { } impl ApiTester { - pub async fn new(runtime: std::sync::Weak) -> Self { + pub async fn new() -> Self { let log = test_logger(); let validator_dir = tempdir().unwrap(); @@ -100,9 +99,7 @@ impl ApiTester { Duration::from_secs(1), ); - let (runtime_shutdown, exit) = exit_future::signal(); - let (shutdown_tx, _) = futures::channel::mpsc::channel(1); - let executor = TaskExecutor::new(runtime.clone(), exit, log.clone(), shutdown_tx); + let test_runtime = TestRuntime::default(); let validator_store = Arc::new(ValidatorStore::<_, E>::new( initialized_validators, @@ -112,7 +109,7 @@ impl ApiTester { Some(Arc::new(DoppelgangerService::new(log.clone()))), slot_clock.clone(), &config, - executor.clone(), + test_runtime.task_executor.clone(), log.clone(), )); @@ -123,7 +120,7 @@ impl ApiTester { let initialized_validators = validator_store.initialized_validators(); let context = Arc::new(Context { - task_executor: executor, + task_executor: test_runtime.task_executor.clone(), api_secret, validator_dir: Some(validator_dir.path().into()), secrets_dir: Some(secrets_dir.path().into()), @@ -145,12 +142,8 @@ impl ApiTester { _phantom: PhantomData, }); let ctx = context.clone(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_shutdown = async { - // It's not really interesting why this triggered, just that it happened. - let _ = shutdown_rx.await; - }; - let (listening_socket, server) = super::serve(ctx, server_shutdown).unwrap(); + let (listening_socket, server) = + super::serve(ctx, test_runtime.task_executor.exit()).unwrap(); tokio::spawn(async { server.await }); @@ -169,9 +162,8 @@ impl ApiTester { validator_store, url, slot_clock, - _server_shutdown: shutdown_tx, _validator_dir: validator_dir, - _runtime_shutdown: runtime_shutdown, + _test_runtime: test_runtime, } } @@ -679,387 +671,341 @@ struct Web3SignerValidatorScenario { enabled: bool, } -#[test] -fn invalid_pubkey() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .invalidate_api_token() - .test_get_lighthouse_version_invalid() - .await; - }); +#[tokio::test] +async fn invalid_pubkey() { + ApiTester::new() + .await + .invalidate_api_token() + .test_get_lighthouse_version_invalid() + .await; } -#[test] -fn routes_with_invalid_auth() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .test_with_invalid_auth(|client| async move { client.get_lighthouse_version().await }) - .await - .test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await }) - .await - .test_with_invalid_auth(|client| async move { - client.get_lighthouse_spec::().await - }) - .await - .test_with_invalid_auth( - |client| async move { client.get_lighthouse_validators().await }, - ) - .await - .test_with_invalid_auth(|client| async move { - client - .get_lighthouse_validators_pubkey(&PublicKeyBytes::empty()) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .post_lighthouse_validators(vec![ValidatorRequest { - enable: <_>::default(), - description: <_>::default(), - graffiti: <_>::default(), - suggested_fee_recipient: <_>::default(), - gas_limit: <_>::default(), - builder_proposals: <_>::default(), - deposit_gwei: <_>::default(), - }]) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .post_lighthouse_validators_mnemonic(&CreateValidatorsMnemonicRequest { - mnemonic: String::default().into(), - key_derivation_path_offset: <_>::default(), - validators: <_>::default(), - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - let password = random_password(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) - .unwrap() - .build() - .unwrap(); - client - .post_lighthouse_validators_keystore(&KeystoreValidatorsPostRequest { - password: String::default().into(), - enable: <_>::default(), - keystore, - graffiti: <_>::default(), - suggested_fee_recipient: <_>::default(), - gas_limit: <_>::default(), - builder_proposals: <_>::default(), - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - client - .patch_lighthouse_validators( - &PublicKeyBytes::empty(), - Some(false), - None, - None, - None, - ) - .await - }) - .await - .test_with_invalid_auth(|client| async move { client.get_keystores().await }) - .await - .test_with_invalid_auth(|client| async move { - let password = random_password_string(); - let keypair = Keypair::random(); - let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) - .unwrap() - .build() - .map(KeystoreJsonStr) - .unwrap(); - client - .post_keystores(&ImportKeystoresRequest { - keystores: vec![keystore], - passwords: vec![password], - slashing_protection: None, - }) - .await - }) - .await - .test_with_invalid_auth(|client| async move { - let keypair = Keypair::random(); - client - .delete_keystores(&DeleteKeystoresRequest { - pubkeys: vec![keypair.pk.compress()], - }) - .await - }) - .await - }); +#[tokio::test] +async fn routes_with_invalid_auth() { + ApiTester::new() + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_version().await }) + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_health().await }) + .await + .test_with_invalid_auth(|client| async move { + client.get_lighthouse_spec::().await + }) + .await + .test_with_invalid_auth(|client| async move { client.get_lighthouse_validators().await }) + .await + .test_with_invalid_auth(|client| async move { + client + .get_lighthouse_validators_pubkey(&PublicKeyBytes::empty()) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .post_lighthouse_validators(vec![ValidatorRequest { + enable: <_>::default(), + description: <_>::default(), + graffiti: <_>::default(), + suggested_fee_recipient: <_>::default(), + gas_limit: <_>::default(), + builder_proposals: <_>::default(), + deposit_gwei: <_>::default(), + }]) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .post_lighthouse_validators_mnemonic(&CreateValidatorsMnemonicRequest { + mnemonic: String::default().into(), + key_derivation_path_offset: <_>::default(), + validators: <_>::default(), + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), String::new()) + .unwrap() + .build() + .unwrap(); + client + .post_lighthouse_validators_keystore(&KeystoreValidatorsPostRequest { + password: String::default().into(), + enable: <_>::default(), + keystore, + graffiti: <_>::default(), + suggested_fee_recipient: <_>::default(), + gas_limit: <_>::default(), + builder_proposals: <_>::default(), + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + client + .patch_lighthouse_validators( + &PublicKeyBytes::empty(), + Some(false), + None, + None, + None, + ) + .await + }) + .await + .test_with_invalid_auth(|client| async move { client.get_keystores().await }) + .await + .test_with_invalid_auth(|client| async move { + let password = random_password_string(); + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, password.as_ref(), String::new()) + .unwrap() + .build() + .map(KeystoreJsonStr) + .unwrap(); + client + .post_keystores(&ImportKeystoresRequest { + keystores: vec![keystore], + passwords: vec![password], + slashing_protection: None, + }) + .await + }) + .await + .test_with_invalid_auth(|client| async move { + let keypair = Keypair::random(); + client + .delete_keystores(&DeleteKeystoresRequest { + pubkeys: vec![keypair.pk.compress()], + }) + .await + }) + .await; } -#[test] -fn simple_getters() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .test_get_lighthouse_version() - .await - .test_get_lighthouse_health() - .await - .test_get_lighthouse_spec() - .await; - }); +#[tokio::test] +async fn simple_getters() { + ApiTester::new() + .await + .test_get_lighthouse_version() + .await + .test_get_lighthouse_health() + .await + .test_get_lighthouse_spec() + .await; } -#[test] -fn hd_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: true, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .create_hd_validators(HdValidatorScenario { - count: 1, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![0], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(3) - .create_hd_validators(HdValidatorScenario { - count: 0, - specify_mnemonic: true, - key_derivation_path_offset: 4, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(3); - }); +#[tokio::test] +async fn hd_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: true, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .create_hd_validators(HdValidatorScenario { + count: 1, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![0], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3) + .create_hd_validators(HdValidatorScenario { + count: 0, + specify_mnemonic: true, + key_derivation_path_offset: 4, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(3); } -#[test] -fn validator_exit() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .test_sign_voluntary_exits(0, None) - .await - .test_sign_voluntary_exits(0, Some(Epoch::new(256))) - .await; - }); +#[tokio::test] +async fn validator_exit() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .test_sign_voluntary_exits(0, None) + .await + .test_sign_voluntary_exits(0, Some(Epoch::new(256))) + .await; } -#[test] -fn validator_enabling() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2); - }); +#[tokio::test] +async fn validator_enabling() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2); } -#[test] -fn validator_gas_limit() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_gas_limit(0, 500) - .await - .assert_gas_limit(0, 500) - .await - // Update gas limit while validator is disabled. - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_gas_limit(0, 1000) - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_gas_limit(0, 1000) - .await - }); +#[tokio::test] +async fn validator_gas_limit() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_gas_limit(0, 500) + .await + .assert_gas_limit(0, 500) + .await + // Update gas limit while validator is disabled. + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_gas_limit(0, 1000) + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_gas_limit(0, 1000) + .await; } -#[test] -fn validator_builder_proposals() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_builder_proposals(0, true) - .await - // Test setting builder proposals while the validator is disabled - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_builder_proposals(0, false) - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_builder_proposals(0, false) - .await - }); +#[tokio::test] +async fn validator_builder_proposals() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_builder_proposals(0, true) + .await + // Test setting builder proposals while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_builder_proposals(0, false) + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_builder_proposals(0, false) + .await; } -#[test] -fn validator_graffiti() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .create_hd_validators(HdValidatorScenario { - count: 2, - specify_mnemonic: false, - key_derivation_path_offset: 0, - disabled: vec![], - }) - .await - .assert_enabled_validators_count(2) - .assert_validators_count(2) - .set_graffiti(0, "Mr F was here") - .await - .assert_graffiti(0, "Mr F was here") - .await - // Test setting graffiti while the validator is disabled - .set_validator_enabled(0, false) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2) - .set_graffiti(0, "Mr F was here again") - .await - .set_validator_enabled(0, true) - .await - .assert_enabled_validators_count(2) - .assert_graffiti(0, "Mr F was here again") - .await - }); +#[tokio::test] +async fn validator_graffiti() { + ApiTester::new() + .await + .create_hd_validators(HdValidatorScenario { + count: 2, + specify_mnemonic: false, + key_derivation_path_offset: 0, + disabled: vec![], + }) + .await + .assert_enabled_validators_count(2) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here") + .await + .assert_graffiti(0, "Mr F was here") + .await + // Test setting graffiti while the validator is disabled + .set_validator_enabled(0, false) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2) + .set_graffiti(0, "Mr F was here again") + .await + .set_validator_enabled(0, true) + .await + .assert_enabled_validators_count(2) + .assert_graffiti(0, "Mr F was here again") + .await; } -#[test] -fn keystore_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: true, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: false, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1) - .create_keystore_validators(KeystoreValidatorScenario { - correct_password: true, - enabled: false, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(2); - }); +#[tokio::test] +async fn keystore_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: false, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1) + .create_keystore_validators(KeystoreValidatorScenario { + correct_password: true, + enabled: false, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(2); } -#[test] -fn web3signer_validator_creation() { - let runtime = build_runtime(); - let weak_runtime = Arc::downgrade(&runtime); - runtime.block_on(async { - ApiTester::new(weak_runtime) - .await - .assert_enabled_validators_count(0) - .assert_validators_count(0) - .create_web3signer_validators(Web3SignerValidatorScenario { - count: 1, - enabled: true, - }) - .await - .assert_enabled_validators_count(1) - .assert_validators_count(1); - }); +#[tokio::test] +async fn web3signer_validator_creation() { + ApiTester::new() + .await + .assert_enabled_validators_count(0) + .assert_validators_count(0) + .create_web3signer_validators(Web3SignerValidatorScenario { + count: 1, + enabled: true, + }) + .await + .assert_enabled_validators_count(1) + .assert_validators_count(1); } From 4af863a385943a81969bf0a1df9c456a3dd193c6 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 11 Jul 2023 09:43:44 +1000 Subject: [PATCH 099/138] Fix warnings --- validator_client/src/http_api/tests.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index affe7a99999..f2b30d39fd1 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -34,7 +34,6 @@ use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; use tokio::runtime::Runtime; -use tokio::sync::oneshot; use types::graffiti::GraffitiString; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; @@ -52,16 +51,6 @@ struct ApiTester { _test_runtime: TestRuntime, } -// Builds a runtime to be used in the testing configuration. -fn build_runtime() -> Arc { - Arc::new( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("Should be able to build a testing runtime"), - ) -} - impl ApiTester { pub async fn new() -> Self { let log = test_logger(); From fbb844da12578fc43e4f440d04c59808ede0f7d5 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 11 Jul 2023 10:24:30 +1000 Subject: [PATCH 100/138] Add `--count` flag as per @michaelsproul's comment --- lighthouse/tests/validator_manager.rs | 2 +- validator_client/src/http_api/tests.rs | 1 - validator_manager/src/common.rs | 1 + validator_manager/src/create_validators.rs | 1 - validator_manager/src/move_validators.rs | 49 ++++++++++++---------- 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index 653e4a56421..df6a77bbea2 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -321,7 +321,7 @@ pub fn validator_move_count() { .flag("--src-vc-token", Some("./1.json")) .flag("--dest-vc-url", Some("http://localhost:2")) .flag("--dest-vc-token", Some("./2.json")) - .flag("--validators", Some("42")) + .flag("--count", Some("42")) .assert_success(|config| { let expected = MoveConfig { src_vc_url: SensitiveUrl::parse("http://localhost:1").unwrap(), diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index f2b30d39fd1..3bff444703b 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -33,7 +33,6 @@ use std::sync::Arc; use std::time::Duration; use task_executor::test_utils::TestRuntime; use tempfile::{tempdir, TempDir}; -use tokio::runtime::Runtime; use types::graffiti::GraffitiString; const PASSWORD_BYTES: &[u8] = &[42, 50, 37]; diff --git a/validator_manager/src/common.rs b/validator_manager/src/common.rs index 7bfcc2e66d8..6a3f93a3f78 100644 --- a/validator_manager/src/common.rs +++ b/validator_manager/src/common.rs @@ -16,6 +16,7 @@ use types::*; pub const IGNORE_DUPLICATES_FLAG: &str = "ignore-duplicates"; pub const STDIN_INPUTS_FLAG: &str = "stdin-inputs"; +pub const COUNT_FLAG: &str = "count"; /// When the `ethereum/staking-deposit-cli` tool generates deposit data JSON, it adds a /// `deposit_cli_version` to protect the web-based "Launchpad" tool against a breaking change that diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index 2704a1a271d..ffa7b13a1c2 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -18,7 +18,6 @@ pub const CMD: &str = "create"; pub const OUTPUT_PATH_FLAG: &str = "output-path"; pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; pub const DISABLE_DEPOSITS_FLAG: &str = "disable-deposits"; -pub const COUNT_FLAG: &str = "count"; pub const FIRST_INDEX_FLAG: &str = "first-index"; pub const MNEMONIC_FLAG: &str = "mnemonic-path"; pub const SPECIFY_VOTING_KEYSTORE_PASSWORD_FLAG: &str = "specify-voting-keystore-password"; diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 9608eafa53c..16b40204f6d 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -120,7 +120,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { validator pubkeys, an integer count of validators or the \ keyword \"all\".", ) - .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name(COUNT_FLAG) + .long(COUNT_FLAG) + .value_name("VALIDATOR_COUNT") + .help("The number of validators to move.") + .conflicts_with(VALIDATORS_FLAG) .takes_value(true), ) .arg( @@ -172,26 +179,6 @@ pub enum Validators { Specific(Vec), } -impl FromStr for Validators { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "all" => Ok(Validators::All), - pubkeys if pubkeys.starts_with("0x") => pubkeys - .split(',') - .map(PublicKeyBytes::from_str) - .collect::>() - .map(Validators::Specific), - other => usize::from_str(other) - .map_err(|_| { - "Expected \"all\", a list of 0x-prefixed pubkeys or an integer".to_string() - }) - .map(Validators::Count), - } - } -} - #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] pub struct MoveConfig { pub src_vc_url: SensitiveUrl, @@ -207,12 +194,30 @@ pub struct MoveConfig { impl MoveConfig { fn from_cli(matches: &ArgMatches) -> Result { + let count_flag = clap_utils::parse_optional(matches, COUNT_FLAG)?; + let validators_flag = matches.value_of(VALIDATORS_FLAG); + let validators = match (count_flag, validators_flag) { + (Some(count), None) => Validators::Count(count), + (None, Some(string)) => match string { + "all" => Validators::All, + pubkeys => pubkeys + .split(',') + .map(PublicKeyBytes::from_str) + .collect::, _>>() + .map(Validators::Specific)?, + }, + (None, None) => Err("Must supply either --{:VALIDATORS_FLAG} or --{:COUNT_FLAG}.")?, + (Some(_), Some(_)) => { + Err("Cannot supply both --{:VALIDATORS_FLAG} and --{:COUNT_FLAG}.")? + } + }; + Ok(Self { src_vc_url: clap_utils::parse_required(matches, SRC_VC_URL_FLAG)?, src_vc_token_path: clap_utils::parse_required(matches, SRC_VC_TOKEN_FLAG)?, dest_vc_url: clap_utils::parse_required(matches, DEST_VC_URL_FLAG)?, dest_vc_token_path: clap_utils::parse_required(matches, DEST_VC_TOKEN_FLAG)?, - validators: clap_utils::parse_required(matches, VALIDATORS_FLAG)?, + validators, builder_proposals: clap_utils::parse_optional(matches, BUILDER_PROPOSALS_FLAG)?, fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, From 66fd861c05a71fafec5c95cf01269aba3e0c0d66 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 11 Jul 2023 12:00:08 +1000 Subject: [PATCH 101/138] Update `KeyCache` after removing validator --- .../src/initialized_validators.rs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index c60fcea90e4..bf9890d6b6b 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -105,6 +105,7 @@ pub enum Error { InvalidActionOnValidator, UnableToReadValidatorPassword(String), UnableToReadKeystoreFile(eth2_keystore::Error), + UnableToSaveKeyCache(key_cache::Error), } impl From for Error { @@ -551,6 +552,7 @@ impl InitializedValidators { // // We disable before removing so that in case of a crash the auto-discovery mechanism // won't re-activate the keystore. + let mut uuid_opt = None; let keystore_and_password = if let Some(def) = self .definitions .as_mut_slice() @@ -573,6 +575,7 @@ impl InitializedValidators { }; let keystore = Keystore::from_json_file(voting_keystore_path) .map_err(Error::UnableToReadKeystoreFile)?; + uuid_opt = Some(*keystore.uuid()); def.enabled = false; self.definitions @@ -591,7 +594,27 @@ impl InitializedValidators { return Err(Error::ValidatorNotInitialized(pubkey.clone())); }; - // 2. Delete from `self.validators`, which holds the signing method. + // 2. Remove the validator from the key cache. This ensures the key + // cache is consistent next time the VC starts. + // + // It's not a big deal if this succeeds and something fails later in + // this function because the VC will self-heal from a corrupt key cache. + // + // Do this before modifying `self.validators` or deleting anything from + // the filesystem. + if let Some(uuid) = uuid_opt { + let key_cache = KeyCache::open_or_create(&self.validators_dir) + .map_err(Error::UnableToOpenKeyCache)?; + let mut decrypted_key_cache = self + .decrypt_key_cache(key_cache, &mut <_>::default()) + .await?; + decrypted_key_cache.remove(&uuid); + decrypted_key_cache + .save(&self.validators_dir) + .map_err(Error::UnableToSaveKeyCache)?; + } + + // 3. Delete from `self.validators`, which holds the signing method. // Delete the keystore files. if let Some(initialized_validator) = self.validators.remove(&pubkey.compress()) { if let SigningMethod::LocalKeystore { @@ -609,7 +632,7 @@ impl InitializedValidators { } } - // 3. Delete from validator definitions entirely. + // 4. Delete from validator definitions entirely. self.definitions .retain(|def| &def.voting_public_key != pubkey); self.definitions From 4c4f2271e161c42c8b6a6be14aec079d3ec18df1 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 11 Jul 2023 12:26:40 +1000 Subject: [PATCH 102/138] Check key cache consistency in tests --- validator_client/src/http_api/test_utils.rs | 23 ++++++++++++++--- .../src/initialized_validators.rs | 25 ++++++++++++++----- validator_manager/src/import_validators.rs | 2 ++ validator_manager/src/move_validators.rs | 3 +++ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index fc9083ea010..d221de1425f 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -1,7 +1,8 @@ use crate::doppelganger_service::DoppelgangerService; +use crate::key_cache::{KeyCache, CACHE_FILENAME}; use crate::{ http_api::{ApiSecret, Config as HttpConfig, Context}, - initialized_validators::InitializedValidators, + initialized_validators::{InitializedValidators, OnDecryptFailure}, Config, ValidatorDefinitions, ValidatorStore, }; use account_utils::{ @@ -59,7 +60,7 @@ pub struct ApiTester { pub api_token: String, pub test_runtime: TestRuntime, pub _server_shutdown: oneshot::Sender<()>, - pub _validator_dir: TempDir, + pub validator_dir: TempDir, } impl ApiTester { @@ -166,10 +167,26 @@ impl ApiTester { api_token: api_pubkey, test_runtime, _server_shutdown: shutdown_tx, - _validator_dir: validator_dir, + validator_dir, } } + /// Checks that the key cache exists and can be decrypted with the current + /// set of known validators. + pub async fn ensure_key_cache_consistency(&self) { + assert!( + self.validator_dir.as_ref().join(CACHE_FILENAME).exists(), + "the key cache should exist" + ); + let key_cache = + KeyCache::open_or_create(self.validator_dir.as_ref()).expect("should open a key cache"); + self.initialized_validators + .read() + .decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::Error) + .await + .expect("key cache should decypt"); + } + pub fn invalid_token_client(&self) -> ValidatorClientHttpClient { let tmp = tempdir().unwrap(); let api_secret = ApiSecret::create_or_open(tmp.path()).unwrap(); diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index bf9890d6b6b..6eebeb78205 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -44,6 +44,14 @@ const DEFAULT_REMOTE_SIGNER_REQUEST_TIMEOUT: Duration = Duration::from_secs(12); // Use TTY instead of stdin to capture passwords from users. const USE_STDIN: bool = false; +pub enum OnDecryptFailure { + /// If the key cache fails to decrypt, create a new cache. + CreateNew, + /// Return an error if the key cache fails to decrypt. This should only be + /// used in testing. + Error, +} + pub struct KeystoreAndPassword { pub keystore: Keystore, pub password: Option, @@ -106,6 +114,7 @@ pub enum Error { UnableToReadValidatorPassword(String), UnableToReadKeystoreFile(eth2_keystore::Error), UnableToSaveKeyCache(key_cache::Error), + UnableToDecryptKeyCache(key_cache::Error), } impl From for Error { @@ -606,7 +615,7 @@ impl InitializedValidators { let key_cache = KeyCache::open_or_create(&self.validators_dir) .map_err(Error::UnableToOpenKeyCache)?; let mut decrypted_key_cache = self - .decrypt_key_cache(key_cache, &mut <_>::default()) + .decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::CreateNew) .await?; decrypted_key_cache.remove(&uuid); decrypted_key_cache @@ -949,10 +958,11 @@ impl InitializedValidators { /// filesystem accesses for keystores that are already known. In the case that a keystore /// from the validator definitions is not yet in this map, it will be loaded from disk and /// inserted into the map. - async fn decrypt_key_cache( + pub async fn decrypt_key_cache( &self, mut cache: KeyCache, key_stores: &mut HashMap, + on_failure: OnDecryptFailure, ) -> Result { // Read relevant key stores from the filesystem. let mut definitions_map = HashMap::new(); @@ -1020,11 +1030,13 @@ impl InitializedValidators { //decrypt tokio::task::spawn_blocking(move || match cache.decrypt(passwords, public_keys) { - Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => cache, - _ => KeyCache::new(), + Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => Ok(cache), + _ if matches!(on_failure, OnDecryptFailure::CreateNew) => Ok(KeyCache::new()), + Err(e) => Err(e), }) .await - .map_err(Error::TokioJoin) + .map_err(Error::TokioJoin)? + .map_err(Error::UnableToDecryptKeyCache) } /// Scans `self.definitions` and attempts to initialize and validators which are not already @@ -1062,7 +1074,8 @@ impl InitializedValidators { // Only decrypt cache when there is at least one local definition. // Decrypting cache is a very expensive operation which is never used for web3signer. let mut key_cache = if has_local_definitions { - self.decrypt_key_cache(cache, &mut key_stores).await? + self.decrypt_key_cache(cache, &mut key_stores, OnDecryptFailure::CreateNew) + .await? } else { // Assign an empty KeyCache if all definitions are of the Web3Signer type. KeyCache::new() diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index c9e077eb858..10db19b008f 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -307,6 +307,8 @@ pub mod tests { let result = run(self.import_config.clone()).await; if result.is_ok() { + self.vc.ensure_key_cache_consistency().await; + let local_validators: Vec = { let contents = fs::read_to_string(&self.import_config.validators_file_path).unwrap(); diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 16b40204f6d..cacfbb5dc5f 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -754,6 +754,9 @@ mod test { let src_vc_final_keystores = src_vc_client.get_keystores().await.unwrap().data; let dest_vc_final_keystores = dest_vc_client.get_keystores().await.unwrap().data; + src_vc.ensure_key_cache_consistency().await; + dest_vc.ensure_key_cache_consistency().await; + match validators { Validators::All => { assert!( From c7ee1ead0b843a68f3e61ef0e7af726740302ac4 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 11 Jul 2023 14:21:14 +1000 Subject: [PATCH 103/138] Fix clippy lints --- validator_client/src/http_api/test_utils.rs | 2 ++ validator_client/src/key_cache.rs | 6 ++++++ validator_manager/src/create_validators.rs | 2 +- validator_manager/src/move_validators.rs | 4 +++- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index d221de1425f..a1bedb59bb2 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -173,6 +173,7 @@ impl ApiTester { /// Checks that the key cache exists and can be decrypted with the current /// set of known validators. + #[allow(clippy::await_holding_lock)] // This is a test, so it should be fine. pub async fn ensure_key_cache_consistency(&self) { assert!( self.validator_dir.as_ref().join(CACHE_FILENAME).exists(), @@ -180,6 +181,7 @@ impl ApiTester { ); let key_cache = KeyCache::open_or_create(self.validator_dir.as_ref()).expect("should open a key cache"); + self.initialized_validators .read() .decrypt_key_cache(key_cache, &mut <_>::default(), OnDecryptFailure::Error) diff --git a/validator_client/src/key_cache.rs b/validator_client/src/key_cache.rs index b7abaaed069..c2dd7aa8fed 100644 --- a/validator_client/src/key_cache.rs +++ b/validator_client/src/key_cache.rs @@ -47,6 +47,12 @@ pub struct KeyCache { type SerializedKeyMap = HashMap; +impl Default for KeyCache { + fn default() -> Self { + Self::new() + } +} + impl KeyCache { pub fn new() -> Self { KeyCache { diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index ffa7b13a1c2..e83a50d54e6 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -863,7 +863,7 @@ pub mod tests { } }) .unwrap(); - let contents = fs::read_to_string(&path).unwrap(); + let contents = fs::read_to_string(path).unwrap(); serde_json::from_str(&contents).unwrap() }; diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index cacfbb5dc5f..19074de5cc3 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -639,6 +639,8 @@ mod test { const SRC_VC_TOKEN_FILE_NAME: &str = "src_vc_token.json"; const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; + type MutatePasswordFn = Box>)>; + struct TestBuilder { src_import_builder: Option, dest_import_builder: Option, @@ -646,7 +648,7 @@ mod test { dir: TempDir, move_back_again: bool, remove_passwords_from_src_vc: bool, - mutate_passwords: Option>)>>, + mutate_passwords: Option, passwords: HashMap>, } From 98bcb947cab8473ecd361cef066d0204f198034b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 12 Jul 2023 14:51:18 +1000 Subject: [PATCH 104/138] Reduce loglevel for `UnknownPubkey` in the VC --- validator_client/src/attestation_service.rs | 30 +++++++++- validator_client/src/block_service.rs | 55 ++++++++++++++++--- validator_client/src/duties_service.rs | 14 +++++ validator_client/src/duties_service/sync.rs | 13 +++++ validator_client/src/preparation_service.rs | 19 ++++++- .../src/sync_committee_service.rs | 29 +++++++++- 6 files changed, 145 insertions(+), 15 deletions(-) diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index a7118aa945c..f0a9258c747 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -2,12 +2,12 @@ use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; use crate::{ duties_service::{DutiesService, DutyAndProof}, http_metrics::metrics, - validator_store::ValidatorStore, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, OfflineOnFailure, }; use environment::RuntimeContext; use futures::future::join_all; -use slog::{crit, error, info, trace}; +use slog::{crit, debug, error, info, trace, warn}; use slot_clock::SlotClock; use std::collections::HashMap; use std::ops::Deref; @@ -395,6 +395,20 @@ impl AttestationService { .await { Ok(()) => Some((attestation, duty.validator_index)), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + warn!( + log, + "Missing pubkey for attestation"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "validator" => ?duty.pubkey, + "committee_index" => committee_index, + "slot" => slot.as_u64(), + ); + None + } Err(e) => { crit!( log, @@ -527,10 +541,20 @@ impl AttestationService { .await { Ok(aggregate) => Some(aggregate), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for aggregate"; + "pubkey" => ?pubkey, + ); + None + } Err(e) => { crit!( log, - "Failed to sign attestation"; + "Failed to sign aggregate"; "error" => ?e, "pubkey" => ?duty.pubkey, ); diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index d22e6c95f35..2a09455b6ff 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -5,7 +5,10 @@ use crate::{ graffiti_file::GraffitiFile, OfflineOnFailure, }; -use crate::{http_metrics::metrics, validator_store::ValidatorStore}; +use crate::{ + http_metrics::metrics, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, +}; use environment::RuntimeContext; use eth2::BeaconNodeHttpClient; use slog::{crit, debug, error, info, trace, warn}; @@ -417,17 +420,31 @@ impl BlockService { BlockError::Recoverable("Unable to determine current slot from clock".to_string()) })?; - let randao_reveal = self + let randao_reveal = match self .validator_store .randao_reveal(validator_pubkey, slot.epoch(E::slots_per_epoch())) .await - .map_err(|e| { - BlockError::Recoverable(format!( + { + Ok(signature) => signature.into(), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently removed + // via the API. + warn!( + log, + "Missing pubkey for block randao"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "slot" => ?slot + ); + return Ok(()); + } + Err(e) => { + return Err(BlockError::Recoverable(format!( "Unable to produce randao reveal signature: {:?}", e - )) - })? - .into(); + ))) + } + }; let graffiti = determine_graffiti( &validator_pubkey, @@ -522,11 +539,31 @@ impl BlockService { .await?; let signing_timer = metrics::start_timer(&metrics::BLOCK_SIGNING_TIMES); - let signed_block = self_ref + let signed_block = match self_ref .validator_store .sign_block::(*validator_pubkey_ref, block, current_slot) .await - .map_err(|e| BlockError::Recoverable(format!("Unable to sign block: {:?}", e)))?; + { + Ok(block) => block, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently removed + // via the API. + warn!( + log, + "Missing pubkey for block"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + "slot" => ?slot + ); + return Ok(()); + } + Err(e) => { + return Err(BlockError::Recoverable(format!( + "Unable to sign block: {:?}", + e + ))) + } + }; let signing_time_ms = Duration::from_secs_f64(signing_timer.map_or(0.0, |t| t.stop_and_record())).as_millis(); diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 83cdb936aa3..535f6aeb0a7 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -932,6 +932,20 @@ async fn fill_in_selection_proofs( for result in duty_and_proof_results { let duty_and_proof = match result { Ok(duty_and_proof) => duty_and_proof, + Err(Error::FailedToProduceSelectionProof( + ValidatorStoreError::UnknownPubkey(pubkey), + )) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + warn!( + log, + "Missing pubkey for duty and proof"; + "info" => "a validator may have recently been removed from this VC", + "pubkey" => ?pubkey, + ); + // Do not abort the entire batch for a single failure. + continue; + } Err(e) => { error!( log, diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/src/duties_service/sync.rs index 7a852091aa3..1e66d947a21 100644 --- a/validator_client/src/duties_service/sync.rs +++ b/validator_client/src/duties_service/sync.rs @@ -2,6 +2,7 @@ use crate::beacon_node_fallback::{OfflineOnFailure, RequireSynced}; use crate::{ doppelganger_service::DoppelgangerStatus, duties_service::{DutiesService, Error}, + validator_store::Error as ValidatorStoreError, }; use futures::future::join_all; use itertools::Itertools; @@ -539,6 +540,18 @@ pub async fn fill_in_aggregation_proofs( .await { Ok(proof) => proof, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync selection proof"; + "pubkey" => ?pubkey, + "pubkey" => ?duty.pubkey, + "slot" => slot, + ); + return None; + } Err(e) => { warn!( log, diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index 7d6e1744c83..2d2221680f9 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -1,5 +1,5 @@ use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; -use crate::validator_store::{DoppelgangerStatus, ValidatorStore}; +use crate::validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; use crate::OfflineOnFailure; use bls::PublicKeyBytes; use environment::RuntimeContext; @@ -442,8 +442,23 @@ impl PreparationService { .await { Ok(data) => data, + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for registration data"; + "pubkey" => ?pubkey, + ); + continue; + } Err(e) => { - error!(log, "Unable to sign validator registration data"; "error" => ?e, "pubkey" => ?pubkey); + error!( + log, + "Unable to sign validator registration data"; + "error" => ?e, + "pubkey" => ?pubkey + ); continue; } }; diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs index cc20cedfc6c..e01bf09cf2f 100644 --- a/validator_client/src/sync_committee_service.rs +++ b/validator_client/src/sync_committee_service.rs @@ -1,5 +1,9 @@ use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; -use crate::{duties_service::DutiesService, validator_store::ValidatorStore, OfflineOnFailure}; +use crate::{ + duties_service::DutiesService, + validator_store::{Error as ValidatorStoreError, ValidatorStore}, + OfflineOnFailure, +}; use environment::RuntimeContext; use eth2::types::BlockId; use futures::future::join_all; @@ -264,6 +268,18 @@ impl SyncCommitteeService { .await { Ok(signature) => Some(signature), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync committee signature"; + "pubkey" => ?pubkey, + "validator_index" => duty.validator_index, + "slot" => slot, + ); + None + } Err(e) => { crit!( log, @@ -405,6 +421,17 @@ impl SyncCommitteeService { .await { Ok(signed_contribution) => Some(signed_contribution), + Err(ValidatorStoreError::UnknownPubkey(pubkey)) => { + // A pubkey can be missing when a validator was recently + // removed via the API. + debug!( + log, + "Missing pubkey for sync contribution"; + "pubkey" => ?pubkey, + "slot" => slot, + ); + None + } Err(e) => { crit!( log, From 038c7a2718e66117b26abaa10843c38711018823 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 12 Jul 2023 15:03:17 +1000 Subject: [PATCH 105/138] Update help text for move command --- validator_manager/src/move_validators.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 19074de5cc3..4b06fea1de4 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -69,7 +69,8 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .about( "Uploads validators to a validator client using the HTTP API. The validators \ are defined in a JSON file which can be generated using the \"create-validators\" \ - command.", + command. This command only supports validators signing via a keystore on the local \ + file system (i.e., not Web3Signer validators).", ) .arg( Arg::with_name(SRC_VC_URL_FLAG) From 856cd7e37db11e0318dff9db0979d67b79c2a633 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 12 Jul 2023 17:53:04 +1000 Subject: [PATCH 106/138] Delete password files unless its being used elsewhere --- .../src/validator_definitions.rs | 17 +++ validator_client/src/http_api/test_utils.rs | 26 +++- .../src/initialized_validators.rs | 31 +++- validator_manager/src/import_validators.rs | 8 +- validator_manager/src/move_validators.rs | 141 +++++++++++++++++- 5 files changed, 205 insertions(+), 18 deletions(-) diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 1b01bedb749..c91e717d11b 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -124,6 +124,16 @@ impl SigningDefinition { SigningDefinition::Web3Signer(_) => Ok(None), } } + + pub fn voting_keystore_password_path(&self) -> Option<&PathBuf> { + match self { + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => Some(path), + _ => None, + } + } } /// A validator that may be initialized by this validator client. @@ -384,6 +394,13 @@ impl ValidatorDefinitions { pub fn as_mut_slice(&mut self) -> &mut [ValidatorDefinition] { self.0.as_mut_slice() } + + // Returns an iterator over all the `voting_keystore_password_paths` in self. + pub fn iter_voting_keystore_password_paths(&self) -> impl Iterator { + self.0 + .iter() + .filter_map(|def| def.signing_definition.voting_keystore_password_path()) + } } /// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to diff --git a/validator_client/src/http_api/test_utils.rs b/validator_client/src/http_api/test_utils.rs index a1bedb59bb2..c7558dd586d 100644 --- a/validator_client/src/http_api/test_utils.rs +++ b/validator_client/src/http_api/test_utils.rs @@ -61,10 +61,15 @@ pub struct ApiTester { pub test_runtime: TestRuntime, pub _server_shutdown: oneshot::Sender<()>, pub validator_dir: TempDir, + pub secrets_dir: TempDir, } impl ApiTester { pub async fn new() -> Self { + Self::new_with_http_config(Self::default_http_config()).await + } + + pub async fn new_with_http_config(http_config: HttpConfig) -> Self { let log = test_logger(); let validator_dir = tempdir().unwrap(); @@ -127,14 +132,7 @@ impl ApiTester { graffiti_file: None, graffiti_flag: Some(Graffiti::default()), spec: E::default_spec(), - config: HttpConfig { - enabled: true, - listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - listen_port: 0, - allow_origin: None, - allow_keystore_export: true, - store_passwords_in_secrets_dir: false, - }, + config: http_config, log, sse_logging_components: None, slot_clock, @@ -168,6 +166,18 @@ impl ApiTester { test_runtime, _server_shutdown: shutdown_tx, validator_dir, + secrets_dir, + } + } + + pub fn default_http_config() -> HttpConfig { + HttpConfig { + enabled: true, + listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + listen_port: 0, + allow_origin: None, + allow_keystore_export: true, + store_passwords_in_secrets_dir: false, } } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 6eebeb78205..b6c535f501f 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -115,6 +115,7 @@ pub enum Error { UnableToReadKeystoreFile(eth2_keystore::Error), UnableToSaveKeyCache(key_cache::Error), UnableToDecryptKeyCache(key_cache::Error), + UnableToDeletePasswordFile(PathBuf, io::Error), } impl From for Error { @@ -562,6 +563,7 @@ impl InitializedValidators { // We disable before removing so that in case of a crash the auto-discovery mechanism // won't re-activate the keystore. let mut uuid_opt = None; + let mut password_path_opt = None; let keystore_and_password = if let Some(def) = self .definitions .as_mut_slice() @@ -577,9 +579,12 @@ impl InitializedValidators { } if is_local_keystore => { let password = match (voting_keystore_password, voting_keystore_password_path) { (Some(password), _) => Some(password.clone()), - (_, Some(path)) => read_password_string(path) - .map(Option::Some) - .map_err(Error::UnableToReadValidatorPassword)?, + (_, Some(path)) => { + password_path_opt = Some(path.clone()); + read_password_string(path) + .map(Option::Some) + .map_err(Error::UnableToReadValidatorPassword)? + } (None, None) => None, }; let keystore = Keystore::from_json_file(voting_keystore_path) @@ -648,6 +653,20 @@ impl InitializedValidators { .save(&self.validators_dir) .map_err(Error::UnableToSaveDefinitions)?; + // 4. Delete the keystore password if it's not being used by any definition. + if let Some(password_path) = password_path_opt.and_then(|p| p.canonicalize().ok()) { + if self + .definitions + .iter_voting_keystore_password_paths() + // Require canonicalized paths so we can do a true equality check. + .filter_map(|existing| existing.canonicalize().ok()) + .all(|existing| existing != password_path) + { + fs::remove_file(&password_path) + .map_err(|e| Error::UnableToDeletePasswordFile(password_path.into(), e))?; + } + } + Ok(keystore_and_password) } @@ -1282,4 +1301,10 @@ impl InitializedValidators { Ok(passwords) } + + /// Prefer other methods in production. Arbitrarily modifying a validator + /// definition manually may result in inconsistencies. + pub fn as_mut_slice_testing_only(&mut self) -> &mut [ValidatorDefinition] { + self.definitions.as_mut_slice() + } } diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index 10db19b008f..e85f7763537 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -240,7 +240,7 @@ pub mod tests { use crate::create_validators::tests::TestBuilder as CreateTestBuilder; use std::fs; use tempfile::{tempdir, TempDir}; - use validator_client::http_api::test_utils::ApiTester; + use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; const VC_TOKEN_FILE_NAME: &str = "vc_token.json"; @@ -255,8 +255,12 @@ pub mod tests { impl TestBuilder { pub async fn new() -> Self { + Self::new_with_http_config(ApiTester::default_http_config()).await + } + + pub async fn new_with_http_config(http_config: HttpConfig) -> Self { let dir = tempdir().unwrap(); - let vc = ApiTester::new().await; + let vc = ApiTester::new_with_http_config(http_config).await; let vc_token_path = dir.path().join(VC_TOKEN_FILE_NAME); fs::write(&vc_token_path, &vc.api_token).unwrap(); diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 4b06fea1de4..f41b5675e31 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -633,9 +633,10 @@ async fn sleep_with_retry_message(pubkey: &PublicKeyBytes, path: Option<&str>) { mod test { use super::*; use crate::import_validators::tests::TestBuilder as ImportTestBuilder; + use account_utils::validator_definitions::SigningDefinition; use std::fs; use tempfile::{tempdir, TempDir}; - use validator_client::http_api::test_utils::ApiTester; + use validator_client::http_api::{test_utils::ApiTester, Config as HttpConfig}; const SRC_VC_TOKEN_FILE_NAME: &str = "src_vc_token.json"; const DEST_VC_TOKEN_FILE_NAME: &str = "dest_vc_token.json"; @@ -645,12 +646,15 @@ mod test { struct TestBuilder { src_import_builder: Option, dest_import_builder: Option, + http_config: HttpConfig, duplicates: usize, dir: TempDir, move_back_again: bool, remove_passwords_from_src_vc: bool, mutate_passwords: Option, passwords: HashMap>, + use_password_files: bool, + reuse_password_files: Option, } impl TestBuilder { @@ -659,12 +663,15 @@ mod test { Self { src_import_builder: None, dest_import_builder: None, + http_config: ApiTester::default_http_config(), duplicates: 0, dir, move_back_again: false, remove_passwords_from_src_vc: false, mutate_passwords: None, passwords: <_>::default(), + use_password_files: false, + reuse_password_files: None, } } @@ -673,8 +680,19 @@ mod test { self } + fn use_password_files(mut self) -> Self { + self.use_password_files = true; + self.http_config.store_passwords_in_secrets_dir = true; + self + } + + fn reuse_password_files(mut self, index: usize) -> Self { + self.reuse_password_files = Some(index); + self + } + async fn with_src_validators(mut self, count: u32, first_index: u32) -> Self { - let builder = ImportTestBuilder::new() + let builder = ImportTestBuilder::new_with_http_config(self.http_config.clone()) .await .create_validators(count, first_index) .await; @@ -683,7 +701,7 @@ mod test { } async fn with_dest_validators(mut self, count: u32, first_index: u32) -> Self { - let builder = ImportTestBuilder::new() + let builder = ImportTestBuilder::new_with_http_config(self.http_config.clone()) .await .create_validators(count, first_index) .await; @@ -776,6 +794,14 @@ mod test { assert!( dest_vc_final_keystores.contains(initial_keystore), "the source keystore should be present at the dest" + ); + assert!( + !src_vc + .secrets_dir + .path() + .join(format!("{:?}", initial_keystore.validating_pubkey)) + .exists(), + "the source password file should be deleted" ) } } @@ -800,6 +826,14 @@ mod test { assert!( dest_vc_final_keystores.contains(moved_keystore), "the moved keystore should be present at the dest" + ); + assert!( + !src_vc + .secrets_dir + .path() + .join(format!("{:?}", moved_keystore.validating_pubkey)) + .exists(), + "the source password file should be deleted" ) } } @@ -830,9 +864,49 @@ mod test { dest_vc_final_keystores.contains(initial_keystore), "the keystore should be present at the dest" ); + if self.reuse_password_files.is_some() { + assert!( + src_vc + .secrets_dir + .path() + .join(format!("{:?}", pubkey)) + .exists(), + "the source password file was used by another validator and should not be deleted" + ) + } else { + assert!( + !src_vc + .secrets_dir + .path() + .join(format!("{:?}", pubkey)) + .exists(), + "the source password file should be deleted" + ) + } } } } + + // If enabled, check that all VCs still have the password files for their validators. + if self.use_password_files { + src_vc_final_keystores + .iter() + .map(|keystore| (&src_vc, keystore)) + .chain( + dest_vc_final_keystores + .iter() + .map(|keystore| (&dest_vc, keystore)), + ) + .for_each(|(vc, keystore)| { + assert!( + vc.secrets_dir + .path() + .join(format!("{:?}", keystore.validating_pubkey)) + .exists(), + "the password file should exist" + ) + }); + } } result @@ -847,15 +921,45 @@ mod test { assert!(import_test_result.result.is_ok()); import_test_result.vc } else { - ApiTester::new().await + ApiTester::new_with_http_config(self.http_config.clone()).await }; + // If enabled, set all the validator definitions on the src_vc to + // use the same password path as the given `master_index`. This + // helps test that we don't delete a password file if it's in use by + // another validator. + if let Some(primary_index) = self.reuse_password_files { + let mut initialized_validators = src_vc.initialized_validators.write(); + let definitions = initialized_validators.as_mut_slice_testing_only(); + // Find the path of the "primary" definition. + let primary_path = definitions + .get(primary_index) + .map(|def| match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => path.clone(), + _ => panic!("primary index does not have password path"), + }) + .unwrap(); + // Set all definitions to use the same password path as the primary. + definitions.iter_mut().enumerate().for_each(|(_, def)| { + match &mut def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_password_path: Some(path), + .. + } => *path = primary_path.clone(), + _ => (), + } + }) + } + let dest_vc = if let Some(import_builder) = self.dest_import_builder.take() { let import_test_result = import_builder.run_test().await; assert!(import_test_result.result.is_ok()); import_test_result.vc } else { - ApiTester::new().await + ApiTester::new_with_http_config(self.http_config.clone()).await }; if self.remove_passwords_from_src_vc { @@ -1125,4 +1229,31 @@ mod test { .await .assert_ok(); } + + #[tokio::test] + async fn one_validator_move_all_with_password_files() { + TestBuilder::new() + .await + .use_password_files() + .with_src_validators(1, 0) + .await + .run_test(|_| Validators::All) + .await + .assert_ok(); + } + + #[tokio::test] + async fn two_validators_move_one_with_identical_password_files() { + TestBuilder::new() + .await + .use_password_files() + // The password file for validator 0 will be shared with other + // validators on the src vc. + .reuse_password_files(0) + .with_src_validators(2, 0) + .await + .run_test(|validators| Validators::Specific(validators[0..1].to_vec())) + .await + .assert_ok(); + } } From 1ec051c0028a96965ca0b838f4e51a4dad469253 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 11:25:43 +1000 Subject: [PATCH 107/138] Update docs --- book/src/validator-manager-move.md | 78 +++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 67cd4b44aa1..2abe4cf1ed8 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -1,7 +1,47 @@ # Moving Validators -This document describes the steps to move validators between two validator clients (VCs) which are -able to SSH between each other. This guides assumes experience with the Linux command line and SSH +The `lighthouse validator-manager move` command uses the VC HTTP API to move +validators from one VC (the "src" VC) to another VC (the "dest" VC). The move +operation is *comprehensive*; it will: + +- Disable the validators the src VC. +- Remove the validator keystores from the src VC file system. +- Export the slashing database records for the appropriate validators from the src VC to the dest VC. +- Enable the validators on the dest VC. +- Generally result in very little or no validator downtime. + +It is capable of moving all validators on the src VC, a number of validators or +specific validators from one VC to another. + +The `move` command is only guaranteed to work between two Lighthouse VCs (i.e., +there is no guarantee that the commands will work between Lighthouse and Teku or +another client). + +The `move` command only supports moving validators using a keystore on the local +file system, it does not support `Web3Signer` validators. + +Although all efforts are taken to avoid it, it's possible for the `move` command +to fail in a way removes the validator from the src VC without adding it to the +dest VC. Therefore, it is recommended to **never use the `move` command without +having a backup of all validator keystores (e.g., the mnemonic).** + +## Simple Example + +``` +lighthouse \ + validator-manager \ + move \ + --src-vc-url http://localhost:6062 \ + --src-vc-token ~/src-token.txt \ + --dest-vc-url http://localhost:5062 \ + --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ + --validators all \ +``` + +## Guide + +This guide describes the steps to move validators between two validator clients (VCs) which are +able to SSH between each other. This guide assumes experience with the Linux command line and SSH connections. There will be two VCs in this example: @@ -9,8 +49,11 @@ There will be two VCs in this example: - The *source* VC which contains the validators/keystores to be moved. - The *destination* VC which is to take the validators/keystores from the source. -This example will assume the source VC is accessible at `src-host` and the destination VC is -accessible at `dest-host`. Replace these values with your own. +The example will assume the source VC is accessible at `src-host` and the destination VC is +accessible at `dest-host`. Replace these values with your own hostnames or IP addresses. + +The example assumes that the reader is currently logged into `dest-host` via SSH +and that the reader can SSH from `dest-host` to `src-host`. ### 1. Configure the Source VC @@ -38,6 +81,14 @@ lighthouse \ The destination VC needs to have the following flags: +- `--http` +- `--unencrypted-http-transport` +- `--http-address 127.0.0.1` +- `--http-port 5062` +- `--enable-doppelganger-protection` + +Therefore, the destination VC command might look like: + ```bash lighthouse \ vc \ @@ -45,8 +96,14 @@ lighthouse \ --unencrypted-http-transport \ --http-address 127.0.0.1 \ --http-port 5062 \ + --enable-doppelganger-protection ``` +The `--enable-doppelganger-protection` flag is not *strictly* required, however +it is recommended for an additional layer of safety. It will result in 3-4 +epochs of downtime for the validator after it is moved, which is generally an +inconsequential cost in lost rewards or penalties. + Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have the import validator keystore passwords stored in separate files rather than in the `valdiator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. @@ -73,7 +130,6 @@ alongside validator keystores. For example: `~/.lighthouse/mainnet/validators/ap Copy the contents of that file into a new file on the **destination host** at `~/src-token.txt`. The API token should be similar to `api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123`. - ### 4. Create an SSH Tunnel In one terminal window, SSH to the **destination host** and establish a reverse-SSH connection @@ -103,4 +159,14 @@ lighthouse \ --validators all \ ``` -TODO +The command will provide information about the progress of the operation and +emit `Done.` when the operation has completed successfully. + +Once the operation completes successfully, there is nothing else to be done. The +validators have been removed from the `src-host` and enabled at the `dest-host`. +If the `--enable-doppelganger-protection` flag was used it may take 3-4 epochs +for the validators to start attesting and producing blocks on the `dest-host`. + +Any errors encounted during the operation should include information on how to +proceed. Assistance is also available on our +[Discord](https://discord.gg/cyAszAh). \ No newline at end of file From 31dc52061ba2c801ee9778843c42ed5ab8f0e41c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 12:14:48 +1000 Subject: [PATCH 108/138] Avoid using BLS credentials --- validator_manager/src/create_validators.rs | 58 ++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index e83a50d54e6..cbd61886f42 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -26,6 +26,7 @@ pub const GAS_LIMIT_FLAG: &str = "gas-limit"; pub const FEE_RECIPIENT_FLAG: &str = "suggested-fee-recipient"; pub const BUILDER_PROPOSALS_FLAG: &str = "builder-proposals"; pub const BEACON_NODE_FLAG: &str = "beacon-node"; +pub const FORCE_BLS_WITHDRAWAL_CREDENTIALS: &str = "force-bls-withdrawal-credentials"; pub const VALIDATORS_FILENAME: &str = "validators.json"; pub const DEPOSITS_FILENAME: &str = "deposits.json"; @@ -173,6 +174,15 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .takes_value(true), ) + .arg( + Arg::with_name(FORCE_BLS_WITHDRAWAL_CREDENTIALS) + .takes_value(false) + .long(FORCE_BLS_WITHDRAWAL_CREDENTIALS) + .help( + "If present, allows BLS withdrawal credentials rather than an execution \ + address. This is not recommended.", + ), + ) } /// The CLI arguments are parsed into this struct before running the application. This step of @@ -192,6 +202,7 @@ pub struct CreateConfig { pub fee_recipient: Option
, pub gas_limit: Option, pub bn_url: Option, + pub force_bls_withdrawal_credentials: bool, } impl CreateConfig { @@ -215,6 +226,7 @@ impl CreateConfig { fee_recipient: clap_utils::parse_optional(matches, FEE_RECIPIENT_FLAG)?, gas_limit: clap_utils::parse_optional(matches, GAS_LIMIT_FLAG)?, bn_url: clap_utils::parse_optional(matches, BEACON_NODE_FLAG)?, + force_bls_withdrawal_credentials: matches.is_present(FORCE_BLS_WITHDRAWAL_CREDENTIALS), }) } } @@ -241,8 +253,17 @@ impl ValidatorsAndDeposits { fee_recipient, gas_limit, bn_url, + force_bls_withdrawal_credentials, } = config; + // Since Capella, it really doesn't make much sense to use BLS + // withdrawal credentials. Try to guide users away from doing so. + if eth1_withdrawal_address.is_none() && !force_bls_withdrawal_credentials { + return Err(format!( + "--{ETH1_WITHDRAWAL_ADDRESS_FLAG} is required. See --help for more information." + )); + } + if count == 0 { return Err(format!("--{} cannot be 0", COUNT_FLAG)); } @@ -524,6 +545,10 @@ pub mod tests { const TEST_VECTOR_DEPOSIT_CLI_VERSION: &str = "2.3.0"; + fn junk_execution_address() -> Option
{ + Some(Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap()) + } + pub struct TestBuilder { spec: ChainSpec, output_dir: TempDir, @@ -557,11 +582,12 @@ pub mod tests { stdin_inputs: false, disable_deposits: false, specify_voting_keystore_password: false, - eth1_withdrawal_address: None, + eth1_withdrawal_address: junk_execution_address(), builder_proposals: None, fee_recipient: None, gas_limit: None, bn_url: None, + force_bls_withdrawal_credentials: false, }; Self { @@ -709,6 +735,30 @@ pub mod tests { TestBuilder::default().run_test().await.assert_ok(); } + #[tokio::test] + async fn no_eth1_address_without_force() { + TestBuilder::default() + .mutate_config(|config| { + config.eth1_withdrawal_address = None; + config.force_bls_withdrawal_credentials = false + }) + .run_test() + .await + .assert_err(); + } + + #[tokio::test] + async fn bls_withdrawal_credentials() { + TestBuilder::default() + .mutate_config(|config| { + config.eth1_withdrawal_address = None; + config.force_bls_withdrawal_credentials = true + }) + .run_test() + .await + .assert_ok(); + } + #[tokio::test] async fn default_test_values_deposits_disabled() { TestBuilder::default() @@ -732,8 +782,7 @@ pub mod tests { TestBuilder::default() .mutate_config(|config| { config.count = 2; - config.eth1_withdrawal_address = - Some(Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap()); + config.eth1_withdrawal_address = junk_execution_address(); }) .run_test() .await @@ -822,6 +871,9 @@ pub mod tests { config.eth1_withdrawal_address = Some( Address::from_str("0x0f51bb10119727a7e5ea3538074fb341f56b09ad").unwrap(), ); + } else { + config.eth1_withdrawal_address = None; + config.force_bls_withdrawal_credentials = true; } }) .run_test() From c3fe9d7fec38a5de0553fa3d05ec7893c065d52e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 12:48:57 +1000 Subject: [PATCH 109/138] Add more docs --- book/src/validator-manager-create.md | 170 +++++++++++++++++++++++++++ book/src/validator-manager-move.md | 9 +- book/src/validator-manager.md | 23 +--- 3 files changed, 181 insertions(+), 21 deletions(-) create mode 100644 book/src/validator-manager-create.md diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md new file mode 100644 index 00000000000..cf06dad7368 --- /dev/null +++ b/book/src/validator-manager-create.md @@ -0,0 +1,170 @@ +# Creating and Importing Validators + +[Ethereum Staking Launchpad]: https://launchpad.ethereum.org/en/ + +The `lighthouse validator-manager create` command derives validators from a +mnemonic and produces two files: + +- `validators.json`: the keystores and passwords for the newly generated + validators in JSON format. +- `deposits.json`: a JSON file of the same format as + [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) which can + be used for deposit submission via the [Ethereum Staking + Launchpad][]. + +The `lighthouse validator-manager import` command accepts a `validators.json` +file (from the `create` command) and submits those validators to a running +Lighthouse Validator Client via the HTTP API. + +These two commands enable a workflow of: + +1. Creating the validators via the `create` command. +1. Importing the validators via the `import` command. +1. Depositing validators via the [Ethereum Staking + Launchpad][]. + +The separation of the `create` and `import` commands allows for running the +`create` command on an air-gapped host whilst performing the `import` command on +an internet-connected host. + +The `create` and `import` commands are recommended for advanced users who are +familiar with command line tools and the practicalities of managing sensitive +key information. **We recommend that novice users follow the workflow on +[Ethereum Staking Launchpad][] rather than using the `move` and `create` +tools.** + +## Simple Example + +Create validators from a mnemonic with: + +```bash +lighthouse \ + validator-manager \ + create \ + --first-index 0 \ + --count 2 \ + --eth1-withdrawal-address
\ + --output-path ./ +``` + +Import the validators to a running VC with: + +```bash +lighthouse \ + validator-manager \ + import \ + --validators-file validators.json \ + --vc-token +``` + +Be sure to remove `./validators.json` after the import is successful since it +contains unencrypted validator keystores. + +## Detailed Guide + +This guide will create two validators and import them to a VC. For simplicity, +the same host will be used to generate the keys and run the VC. In reality, +users may want to perform the `import` command on an air-gapped machine and then +move the `validators.json` and `deposits.json` files to an Internet-connected +host. This would help protect the mnemonic from being exposed to the Internet. + +### 1. Create the Validators + +Run the `create` command, subsituting `
` for an execution address that +you control. This is where all the staked ETH and rewards will ultimately +reside, so it's very important that this address is secure, acessible and +backed-up. + +```bash +lighthouse \ + validator-manager \ + create \ + --first-index 0 \ + --count 2 \ + --eth1-withdrawal-address
\ + --output-path ./ +``` + +If successful, the command output will appear like below: + +```bash +Running validator manager for mainnet network + +Enter the mnemonic phrase: + +Valid mnemonic provided. + +Starting derivation of 2 keystores. Each keystore may take several seconds. +Completed 1/2: 0x8885c29b8f88ee9b9a37b480fd4384fed74bda33d85bc8171a904847e65688b6c9bb4362d6597fd30109fb2def6c3ae4 +Completed 2/2: 0xa262dae3dcd2b2e280af534effa16bedb27c06f2959e114d53bd2a248ca324a018dc73179899a066149471a94a1bc92f +Keystore generation complete +Writing "./validators.json" +Writing "./deposits.json" +``` + +This command will create validators at indices `0, 1`. The exact indices created +can be influenced with the `--first-index` and `--count` flags. Use these flags +with caution to prevent creating the same validator twice, this may result in a +slashing! + +The command will create two files: + +- `./deposits.json`: this file does not contain sensitive information and may be uploaded to the [Ethereum Staking Launchpad]. +- `./validators.json`: this file contains **sensitive unencrypted validator keys, do not share it with anyone or upload it to any website**. + +### 2. Import the validators + +The VC which will receive the validators needs to have the following flags at a minimum: + +- `--http` +- `--unencrypted-http-transport` +- `--http-address 127.0.0.1` +- `--http-port 5062` +- `--enable-doppelganger-protection` + +Therefore, the VC command might look like: + +```bash +lighthouse \ + vc \ + --http \ + --unencrypted-http-transport \ + --http-address 127.0.0.1 \ + --http-port 5062 \ + --enable-doppelganger-protection +``` + +In order to import the validators, the location of the VC `api-token.txt` file +must be known. The location of the file varies, but it is located in the +"validator directory" of your data directory. For example: +`~/.lighthouse/mainnet/validators/api-token.txt`. We will use `` +to subsitute this value. + + +Once the VC is running, use the `import` command to import the validators to the VC: + +```bash +lighthouse \ + validator-manager \ + import \ + --validators-file validators.json \ + --vc-token +``` + +If successful, the command output will appear like below: + +```bash +Running validator manager for mainnet network +Validator client is reachable at http://localhost:5062/ and reports 0 validators +Starting to submit validators 2 to VC, each validator may take several seconds +Uploaded keystore 1 of 2 to the VC +Uploaded keystore 2 of 2 to the VC +``` + +The user should now *securely* delete the `validators.json` file (e.g., `shred -u validators.json`). +The `validators.json` contains the unencrypted validator keys and must not be +shared with anyone. + +The validators will now go through 3-4 epochs of [doppelganger +protection](./validator-doppelganger.md) and will automatically start performing +their duties when they are deposited and activated. \ No newline at end of file diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 2abe4cf1ed8..b3ce6298201 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -27,6 +27,9 @@ having a backup of all validator keystores (e.g., the mnemonic).** ## Simple Example +The following command will move all validators from the VC running at +`http://localhost:6062` to the VC running at `http://localhost:5062`. + ``` lighthouse \ validator-manager \ @@ -38,7 +41,7 @@ lighthouse \ --validators all \ ``` -## Guide +## Detailed Guide This guide describes the steps to move validators between two validator clients (VCs) which are able to SSH between each other. This guide assumes experience with the Linux command line and SSH @@ -57,7 +60,7 @@ and that the reader can SSH from `dest-host` to `src-host`. ### 1. Configure the Source VC -The source VC needs to have the following flags: +The source VC needs to have the following flags at a mininum: - `--http` - `--unencrypted-http-transport` @@ -79,7 +82,7 @@ lighthouse \ ### 2. Configure the Destination VC -The destination VC needs to have the following flags: +The destination VC needs to have the following flags at a mininum: - `--http` - `--unencrypted-http-transport` diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index 1b8b1f1a038..739a8132af5 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -1,6 +1,6 @@ # Validator Manager -[Ethereum Launchpad]: https://launchpad.ethereum.org/en/ +[Ethereum Staking Launchpad]: https://launchpad.ethereum.org/en/ [Import Validators]: #import-validators ## Introduction @@ -16,25 +16,12 @@ creates files that will be read by the VC next time it starts rather than making a live VC. The validator manager is generally superior to the account manager for the following (non-exhaustive) reasons: -- The validator manager generates deposit files compatible with the [Ethereum Launchpad](). +- The validator manager generates deposit files compatible with the [Ethereum Staking Launchpad](). - Changes made with the validator manager do not require downtime for the VC. - The "key cache" is preserved whenever a validator is added with the validator manager, this prevents long waits at start up when a new validator is added. -## Commands +### Validator Manager Documentation -### Create Validators - -The `lighthouse validator-manager create` command accepts a mnemonic and produces a JSON -file containing validator keystores that can be imported with the [Import -Validators]() command. - -For users that want to add validators to a VC from an existing mnemonic, this is the first half the -process which generates a *validator specifications* JSON file for the new validators. The second -half of the process is to upload those validator specifications to a VC (see [Import Validators](). -The command is split into two steps so that security-conscious validators can generate the validator -specifications on an "air-gapped" computer which is not connected to the Internet. Then, the -validator specifications file can be transferred to a VC for import. This means that the VC never -has access to the mnemonic, just the keystores with which it is concerned. - -### Import Validators +- [Creating and importing validators using the `create` and `import` commands](./validator-manager-create.md) +- [Moving validators between two VCs using the `move` command](./validator-manager.md) \ No newline at end of file From b11ce559a94798527ffee34645a51a3ce331341b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 12:51:39 +1000 Subject: [PATCH 110/138] Update error for missing fee recip --- validator_client/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index f0532f8a6fc..f7a80f0a8e7 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -172,9 +172,12 @@ impl ProductionValidatorClient { let new_validators = validator_defs .discover_local_keystores(&config.validator_dir, &config.secrets_dir, &log) .map_err(|e| format!("Unable to discover local validator keystores: {:?}", e))?; - validator_defs - .save(&config.validator_dir) - .map_err(|e| format!("Unable to update validator definitions: {:?}", e))?; + validator_defs.save(&config.validator_dir).map_err(|e| { + format!( + "Provide --suggested-fee-recipient or update validator definitions: {:?}", + e + ) + })?; info!( log, "Completed validator discovery"; From e2a2e8936c9ebd21bbb61a3404bd3e4338a923e5 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 12:52:58 +1000 Subject: [PATCH 111/138] Add suggested fee recip --- book/src/validator-manager-create.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index cf06dad7368..0cb85b90f3f 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -44,9 +44,14 @@ lighthouse \ --first-index 0 \ --count 2 \ --eth1-withdrawal-address
\ + --suggested-fee-recipient
\ --output-path ./ ``` +The `--suggested-fee-recipient` flag may be omitted to use whatever default +value the VC uses. It does not necessarily need to be idential to +`--eth1-withdrawal-address`. + Import the validators to a running VC with: ```bash From 2341f03556cc914e1993d30aa4de8d28a1a7a12d Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 13:03:54 +1000 Subject: [PATCH 112/138] Update docs more --- book/src/validator-manager.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index 739a8132af5..2c087527000 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -13,11 +13,19 @@ supported by this command. The validator manager tool is similar to the `lighthouse account-manager` tool, except the latter creates files that will be read by the VC next time it starts rather than making instant changes to -a live VC. The validator manager is generally superior to the account manager for the following -(non-exhaustive) reasons: +a live VC. -- The validator manager generates deposit files compatible with the [Ethereum Staking Launchpad](). -- Changes made with the validator manager do not require downtime for the VC. +The `account-manager` is ideal for importing keys created with the +[staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli). On the +other hand, the `validator-manager` is ideal for moving existing validators +between two VCs or for advanced users to create validators at scale with less +downtime. + +The `validator-manager` boasts the following features: + +- One-line command to arbitrarily move validators between two VCs, maintaining the slashing protection database. +- Generates deposit files compatible with the [Ethereum Staking Launchpad](). +- Generally involves zero or very little downtime. - The "key cache" is preserved whenever a validator is added with the validator manager, this prevents long waits at start up when a new validator is added. From b5027a00f4165c137295a8edba36d2ab06ae7306 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 13:06:06 +1000 Subject: [PATCH 113/138] Add docs to summary --- book/src/SUMMARY.md | 1 + book/src/validator-management.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 7431d223871..d1ece09e4e1 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -12,6 +12,7 @@ * [Run a Node](./run_a_node.md) * [Become a Validator](./mainnet-validator.md) * [Validator Management](./validator-management.md) + * [The `validator-manager` Command](./validator-manager.md) * [Slashing Protection](./slashing-protection.md) * [Voluntary Exits](./voluntary-exit.md) * [Partial Withdrawals](./partial-withdrawal.md) diff --git a/book/src/validator-management.md b/book/src/validator-management.md index be34fef2c3c..df7c2ac4760 100644 --- a/book/src/validator-management.md +++ b/book/src/validator-management.md @@ -13,6 +13,10 @@ standard directories and do not start their `lighthouse vc` with the this document. However, users with more complex needs may find this document useful. +The [lighthouse validator-manager](./validator-manager.md) command can be used +to create and import validators to a Lighthouse VC. It can also be used to move +validators between two Lighthouse VCs. + ## Introducing the `validator_definitions.yml` file The `validator_definitions.yml` file is located in the `validator-dir`, which From 392624be0f096e7885b3bb9792df4e44bb74af1e Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 15:44:25 +1000 Subject: [PATCH 114/138] Tidy docs --- book/src/validator-manager-create.md | 26 +++++----- book/src/validator-manager-move.md | 72 +++++++++++++++------------- book/src/validator-manager.md | 18 +++---- 3 files changed, 61 insertions(+), 55 deletions(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index 0cb85b90f3f..0bdb078b248 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -6,7 +6,7 @@ The `lighthouse validator-manager create` command derives validators from a mnemonic and produces two files: - `validators.json`: the keystores and passwords for the newly generated - validators in JSON format. + validators, in JSON format. - `deposits.json`: a JSON file of the same format as [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) which can be used for deposit submission via the [Ethereum Staking @@ -29,9 +29,9 @@ an internet-connected host. The `create` and `import` commands are recommended for advanced users who are familiar with command line tools and the practicalities of managing sensitive -key information. **We recommend that novice users follow the workflow on -[Ethereum Staking Launchpad][] rather than using the `move` and `create` -tools.** +cryptographic material. **We recommend that novice users follow the workflow on +[Ethereum Staking Launchpad][] rather than using the `create` and `import` +commands.** ## Simple Example @@ -48,11 +48,11 @@ lighthouse \ --output-path ./ ``` -The `--suggested-fee-recipient` flag may be omitted to use whatever default -value the VC uses. It does not necessarily need to be idential to -`--eth1-withdrawal-address`. +> The `--suggested-fee-recipient` flag may be omitted to use whatever default +> value the VC uses. It does not necessarily need to be idential to +> `--eth1-withdrawal-address`. -Import the validators to a running VC with: +Then, import the validators to a running VC with: ```bash lighthouse \ @@ -62,8 +62,8 @@ lighthouse \ --vc-token ``` -Be sure to remove `./validators.json` after the import is successful since it -contains unencrypted validator keystores. +> Be sure to remove `./validators.json` after the import is successful since it +> contains unencrypted validator keystores. ## Detailed Guide @@ -78,7 +78,7 @@ host. This would help protect the mnemonic from being exposed to the Internet. Run the `create` command, subsituting `
` for an execution address that you control. This is where all the staked ETH and rewards will ultimately reside, so it's very important that this address is secure, acessible and -backed-up. +backed-up. The `create` command: ```bash lighthouse \ @@ -114,7 +114,7 @@ slashing! The command will create two files: -- `./deposits.json`: this file does not contain sensitive information and may be uploaded to the [Ethereum Staking Launchpad]. +- `./deposits.json`: this file does *not* contain sensitive information and may be uploaded to the [Ethereum Staking Launchpad]. - `./validators.json`: this file contains **sensitive unencrypted validator keys, do not share it with anyone or upload it to any website**. ### 2. Import the validators @@ -172,4 +172,4 @@ shared with anyone. The validators will now go through 3-4 epochs of [doppelganger protection](./validator-doppelganger.md) and will automatically start performing -their duties when they are deposited and activated. \ No newline at end of file +their duties when they are deposited and activated. The guide is complete. \ No newline at end of file diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index b3ce6298201..7c1f5728e4f 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -10,27 +10,26 @@ operation is *comprehensive*; it will: - Enable the validators on the dest VC. - Generally result in very little or no validator downtime. -It is capable of moving all validators on the src VC, a number of validators or -specific validators from one VC to another. +It is capable of moving all validators on the src VC, a count of validators or +a list of pubkeys. The `move` command is only guaranteed to work between two Lighthouse VCs (i.e., -there is no guarantee that the commands will work between Lighthouse and Teku or -another client). +there is no guarantee that the commands will work between Lighthouse and Teku, for instance). The `move` command only supports moving validators using a keystore on the local file system, it does not support `Web3Signer` validators. Although all efforts are taken to avoid it, it's possible for the `move` command -to fail in a way removes the validator from the src VC without adding it to the +to fail in a way that removes the validator from the src VC without adding it to the dest VC. Therefore, it is recommended to **never use the `move` command without -having a backup of all validator keystores (e.g., the mnemonic).** +having a backup of all validator keystores (e.g. the mnemonic).** ## Simple Example The following command will move all validators from the VC running at `http://localhost:6062` to the VC running at `http://localhost:5062`. -``` +```bash lighthouse \ validator-manager \ move \ @@ -38,7 +37,7 @@ lighthouse \ --src-vc-token ~/src-token.txt \ --dest-vc-url http://localhost:5062 \ --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ - --validators all \ + --validators all ``` ## Detailed Guide @@ -52,11 +51,18 @@ There will be two VCs in this example: - The *source* VC which contains the validators/keystores to be moved. - The *destination* VC which is to take the validators/keystores from the source. -The example will assume the source VC is accessible at `src-host` and the destination VC is -accessible at `dest-host`. Replace these values with your own hostnames or IP addresses. +There will be two hosts in this example: + +- Host 1 (*"source host"*): Is running the `src-vc`. +- Host 2 (*"destination host"*): Is running the `dest-vc`. + +The example will assume that all commands are run on Host 1. It also assumes +that Host 1 is able to SSH to Host 2. -The example assumes that the reader is currently logged into `dest-host` via SSH -and that the reader can SSH from `dest-host` to `src-host`. +In reality, many host configurations are possible. For example: + +- Both VCs on the same host. +- Both VCs on different hosts and the `validator-manager` being used on a third host. ### 1. Configure the Source VC @@ -102,25 +108,16 @@ lighthouse \ --enable-doppelganger-protection ``` -The `--enable-doppelganger-protection` flag is not *strictly* required, however -it is recommended for an additional layer of safety. It will result in 3-4 -epochs of downtime for the validator after it is moved, which is generally an -inconsequential cost in lost rewards or penalties. - -Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have -the import validator keystore passwords stored in separate files rather than in the -`valdiator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. - -### 3. Configure SSH +> The `--enable-doppelganger-protection` flag is not *strictly* required, however +> it is recommended for an additional layer of safety. It will result in 3-4 +> epochs of downtime for the validator after it is moved, which is generally an +> inconsequential cost in lost rewards or penalties. +> +> Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have +> the import validator keystore passwords stored in separate files rather than in the +> `valdiator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. -For this example to work, the `dest-host` host must be able to SSH to the `src-host` host. This -configuration is out-of-scope of this article, however it probably involves adding a public key to -the `.ssh/authorized_keys` file on the `dest-host` host. - -You will know this is complete when you can SSH to the `dest-host` from your PC and then run `ssh -src-host` successfully. - -### 4. Obtain the Source API Token +### 3. Obtain the Source API Token The VC API is protected by an *API token*. This is stored in a file on each of the hosts. Since we'll be running our command on the destination host, it will need to have the API token for the @@ -151,7 +148,7 @@ this terminal window then the connection between the destination and source host With the SSH tunnel established between the `dest-host` and `src-host`, from the **destination host** run the command to move the validators: -``` +```bash lighthouse \ validator-manager \ move \ @@ -159,11 +156,20 @@ lighthouse \ --src-vc-token ~/src-token.txt \ --dest-vc-url http://localhost:5062 \ --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ - --validators all \ + --validators all ``` The command will provide information about the progress of the operation and -emit `Done.` when the operation has completed successfully. +emit `Done.` when the operation has completed successfully. For example: + +```bash +Running validator manager for mainnet network +Validator client is reachable at http://localhost:5062/ and reports 2 validators +Validator client is reachable at http://localhost:6062/ and reports 0 validators +Moved keystore 1 of 2 +Moved keystore 2 of 2 +Done. +``` Once the operation completes successfully, there is nothing else to be done. The validators have been removed from the `src-host` and enabled at the `dest-host`. diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index 2c087527000..52243a31b9b 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -5,15 +5,15 @@ ## Introduction -The `lighthouse validator-manager` tool provides utilities for managing validators on a running +The `lighthouse validator-manager` tool provides utilities for managing validators on a *running* Lighthouse Validator Client. The validator manager performs operations via the HTTP API of the validator client (VC). Due to limitations of the [keymanager-APIs](https://ethereum.github.io/keymanager-APIs/), only Lighthouse VCs are fully supported by this command. -The validator manager tool is similar to the `lighthouse account-manager` tool, except the latter -creates files that will be read by the VC next time it starts rather than making instant changes to -a live VC. +The validator manager tool is similar to the `lighthouse account-manager` tool, +except the latter creates files that will be read by the VC next time it starts +whilst the former makers instant changes to a live VC. The `account-manager` is ideal for importing keys created with the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli). On the @@ -26,10 +26,10 @@ The `validator-manager` boasts the following features: - One-line command to arbitrarily move validators between two VCs, maintaining the slashing protection database. - Generates deposit files compatible with the [Ethereum Staking Launchpad](). - Generally involves zero or very little downtime. -- The "key cache" is preserved whenever a validator is added with the validator manager, this - prevents long waits at start up when a new validator is added. +- The "key cache" is preserved whenever a validator is added with the validator + manager, preventing long waits at start up when a new validator is added. -### Validator Manager Documentation +## Guides -- [Creating and importing validators using the `create` and `import` commands](./validator-manager-create.md) -- [Moving validators between two VCs using the `move` command](./validator-manager.md) \ No newline at end of file +- [Creating and importing validators using the `create` and `import` commands.](./validator-manager-create.md) +- [Moving validators between two VCs using the `move` command.](./validator-manager-move.md) \ No newline at end of file From aa5477735352e62b5d6da4f96f0120a4e6d91905 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 15:55:26 +1000 Subject: [PATCH 115/138] Add new pages to summary --- book/src/SUMMARY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index d1ece09e4e1..e0a711f1092 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -13,6 +13,8 @@ * [Become a Validator](./mainnet-validator.md) * [Validator Management](./validator-management.md) * [The `validator-manager` Command](./validator-manager.md) + * [Creating validators](./validator-manager-create.md) + * [Moving validators](./validator-manager-move.md) * [Slashing Protection](./slashing-protection.md) * [Voluntary Exits](./voluntary-exit.md) * [Partial Withdrawals](./partial-withdrawal.md) From 6c9bea86eca47de45e289964094e71b201add012 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 13 Jul 2023 16:50:46 +1000 Subject: [PATCH 116/138] Fix failing test --- lighthouse/tests/validator_manager.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lighthouse/tests/validator_manager.rs b/lighthouse/tests/validator_manager.rs index df6a77bbea2..e0a1e92d6ae 100644 --- a/lighthouse/tests/validator_manager.rs +++ b/lighthouse/tests/validator_manager.rs @@ -125,6 +125,7 @@ pub fn validator_create_defaults() { fee_recipient: None, gas_limit: None, bn_url: None, + force_bls_withdrawal_credentials: false, }; assert_eq!(expected, config); }); @@ -145,6 +146,7 @@ pub fn validator_create_misc_flags() { .flag("--suggested-fee-recipient", Some(EXAMPLE_ETH1_ADDRESS)) .flag("--gas-limit", Some("1337")) .flag("--beacon-node", Some("http://localhost:1001")) + .flag("--force-bls-withdrawal-credentials", None) .assert_success(|config| { let expected = CreateConfig { output_path: PathBuf::from("./meow"), @@ -160,6 +162,7 @@ pub fn validator_create_misc_flags() { fee_recipient: Some(Address::from_str(EXAMPLE_ETH1_ADDRESS).unwrap()), gas_limit: Some(1337), bn_url: Some(SensitiveUrl::parse("http://localhost:1001").unwrap()), + force_bls_withdrawal_credentials: true, }; assert_eq!(expected, config); }); From 95de58e23ff22a86dedac8b16e5d3d6fe9692540 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jul 2023 09:27:11 +1000 Subject: [PATCH 117/138] De-duplicate some code --- .../src/http_api/create_validator.rs | 37 +++++++++++++------ validator_client/src/http_api/mod.rs | 23 ++++-------- .../src/initialized_validators.rs | 2 +- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/validator_client/src/http_api/create_validator.rs b/validator_client/src/http_api/create_validator.rs index edc0db55232..52336afa59d 100644 --- a/validator_client/src/http_api/create_validator.rs +++ b/validator_client/src/http_api/create_validator.rs @@ -1,6 +1,7 @@ use crate::ValidatorStore; use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use account_utils::{ + eth2_keystore::Keystore, eth2_wallet::{bip39::Mnemonic, WalletBuilder}, random_mnemonic, random_password, ZeroizeString, }; @@ -96,17 +97,8 @@ pub async fn create_validators_mnemonic, T: 'static + SlotClock, )) })?; - let voting_password_storage = if let Some(secrets_dir) = &secrets_dir { - let password_path = keystore_password_path(secrets_dir, &keystores.voting); - if password_path.exists() { - return Err(warp_utils::reject::custom_server_error( - "Duplicate keystore password path".to_string(), - )); - } - PasswordStorage::File(password_path) - } else { - PasswordStorage::ValidatorDefinitions(voting_password_string.clone()) - }; + let voting_password_storage = + get_voting_password_storage(&secrets_dir, &keystores.voting, &voting_password_string)?; let validator_dir = ValidatorDirBuilder::new(validator_dir.as_ref().into()) .password_dir_opt(secrets_dir.clone()) @@ -199,3 +191,26 @@ pub async fn create_validators_web3signer( Ok(()) } + +/// Attempts to return a `PasswordStorage::File` if `secrets_dir` is defined. +/// Otherwise, returns a `PasswordStorage::ValidatorDefinitions`. +pub fn get_voting_password_storage( + secrets_dir: &Option, + voting_keystore: &Keystore, + voting_password_string: &ZeroizeString, +) -> Result { + if let Some(secrets_dir) = &secrets_dir { + let password_path = keystore_password_path(secrets_dir, voting_keystore); + if password_path.exists() { + Err(warp_utils::reject::custom_server_error( + "Duplicate keystore password path".to_string(), + )) + } else { + Ok(PasswordStorage::File(password_path)) + } + } else { + Ok(PasswordStorage::ValidatorDefinitions( + voting_password_string.clone(), + )) + } +} diff --git a/validator_client/src/http_api/mod.rs b/validator_client/src/http_api/mod.rs index a5ebe270cbe..f654833cbb4 100644 --- a/validator_client/src/http_api/mod.rs +++ b/validator_client/src/http_api/mod.rs @@ -11,12 +11,12 @@ use crate::http_api::create_signed_voluntary_exit::create_signed_voluntary_exit; use crate::{determine_graffiti, GraffitiFile, ValidatorStore}; use account_utils::{ mnemonic_from_phrase, - validator_definitions::{ - PasswordStorage, SigningDefinition, ValidatorDefinition, Web3SignerDefinition, - }, + validator_definitions::{SigningDefinition, ValidatorDefinition, Web3SignerDefinition}, }; pub use api_secret::ApiSecret; -use create_validator::{create_validators_mnemonic, create_validators_web3signer}; +use create_validator::{ + create_validators_mnemonic, create_validators_web3signer, get_voting_password_storage, +}; use eth2::lighthouse_vc::{ std_types::{AuthResponse, GetFeeRecipientResponse, GetGasLimitResponse}, types::{self as api_types, GenericResponse, Graffiti, PublicKey, PublicKeyBytes}, @@ -38,7 +38,7 @@ use system_health::observe_system_health_vc; use task_executor::TaskExecutor; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use types::{ChainSpec, ConfigAndPreset, EthSpec}; -use validator_dir::{keystore_password_path, Builder as ValidatorDirBuilder}; +use validator_dir::Builder as ValidatorDirBuilder; use warp::{ http::{ header::{HeaderValue, CONTENT_TYPE}, @@ -537,17 +537,8 @@ pub fn serve( })?; let secrets_dir = store_passwords_in_secrets_dir.then_some(secrets_dir); - let password_storage = if let Some(secrets_dir) = &secrets_dir { - let password_path = keystore_password_path(secrets_dir, &body.keystore); - if password_path.exists() { - return Err(warp_utils::reject::custom_server_error( - "Duplicate keystore password path".to_string(), - )); - } - PasswordStorage::File(password_path) - } else { - PasswordStorage::ValidatorDefinitions(body.password.clone()) - }; + let password_storage = + get_voting_password_storage(&secrets_dir, &body.keystore, &body.password)?; let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) .password_dir_opt(secrets_dir) diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index b6c535f501f..698920fb5ee 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -663,7 +663,7 @@ impl InitializedValidators { .all(|existing| existing != password_path) { fs::remove_file(&password_path) - .map_err(|e| Error::UnableToDeletePasswordFile(password_path.into(), e))?; + .map_err(|e| Error::UnableToDeletePasswordFile(password_path, e))?; } } From 745f9afd3cd0f9cdfb70bb437bd8a42107680503 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jul 2023 09:40:02 +1000 Subject: [PATCH 118/138] Fix comments --- validator_client/src/initialized_validators.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index 698920fb5ee..f15ea27c9b2 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -653,7 +653,7 @@ impl InitializedValidators { .save(&self.validators_dir) .map_err(Error::UnableToSaveDefinitions)?; - // 4. Delete the keystore password if it's not being used by any definition. + // 5. Delete the keystore password if it's not being used by any definition. if let Some(password_path) = password_path_opt.and_then(|p| p.canonicalize().ok()) { if self .definitions @@ -1273,8 +1273,10 @@ impl InitializedValidators { } } - /// Deletes any passwords store in the validator definitions file and + /// Deletes any passwords stored in the validator definitions file and /// returns a map of pubkey to deleted password. + /// + /// This should only be used for testing, it's rather destructive. pub fn delete_passwords_from_validator_definitions( &mut self, ) -> Result, Error> { From 3d8d3de4132f487229ad9fb0eca26c0edbc9bf25 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jul 2023 17:09:36 +1000 Subject: [PATCH 119/138] Update Cargo.lock --- Cargo.lock | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abe6021d4c3..0e95ae98fbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,7 +409,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -420,7 +420,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -1969,7 +1969,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -3081,7 +3081,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -5761,7 +5761,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -6069,7 +6069,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -6338,9 +6338,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -6570,9 +6570,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" dependencies = [ "proc-macro2", ] @@ -7334,9 +7334,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" dependencies = [ "serde_derive", ] @@ -7363,13 +7363,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -7385,9 +7385,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1b6471d7496b051e03f1958802a73f88b947866f5146f329e47e36554f4e55" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ "itoa", "serde", @@ -7401,7 +7401,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -8130,9 +8130,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" dependencies = [ "proc-macro2", "quote", @@ -8336,7 +8336,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -8499,7 +8499,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -8699,7 +8699,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] @@ -9383,7 +9383,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", "wasm-bindgen-shared", ] @@ -9417,7 +9417,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10201,7 +10201,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.26", ] [[package]] From 7b4b96df47fc176c2329e24f83680c125cc99732 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jul 2023 17:55:36 +1000 Subject: [PATCH 120/138] Update validator_manager/src/move_validators.rs Co-authored-by: Michael Sproul --- validator_manager/src/move_validators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index f41b5675e31..cd7005ecdac 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -207,7 +207,7 @@ impl MoveConfig { .collect::, _>>() .map(Validators::Specific)?, }, - (None, None) => Err("Must supply either --{:VALIDATORS_FLAG} or --{:COUNT_FLAG}.")?, + (None, None) => Err(format!("Must supply either --{VALIDATORS_FLAG} or --{COUNT_FLAG}."))?, (Some(_), Some(_)) => { Err("Cannot supply both --{:VALIDATORS_FLAG} and --{:COUNT_FLAG}.")? } From 09bf5ab4314d6ece806fe8aa368d1de0191dd256 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 17 Jul 2023 17:56:05 +1000 Subject: [PATCH 121/138] Fix format string --- validator_manager/src/move_validators.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index cd7005ecdac..b09cc4a2755 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -207,9 +207,11 @@ impl MoveConfig { .collect::, _>>() .map(Validators::Specific)?, }, - (None, None) => Err(format!("Must supply either --{VALIDATORS_FLAG} or --{COUNT_FLAG}."))?, + (None, None) => Err(format!( + "Must supply either --{VALIDATORS_FLAG} or --{COUNT_FLAG}." + ))?, (Some(_), Some(_)) => { - Err("Cannot supply both --{:VALIDATORS_FLAG} and --{:COUNT_FLAG}.")? + Err("Cannot supply both --{VALIDATORS_FLAG} and --{COUNT_FLAG}.")? } }; From 5c4b83743177e1d0ed438fc61073de7d649679c2 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 18 Jul 2023 17:22:44 +1000 Subject: [PATCH 122/138] Update validator_manager/src/move_validators.rs Co-authored-by: Michael Sproul --- validator_manager/src/move_validators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index b09cc4a2755..5a16efc3a8f 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -595,7 +595,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } Err(UploadError::PatchValidatorFailed(e)) => { eprintln!( - "Failed to set some values on validator {} (e.g., builder, enabled or gas limit. \ + "Failed to set some values on validator {} (e.g., builder, enabled or gas limit). \ These values value may need to be set manually. Continuing with other validators. \ Error was {:?}", i, e From d749888524737193ca14701a9f72cf5c31d2f255 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 18 Jul 2023 17:24:24 +1000 Subject: [PATCH 123/138] Apply suggestions from code review Co-authored-by: Jimmy Chen --- book/src/validator-manager-create.md | 2 +- book/src/validator-manager-move.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index 0bdb078b248..d75fe4983eb 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -69,7 +69,7 @@ lighthouse \ This guide will create two validators and import them to a VC. For simplicity, the same host will be used to generate the keys and run the VC. In reality, -users may want to perform the `import` command on an air-gapped machine and then +users may want to perform the `create` command on an air-gapped machine and then move the `validators.json` and `deposits.json` files to an Internet-connected host. This would help protect the mnemonic from being exposed to the Internet. diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 7c1f5728e4f..b766ce2c220 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -4,7 +4,7 @@ The `lighthouse validator-manager move` command uses the VC HTTP API to move validators from one VC (the "src" VC) to another VC (the "dest" VC). The move operation is *comprehensive*; it will: -- Disable the validators the src VC. +- Disable the validators on the src VC. - Remove the validator keystores from the src VC file system. - Export the slashing database records for the appropriate validators from the src VC to the dest VC. - Enable the validators on the dest VC. From 5bc0f51c67fac5e13c397043cf71dbabb6fc8867 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 18 Jul 2023 17:27:40 +1000 Subject: [PATCH 124/138] Add comment about omitting --beacon-node --- validator_manager/src/create_validators.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index cbd61886f42..606249ad328 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -386,8 +386,9 @@ impl ValidatorsAndDeposits { return Err(format!( "Validator {:?} at derivation index {} already exists in the beacon chain. \ This indicates a slashing risk, be sure to never run the same validator on two \ - different validator clients", - voting_public_key, derivation_index + different validator clients. If you understand the risks and are certain you \ + wish to generate this validator again, omit the --{} flag.", + voting_public_key, derivation_index, BEACON_NODE_FLAG ))? } Ok(None) => eprintln!( From 9ff16795c72f69042de5234a3bedc2106b64bd61 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 18 Jul 2023 17:28:13 +1000 Subject: [PATCH 125/138] Remove references to dupe flag in move --- validator_manager/src/move_validators.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 5a16efc3a8f..6d879e1ba8b 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -525,14 +525,10 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } ImportKeystoreStatus::Error => { eprintln!( - "Upload of keystore {} of {} failed with message: {:?}. \ - A potential solution is run this command again \ - using the --{} flag, however care should be taken to ensure \ - that there are no duplicate deposits submitted.", + "Upload of keystore {} of {} failed with message: {:?}." i + 1, count, status.message, - IGNORE_DUPLICATES_FLAG ); // Retry uploading this validator. sleep_with_retry_message( @@ -574,10 +570,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } Err(UploadError::IncorrectStatusCount(count)) => { eprintln!( - "Keystore was uploaded, however the validator client returned an invalid response. \ - A potential solution is run this command again using the --{} flag, however care \ - should be taken to ensure that there are no duplicate deposits submitted.", - IGNORE_DUPLICATES_FLAG + "Keystore was uploaded, however the validator client returned an invalid response." ); return Err(format!( "Invalid status count in import response: {}", From 1766dbdddeb2eae383c0ef7ab8de80831043ed67 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 18 Jul 2023 17:30:50 +1000 Subject: [PATCH 126/138] Apply suggestions from code review Co-authored-by: Jimmy Chen --- book/src/validator-manager.md | 2 +- validator_client/src/cli.rs | 2 -- validator_manager/src/create_validators.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index 52243a31b9b..546f1a2e560 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -24,7 +24,7 @@ downtime. The `validator-manager` boasts the following features: - One-line command to arbitrarily move validators between two VCs, maintaining the slashing protection database. -- Generates deposit files compatible with the [Ethereum Staking Launchpad](). +- Generates deposit files compatible with the [Ethereum Staking Launchpad][]. - Generally involves zero or very little downtime. - The "key cache" is preserved whenever a validator is added with the validator manager, preventing long waits at start up when a new validator is added. diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 728357ccc61..0789ac78a00 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -207,7 +207,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-allow-keystore-export") .long("http-allow-keystore-export") - .value_name("ORIGIN") .help("If present, allow access to the DELETE /lighthouse/keystores HTTP \ API method, which allows exporting keystores and passwords to HTTP API \ consumers who have access to the API token. This method is useful for \ @@ -219,7 +218,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .arg( Arg::with_name("http-store-passwords-in-secrets-dir") .long("http-store-passwords-in-secrets-dir") - .value_name("ORIGIN") .help("If present, any validators created via the HTTP will have keystore \ passwords stored in the secrets-dir rather than the validator \ definitions file.") diff --git a/validator_manager/src/create_validators.rs b/validator_manager/src/create_validators.rs index 606249ad328..8ea740ff5bb 100644 --- a/validator_manager/src/create_validators.rs +++ b/validator_manager/src/create_validators.rs @@ -68,7 +68,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { 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.") + .help("The first of consecutive key indexes you wish to create.") .takes_value(true) .required(false) .default_value("0"), From db88cd8708224c6a0ea6f30d85f5d93421a38540 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 20 Jul 2023 09:56:44 +1000 Subject: [PATCH 127/138] Fix clippy error --- validator_manager/src/move_validators.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 6d879e1ba8b..305687df1dc 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -525,7 +525,7 @@ async fn run<'a>(config: MoveConfig) -> Result<(), String> { } ImportKeystoreStatus::Error => { eprintln!( - "Upload of keystore {} of {} failed with message: {:?}." + "Upload of keystore {} of {} failed with message: {:?}.", i + 1, count, status.message, From fac1df7ea31a3e597ebfdde04395d8f346ca72eb Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 3 Aug 2023 11:33:44 +1000 Subject: [PATCH 128/138] Apply suggestions from code review Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/validator-manager-create.md | 16 +++++++++------- book/src/validator-manager-move.md | 18 +++++++++--------- book/src/validator-manager.md | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index d75fe4983eb..904756d7444 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -41,16 +41,18 @@ Create validators from a mnemonic with: lighthouse \ validator-manager \ create \ + --network mainnet \ --first-index 0 \ --count 2 \ --eth1-withdrawal-address
\ --suggested-fee-recipient
\ --output-path ./ ``` - +> If the flag `--first-index` is not provided, it will default to using index 0. > The `--suggested-fee-recipient` flag may be omitted to use whatever default -> value the VC uses. It does not necessarily need to be idential to +> value the VC uses. It does not necessarily need to be identical to > `--eth1-withdrawal-address`. +> The command will create the `deposits.json` and `validators.json` in the present working directory. If you would like these files to be created in a different directory, change the value of `output-path`, for example `--output-path /desired/directory`. The directory will be created if the path does not exist. Then, import the validators to a running VC with: @@ -61,7 +63,7 @@ lighthouse \ --validators-file validators.json \ --vc-token ``` - +> This is assuming that `validators.json` is in the present working directory. If it is not, insert the directory of the file. > Be sure to remove `./validators.json` after the import is successful since it > contains unencrypted validator keystores. @@ -75,9 +77,9 @@ host. This would help protect the mnemonic from being exposed to the Internet. ### 1. Create the Validators -Run the `create` command, subsituting `
` for an execution address that +Run the `create` command, substituting `
` for an execution address that you control. This is where all the staked ETH and rewards will ultimately -reside, so it's very important that this address is secure, acessible and +reside, so it's very important that this address is secure, accessible and backed-up. The `create` command: ```bash @@ -143,7 +145,7 @@ In order to import the validators, the location of the VC `api-token.txt` file must be known. The location of the file varies, but it is located in the "validator directory" of your data directory. For example: `~/.lighthouse/mainnet/validators/api-token.txt`. We will use `` -to subsitute this value. +to subsitute this value. If you are unsure of the `api-token.txt` path, you can run `curl http://localhost:5062/lighthouse/auth` which will show the path. Once the VC is running, use the `import` command to import the validators to the VC: @@ -170,6 +172,6 @@ The user should now *securely* delete the `validators.json` file (e.g., `shred - The `validators.json` contains the unencrypted validator keys and must not be shared with anyone. -The validators will now go through 3-4 epochs of [doppelganger +The validators will now go through 2-3 epochs of [doppelganger protection](./validator-doppelganger.md) and will automatically start performing their duties when they are deposited and activated. The guide is complete. \ No newline at end of file diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index b766ce2c220..ef40c02c0d3 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -56,7 +56,7 @@ There will be two hosts in this example: - Host 1 (*"source host"*): Is running the `src-vc`. - Host 2 (*"destination host"*): Is running the `dest-vc`. -The example will assume that all commands are run on Host 1. It also assumes +The example assumes that Host 1 is able to SSH to Host 2. In reality, many host configurations are possible. For example: @@ -66,7 +66,7 @@ In reality, many host configurations are possible. For example: ### 1. Configure the Source VC -The source VC needs to have the following flags at a mininum: +The source VC needs to have the following flags at a minimum: - `--http` - `--unencrypted-http-transport` @@ -88,7 +88,7 @@ lighthouse \ ### 2. Configure the Destination VC -The destination VC needs to have the following flags at a mininum: +The destination VC needs to have the following flags at a minimum: - `--http` - `--unencrypted-http-transport` @@ -109,13 +109,13 @@ lighthouse \ ``` > The `--enable-doppelganger-protection` flag is not *strictly* required, however -> it is recommended for an additional layer of safety. It will result in 3-4 +> it is recommended for an additional layer of safety. It will result in 2-3 > epochs of downtime for the validator after it is moved, which is generally an > inconsequential cost in lost rewards or penalties. > > Optionally, users can add the `--http-store-passwords-in-secrets-dir` flag if they'd like to have > the import validator keystore passwords stored in separate files rather than in the -> `valdiator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. +> `validator-definitions.yml` file. If you don't know what this means, you can safely omit the flag. ### 3. Obtain the Source API Token @@ -125,14 +125,14 @@ source host on its file-system. On the **source host**, find the location of the `api-token.txt` file and copy the contents. The location of the file varies, but it is located in the "validator directory" of your data directory, -alongside validator keystores. For example: `~/.lighthouse/mainnet/validators/api-token.txt`. +alongside validator keystores. For example: `~/.lighthouse/mainnet/validators/api-token.txt`. If you are unsure of the `api-token.txt` path, you can run `curl http://localhost:5062/lighthouse/auth` which will show the path. Copy the contents of that file into a new file on the **destination host** at `~/src-token.txt`. The API token should be similar to `api-token-0x03eace4c98e8f77477bb99efb74f9af10d800bd3318f92c33b719a4644254d4123`. ### 4. Create an SSH Tunnel -In one terminal window, SSH to the **destination host** and establish a reverse-SSH connection +In the **source host**, open a terminal window, SSH to the **destination host** and establish a reverse-SSH connection between the **desination host** and the **source host**. ```bash @@ -173,9 +173,9 @@ Done. Once the operation completes successfully, there is nothing else to be done. The validators have been removed from the `src-host` and enabled at the `dest-host`. -If the `--enable-doppelganger-protection` flag was used it may take 3-4 epochs +If the `--enable-doppelganger-protection` flag was used it may take 2-3 epochs for the validators to start attesting and producing blocks on the `dest-host`. -Any errors encounted during the operation should include information on how to +Any errors encountered during the operation should include information on how to proceed. Assistance is also available on our [Discord](https://discord.gg/cyAszAh). \ No newline at end of file diff --git a/book/src/validator-manager.md b/book/src/validator-manager.md index 546f1a2e560..e3cb74bd668 100644 --- a/book/src/validator-manager.md +++ b/book/src/validator-manager.md @@ -13,7 +13,7 @@ supported by this command. The validator manager tool is similar to the `lighthouse account-manager` tool, except the latter creates files that will be read by the VC next time it starts -whilst the former makers instant changes to a live VC. +whilst the former makes instant changes to a live VC. The `account-manager` is ideal for importing keys created with the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli). On the From c8317f2c29cf7a9800ef731fce00b7cd2d0cdaf7 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 3 Aug 2023 11:34:38 +1000 Subject: [PATCH 129/138] Apply suggestions from code review Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/validator-manager-create.md | 9 ++++++++- book/src/validator-manager-move.md | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index 904756d7444..9d8a3b0c93e 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -171,7 +171,14 @@ Uploaded keystore 2 of 2 to the VC The user should now *securely* delete the `validators.json` file (e.g., `shred -u validators.json`). The `validators.json` contains the unencrypted validator keys and must not be shared with anyone. - +At the same time, `lighthouse vc` will log: +```bash +INFO Importing keystores via standard HTTP API, count: 1 +WARN No slashing protection data provided with keystores +INFO Enabled validator voting_pubkey: 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f, signing_method: local_keystore +INFO Modified key_cache saved successfully +``` +The WARN message means that the `validators.json` file does not contain the slashing protection data. This is normal if you are starting a new validator. The flag `--enable-doppelganger-protection` will also protect users from potential slashing risk. The validators will now go through 2-3 epochs of [doppelganger protection](./validator-doppelganger.md) and will automatically start performing their duties when they are deposited and activated. The guide is complete. \ No newline at end of file diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index ef40c02c0d3..af1a1623595 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -170,7 +170,11 @@ Moved keystore 1 of 2 Moved keystore 2 of 2 Done. ``` - +At the same time, `lighthouse vc` will log: +```bash +INFO Importing keystores via standard HTTP API, count: 1 +INFO Enabled validator voting_pubkey: 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f, signing_method: local_keystore +INFO Modified key_cache saved successfully Once the operation completes successfully, there is nothing else to be done. The validators have been removed from the `src-host` and enabled at the `dest-host`. If the `--enable-doppelganger-protection` flag was used it may take 2-3 epochs From 37c700356c8f219c28c9ea7830e31fe3b3e6556a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 3 Aug 2023 14:53:05 +1000 Subject: [PATCH 130/138] Apply suggestions from code review Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/validator-manager-move.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index af1a1623595..e126ebbd3cb 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -133,7 +133,7 @@ API token should be similar to `api-token-0x03eace4c98e8f77477bb99efb74f9af10d80 ### 4. Create an SSH Tunnel In the **source host**, open a terminal window, SSH to the **destination host** and establish a reverse-SSH connection -between the **desination host** and the **source host**. +between the **destination host** and the **source host**. ```bash ssh dest-host From 1689ed64b9f83afc448ba28e6cbb5afd9368c2b0 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 15:32:15 +1000 Subject: [PATCH 131/138] Remove unencrypted flags --- book/src/validator-manager-create.md | 4 ---- book/src/validator-manager-move.md | 8 -------- 2 files changed, 12 deletions(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index 9d8a3b0c93e..b015303a1e1 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -124,8 +124,6 @@ The command will create two files: The VC which will receive the validators needs to have the following flags at a minimum: - `--http` -- `--unencrypted-http-transport` -- `--http-address 127.0.0.1` - `--http-port 5062` - `--enable-doppelganger-protection` @@ -135,8 +133,6 @@ Therefore, the VC command might look like: lighthouse \ vc \ --http \ - --unencrypted-http-transport \ - --http-address 127.0.0.1 \ --http-port 5062 \ --enable-doppelganger-protection ``` diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index e126ebbd3cb..7b992d75292 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -69,8 +69,6 @@ In reality, many host configurations are possible. For example: The source VC needs to have the following flags at a minimum: - `--http` -- `--unencrypted-http-transport` -- `--http-address 127.0.0.1` - `--http-port 5062` - `--http-allow-keystore-export` @@ -80,8 +78,6 @@ Therefore, the source VC command might look like: lighthouse \ vc \ --http \ - --unencrypted-http-transport \ - --http-address 127.0.0.1 \ --http-port 5062 \ --http-allow-keystore-export ``` @@ -91,8 +87,6 @@ lighthouse \ The destination VC needs to have the following flags at a minimum: - `--http` -- `--unencrypted-http-transport` -- `--http-address 127.0.0.1` - `--http-port 5062` - `--enable-doppelganger-protection` @@ -102,8 +96,6 @@ Therefore, the destination VC command might look like: lighthouse \ vc \ --http \ - --unencrypted-http-transport \ - --http-address 127.0.0.1 \ --http-port 5062 \ --enable-doppelganger-protection ``` From 3422225ed8f9856c6db775c5dc8b80f177d328f4 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 15:33:36 +1000 Subject: [PATCH 132/138] Fix log message with swapped number --- book/src/validator-manager-create.md | 2 +- validator_manager/src/import_validators.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index b015303a1e1..6862813b81b 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -159,7 +159,7 @@ If successful, the command output will appear like below: ```bash Running validator manager for mainnet network Validator client is reachable at http://localhost:5062/ and reports 0 validators -Starting to submit validators 2 to VC, each validator may take several seconds +Starting to submit 2 validators to VC, each validator may take several seconds Uploaded keystore 1 of 2 to the VC Uploaded keystore 2 of 2 to the VC ``` diff --git a/validator_manager/src/import_validators.rs b/validator_manager/src/import_validators.rs index e85f7763537..4b924189f20 100644 --- a/validator_manager/src/import_validators.rs +++ b/validator_manager/src/import_validators.rs @@ -128,7 +128,7 @@ async fn run<'a>(config: ImportConfig) -> Result<(), String> { let (http_client, _keystores) = vc_http_client(vc_url.clone(), &vc_token_path).await?; eprintln!( - "Starting to submit validators {} to VC, each validator may take several seconds", + "Starting to submit {} validators to VC, each validator may take several seconds", count ); From ec9cf1ca28fda6baefce48228053be3afc1d26d1 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 15:43:15 +1000 Subject: [PATCH 133/138] Apply suggestions from code review Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/validator-manager-move.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/book/src/validator-manager-move.md b/book/src/validator-manager-move.md index 7b992d75292..98932604d5d 100644 --- a/book/src/validator-manager-move.md +++ b/book/src/validator-manager-move.md @@ -171,7 +171,18 @@ Once the operation completes successfully, there is nothing else to be done. The validators have been removed from the `src-host` and enabled at the `dest-host`. If the `--enable-doppelganger-protection` flag was used it may take 2-3 epochs for the validators to start attesting and producing blocks on the `dest-host`. +If you would only like to move some validators, you can replace the flag `--validators all` with one or more validator public keys. For example: +```bash +lighthouse \ + validator-manager \ + move \ + --src-vc-url http://localhost:6062 \ + --src-vc-token ~/src-token.txt \ + --dest-vc-url http://localhost:5062 \ + --dest-vc-token ~/.lighthouse/mainnet/validators/api-token.txt \ + --validators 0x9096aab771e44da149bd7c9926d6f7bb96ef465c0eeb4918be5178cd23a1deb4aec232c61d85ff329b54ed4a3bdfff3a,0x90fc4f72d898a8f01ab71242e36f4545aaf87e3887be81632bb8ba4b2ae8fb70753a62f866344d7905e9a07f5a9cdda1 +``` Any errors encountered during the operation should include information on how to proceed. Assistance is also available on our [Discord](https://discord.gg/cyAszAh). \ No newline at end of file From e86c0e6454a8717d9b92ea4e35d4cc7d8a1c56bf Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 15:56:25 +1000 Subject: [PATCH 134/138] Apply suggestions from code review Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/validator-manager-create.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/book/src/validator-manager-create.md b/book/src/validator-manager-create.md index 6862813b81b..779c159276e 100644 --- a/book/src/validator-manager-create.md +++ b/book/src/validator-manager-create.md @@ -177,4 +177,30 @@ INFO Modified key_cache saved successfully The WARN message means that the `validators.json` file does not contain the slashing protection data. This is normal if you are starting a new validator. The flag `--enable-doppelganger-protection` will also protect users from potential slashing risk. The validators will now go through 2-3 epochs of [doppelganger protection](./validator-doppelganger.md) and will automatically start performing -their duties when they are deposited and activated. The guide is complete. \ No newline at end of file +their duties when they are deposited and activated. + +If the host VC contains the same public key as the `validators.json` file, an error will be shown and the `import` process will stop: + +```bash +Duplicate validator 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f already exists on the destination validator client. This may indicate that some validators are running in two places at once, which can lead to slashing. If you are certain that there is no risk, add the --ignore-duplicates flag. +Err(DuplicateValidator(0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f)) +``` + +If you are certain that it is safe, you can add the flag `--ignore-duplicates` in the `import` command. The command becomes: + +```bash +lighthouse \ + validator-manager \ + import \ + --validators-file validators.json \ + --vc-token \ + --ignore-duplicates +``` +and the output will be as follows: + +```bash +Duplicate validators are ignored, ignoring 0xab6e29f1b98fedfca878edce2b471f1b5ee58ee4c3bd216201f98254ef6f6eac40a53d74c8b7da54f51d3e85cacae92f which exists on the destination validator client +Re-uploaded keystore 1 of 6 to the VC +``` + +The guide is complete. \ No newline at end of file From c07a5540d3f5e1cc8f669a52cb9e3f41f329d521 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 15:58:43 +1000 Subject: [PATCH 135/138] Fix --validators help text --- validator_manager/src/move_validators.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/validator_manager/src/move_validators.rs b/validator_manager/src/move_validators.rs index 305687df1dc..fa886e8f941 100644 --- a/validator_manager/src/move_validators.rs +++ b/validator_manager/src/move_validators.rs @@ -118,8 +118,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("STRING") .help( "The validators to be moved. Either a list of 0x-prefixed \ - validator pubkeys, an integer count of validators or the \ - keyword \"all\".", + validator pubkeys or the keyword \"all\".", ) .takes_value(true), ) From 88db492510191bbe4f6cc1a8f30988ea733203ee Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 15:59:37 +1000 Subject: [PATCH 136/138] Update "no validators present" log --- validator_client/src/notifier.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/validator_client/src/notifier.rs b/validator_client/src/notifier.rs index 732ae68ff86..909e64a78a6 100644 --- a/validator_client/src/notifier.rs +++ b/validator_client/src/notifier.rs @@ -94,8 +94,7 @@ async fn notify( info!( log, "No validators present"; - "msg" => "see `lighthouse account validator create --help` \ - or the HTTP API documentation" + "msg" => "see `lighthouse vm create --help` or the HTTP API documentation" ) } else if total_validators == attesting_validators { info!( From 5654966f1ec608641d3faa9b0b9fbdd558234c52 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 7 Aug 2023 16:37:14 +1000 Subject: [PATCH 137/138] Add deprecation notice to "Key Management" --- book/src/SUMMARY.md | 2 +- book/src/key-management.md | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index e0a711f1092..507896f4311 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -44,7 +44,7 @@ * [Remote Signing with Web3Signer](./validator-web3signer.md) * [Database Configuration](./advanced_database.md) * [Database Migrations](./database-migrations.md) - * [Key Management](./key-management.md) + * [Key Management (Deprecated)](./key-management.md) * [Key Recovery](./key-recovery.md) * [Advanced Networking](./advanced_networking.md) * [Running a Slasher](./slasher.md) diff --git a/book/src/key-management.md b/book/src/key-management.md index cebd84649da..ed53455c0c6 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -1,9 +1,30 @@ -# Key Management +# Key Management (Deprecated) [launchpad]: https://launchpad.ethereum.org/ -> -> **Note: While Lighthouse is able to generate the validator keys and the deposit data file to submit to the deposit contract, we strongly recommend using the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) to create validators keys and the deposit data file. This is because the [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) has the option to assign a withdrawal address during the key generation process, while Lighthouse wallet will always generate keys with withdrawal credentials of type 0x00. This means that users who created keys using Lighthouse will have to update their withdrawal credentials in the future to enable withdrawals. In addition, Lighthouse generates the deposit data file in the form of `*.rlp`, which cannot be uploaded to the [Staking launchpad][launchpad] that accepts only `*.json` file. This means that users have to directly interact with the deposit contract to be able to submit the deposit if they were to generate the files using Lighthouse.** +**⚠️ The information on this page refers to tooling and process that have been deprecated. Please read the "Deprecation Notice". ⚠️** + +## Deprecation Notice + +This page recommends the use of the `lighthouse account-manager` tool to create +validators. This tool will always generate keys with the withdrawal credentials +of type `0x00`. This means the users who created keys using `lighthouse +account-manager` will have to update their withdrawal credentials in in a +separate step to receive staking rewards. + +In addition, Lighthouse generates the deposit data file in the form of `*.rlp`, +which cannot be uploaded to the [Staking launchpad][launchpad] that accepts only +`*.json` file. This means that users have to directly interact with the deposit +contract to be able to submit the deposit if they were to generate the files +using Lighthouse. + +Rather than continuing to read this page, we recommend users visit either: + +- The [Staking Launchpad][launchpad] for detailed, beginner-friendly instructions. +- The [staking-deposit-cli](https://github.com/ethereum/staking-deposit-cli) for a CLI tool used by the [Staking Launchpad][launchpad]. +- The [validator-manager documentation](./validator-manager.md) for a Lighthouse-specific tool for streamlined validator management tools. + +## The `lighthouse account-manager` Lighthouse uses a _hierarchical_ key management system for producing validator keys. It is hierarchical because each validator key can be _derived_ from a From a8865f95c403946446d43871b9178d8c53b04e29 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Tue, 8 Aug 2023 08:54:53 +1000 Subject: [PATCH 138/138] Update book/src/key-management.md Co-authored-by: chonghe <44791194+chong-he@users.noreply.github.com> --- book/src/key-management.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/key-management.md b/book/src/key-management.md index ed53455c0c6..b2bb7737fd4 100644 --- a/book/src/key-management.md +++ b/book/src/key-management.md @@ -9,7 +9,7 @@ This page recommends the use of the `lighthouse account-manager` tool to create validators. This tool will always generate keys with the withdrawal credentials of type `0x00`. This means the users who created keys using `lighthouse -account-manager` will have to update their withdrawal credentials in in a +account-manager` will have to update their withdrawal credentials in a separate step to receive staking rewards. In addition, Lighthouse generates the deposit data file in the form of `*.rlp`,