mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-11 18:04:18 +00:00
338 lines
13 KiB
Rust
338 lines
13 KiB
Rust
use crate::wallet::create::PASSWORD_FLAG;
|
|
use account_utils::validator_definitions::SigningDefinition;
|
|
use account_utils::{
|
|
STDIN_INPUTS_FLAG,
|
|
eth2_keystore::Keystore,
|
|
read_password_from_user,
|
|
validator_definitions::{
|
|
CONFIG_FILENAME, PasswordStorage, ValidatorDefinition, ValidatorDefinitions,
|
|
recursively_find_voting_keystores,
|
|
},
|
|
};
|
|
use clap::{Arg, ArgAction, ArgMatches, Command};
|
|
use clap_utils::FLAG_HEADER;
|
|
use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase};
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use std::thread::sleep;
|
|
use std::time::Duration;
|
|
use zeroize::Zeroizing;
|
|
|
|
pub const CMD: &str = "import";
|
|
pub const KEYSTORE_FLAG: &str = "keystore";
|
|
pub const DIR_FLAG: &str = "directory";
|
|
pub const REUSE_PASSWORD_FLAG: &str = "reuse-password";
|
|
|
|
pub const PASSWORD_PROMPT: &str = "Enter the keystore password, or press enter to omit it:";
|
|
pub const KEYSTORE_REUSE_WARNING: &str = "DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH \
|
|
ANOTHER CLIENT, OR YOU WILL GET SLASHED.";
|
|
|
|
pub fn cli_app() -> Command {
|
|
Command::new(CMD)
|
|
.about(
|
|
"Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \
|
|
requesting passwords interactively. The directory flag provides a convenient \
|
|
method for importing a directory of keys generated by the ethstaker-deposit-cli \
|
|
Python utility.",
|
|
)
|
|
.arg(
|
|
Arg::new(KEYSTORE_FLAG)
|
|
.long(KEYSTORE_FLAG)
|
|
.value_name("KEYSTORE_PATH")
|
|
.help("Path to a single keystore to be imported.")
|
|
.conflicts_with(DIR_FLAG)
|
|
.required_unless_present(DIR_FLAG)
|
|
.action(ArgAction::Set)
|
|
.display_order(0),
|
|
)
|
|
.arg(
|
|
Arg::new(DIR_FLAG)
|
|
.long(DIR_FLAG)
|
|
.value_name("KEYSTORES_DIRECTORY")
|
|
.help(
|
|
"Path to a directory which contains zero or more keystores \
|
|
for import. This directory and all sub-directories will be \
|
|
searched and any file name which contains 'keystore' and \
|
|
has the '.json' extension will be attempted to be imported.",
|
|
)
|
|
.conflicts_with(KEYSTORE_FLAG)
|
|
.required_unless_present(KEYSTORE_FLAG)
|
|
.action(ArgAction::Set)
|
|
.display_order(0),
|
|
)
|
|
.arg(
|
|
Arg::new(REUSE_PASSWORD_FLAG)
|
|
.long(REUSE_PASSWORD_FLAG)
|
|
.action(ArgAction::SetTrue)
|
|
.help_heading(FLAG_HEADER)
|
|
.help("If present, the same password will be used for all imported keystores.")
|
|
.display_order(0),
|
|
)
|
|
.arg(
|
|
Arg::new(PASSWORD_FLAG)
|
|
.long(PASSWORD_FLAG)
|
|
.value_name("KEYSTORE_PASSWORD_PATH")
|
|
.requires(REUSE_PASSWORD_FLAG)
|
|
.help(
|
|
"The path to the file containing the password which will unlock all \
|
|
keystores being imported. This flag must be used with `--reuse-password`. \
|
|
The password will be copied to the `validator_definitions.yml` file, so after \
|
|
import we strongly recommend you delete the file at KEYSTORE_PASSWORD_PATH.",
|
|
)
|
|
.action(ArgAction::Set)
|
|
.display_order(0),
|
|
)
|
|
}
|
|
|
|
pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> {
|
|
let keystore: Option<PathBuf> = clap_utils::parse_optional(matches, KEYSTORE_FLAG)?;
|
|
let keystores_dir: Option<PathBuf> = clap_utils::parse_optional(matches, DIR_FLAG)?;
|
|
let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG);
|
|
let reuse_password = matches.get_flag(REUSE_PASSWORD_FLAG);
|
|
let keystore_password_path: Option<PathBuf> =
|
|
clap_utils::parse_optional(matches, PASSWORD_FLAG)?;
|
|
|
|
let mut defs = ValidatorDefinitions::open_or_create(&validator_dir)
|
|
.map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?;
|
|
|
|
let slashing_protection_path = validator_dir.join(SLASHING_PROTECTION_FILENAME);
|
|
let slashing_protection =
|
|
SlashingDatabase::open_or_create(&slashing_protection_path).map_err(|e| {
|
|
format!(
|
|
"Unable to open or create slashing protection database at {}: {:?}",
|
|
slashing_protection_path.display(),
|
|
e
|
|
)
|
|
})?;
|
|
|
|
// Create an empty transaction and drop it. Used to test if the database is locked.
|
|
slashing_protection.test_transaction().map_err(|e| {
|
|
format!(
|
|
"Cannot import keys while the validator client is running: {:?}",
|
|
e
|
|
)
|
|
})?;
|
|
|
|
// Collect the paths for the keystores that should be imported.
|
|
let keystore_paths = match (keystore, keystores_dir) {
|
|
(Some(keystore), None) => vec![keystore],
|
|
(None, Some(keystores_dir)) => {
|
|
let mut keystores = vec![];
|
|
|
|
recursively_find_voting_keystores(&keystores_dir, &mut keystores)
|
|
.map_err(|e| format!("Unable to search {:?}: {:?}", keystores_dir, e))?;
|
|
|
|
if keystores.is_empty() {
|
|
eprintln!("No keystores found in {:?}", keystores_dir);
|
|
return Ok(());
|
|
}
|
|
|
|
keystores
|
|
}
|
|
_ => {
|
|
return Err(format!(
|
|
"Must supply either --{} or --{}",
|
|
KEYSTORE_FLAG, DIR_FLAG
|
|
));
|
|
}
|
|
};
|
|
|
|
eprintln!("WARNING: {}", KEYSTORE_REUSE_WARNING);
|
|
|
|
// For each keystore:
|
|
//
|
|
// - Obtain the keystore password, if the user desires.
|
|
// - Copy the keystore into the `validator_dir`.
|
|
// - Register the voting key with the slashing protection database.
|
|
// - Add the keystore to the validator definitions file.
|
|
//
|
|
// Skip keystores that already exist, but exit early if any operation fails.
|
|
// Reuses the same password for all keystores if the `REUSE_PASSWORD_FLAG` flag is set.
|
|
let mut num_imported_keystores = 0;
|
|
let mut previous_password: Option<Zeroizing<String>> = None;
|
|
|
|
for src_keystore in &keystore_paths {
|
|
let keystore = Keystore::from_json_file(src_keystore)
|
|
.map_err(|e| format!("Unable to read keystore JSON {:?}: {:?}", src_keystore, e))?;
|
|
|
|
eprintln!();
|
|
eprintln!("Keystore found at {:?}:", src_keystore);
|
|
eprintln!();
|
|
eprintln!(" - Public key: 0x{}", keystore.pubkey());
|
|
eprintln!(" - UUID: {}", keystore.uuid());
|
|
eprintln!();
|
|
eprintln!(
|
|
"If you enter the password it will be stored as plain-text in {} so that it is not \
|
|
required each time the validator client starts.",
|
|
CONFIG_FILENAME
|
|
);
|
|
|
|
let password_opt = loop {
|
|
if let Some(password) = previous_password.clone() {
|
|
eprintln!("Reuse previous password.");
|
|
if check_password_on_keystore(&keystore, &password)? {
|
|
break Some(password);
|
|
} else {
|
|
eprintln!("Reused password incorrect. Retry!");
|
|
previous_password = None;
|
|
continue;
|
|
}
|
|
}
|
|
eprintln!();
|
|
eprintln!("{}", PASSWORD_PROMPT);
|
|
|
|
let password = match keystore_password_path.as_ref() {
|
|
Some(path) => {
|
|
let password_from_file: Zeroizing<String> = fs::read_to_string(path)
|
|
.map_err(|e| format!("Unable to read {:?}: {:?}", path, e))?
|
|
.into();
|
|
password_from_file
|
|
.trim_end_matches(['\r', '\n'])
|
|
.to_string()
|
|
.into()
|
|
}
|
|
None => {
|
|
let password_from_user = read_password_from_user(stdin_inputs)?;
|
|
if password_from_user.is_empty() {
|
|
eprintln!("Continuing without password.");
|
|
sleep(Duration::from_secs(1)); // Provides nicer UX.
|
|
break None;
|
|
}
|
|
password_from_user
|
|
}
|
|
};
|
|
|
|
// Check if the password unlocks the keystore
|
|
if check_password_on_keystore(&keystore, &password)? {
|
|
if reuse_password {
|
|
previous_password = Some(password.clone());
|
|
}
|
|
break Some(password);
|
|
}
|
|
};
|
|
|
|
let voting_pubkey = keystore
|
|
.public_key()
|
|
.ok_or_else(|| format!("Keystore public key is invalid: {}", keystore.pubkey()))?;
|
|
|
|
// The keystore is placed in a directory that matches the name of the public key. This
|
|
// provides some loose protection against adding the same keystore twice.
|
|
let dest_dir = validator_dir.join(format!("0x{}", keystore.pubkey()));
|
|
if dest_dir.exists() {
|
|
// Check if we should update password for existing validator in case if it was provided via reimport: #2854
|
|
let old_validator_def_opt = defs
|
|
.as_mut_slice()
|
|
.iter_mut()
|
|
.find(|def| def.voting_public_key == voting_pubkey);
|
|
if let Some(ValidatorDefinition {
|
|
signing_definition:
|
|
SigningDefinition::LocalKeystore {
|
|
voting_keystore_password: old_passwd,
|
|
..
|
|
},
|
|
..
|
|
}) = old_validator_def_opt
|
|
&& old_passwd.is_none()
|
|
&& password_opt.is_some()
|
|
{
|
|
*old_passwd = password_opt;
|
|
defs.save(&validator_dir)
|
|
.map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?;
|
|
eprintln!("Password updated for public key {}", voting_pubkey);
|
|
}
|
|
|
|
eprintln!(
|
|
"Skipping import of keystore for existing public key: {:?}",
|
|
src_keystore
|
|
);
|
|
continue;
|
|
}
|
|
|
|
fs::create_dir_all(&dest_dir)
|
|
.map_err(|e| format!("Unable to create import directory: {:?}", e))?;
|
|
|
|
// Retain the keystore file name, but place it in the new directory.
|
|
let dest_keystore = src_keystore
|
|
.file_name()
|
|
.and_then(|file_name| file_name.to_str())
|
|
.map(|file_name_str| dest_dir.join(file_name_str))
|
|
.ok_or_else(|| format!("Badly formatted file name: {:?}", src_keystore))?;
|
|
|
|
// Copy the keystore to the new location.
|
|
fs::copy(src_keystore, &dest_keystore)
|
|
.map_err(|e| format!("Unable to copy keystore: {:?}", e))?;
|
|
|
|
// Register with slashing protection.
|
|
slashing_protection
|
|
.register_validator(voting_pubkey.compress())
|
|
.map_err(|e| {
|
|
format!(
|
|
"Error registering validator {}: {:?}",
|
|
voting_pubkey.as_hex_string(),
|
|
e
|
|
)
|
|
})?;
|
|
|
|
eprintln!("Successfully imported keystore.");
|
|
num_imported_keystores += 1;
|
|
|
|
let graffiti = None;
|
|
let suggested_fee_recipient = None;
|
|
let validator_def = ValidatorDefinition::new_keystore_with_password(
|
|
&dest_keystore,
|
|
password_opt
|
|
.map(PasswordStorage::ValidatorDefinitions)
|
|
.unwrap_or(PasswordStorage::None),
|
|
graffiti,
|
|
suggested_fee_recipient,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.map_err(|e| format!("Unable to create new validator definition: {:?}", e))?;
|
|
|
|
defs.push(validator_def);
|
|
|
|
defs.save(&validator_dir)
|
|
.map_err(|e| format!("Unable to save {}: {:?}", CONFIG_FILENAME, e))?;
|
|
|
|
eprintln!("Successfully updated {}.", CONFIG_FILENAME);
|
|
}
|
|
|
|
eprintln!();
|
|
eprintln!(
|
|
"Successfully imported {} validators ({} skipped).",
|
|
num_imported_keystores,
|
|
keystore_paths.len() - num_imported_keystores
|
|
);
|
|
eprintln!();
|
|
eprintln!("WARNING: {}", KEYSTORE_REUSE_WARNING);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks if the given password unlocks the keystore.
|
|
///
|
|
/// Returns `Ok(true)` if password unlocks the keystore successfully.
|
|
/// Returns `Ok(false` if password is incorrect.
|
|
/// Otherwise, returns the keystore error.
|
|
fn check_password_on_keystore(
|
|
keystore: &Keystore,
|
|
password: &Zeroizing<String>,
|
|
) -> Result<bool, String> {
|
|
match keystore.decrypt_keypair(password.as_ref()) {
|
|
Ok(_) => {
|
|
eprintln!("Password is correct.");
|
|
eprintln!();
|
|
sleep(Duration::from_secs(1)); // Provides nicer UX.
|
|
Ok(true)
|
|
}
|
|
Err(eth2_keystore::Error::InvalidPassword) => {
|
|
eprintln!("Invalid password");
|
|
Ok(false)
|
|
}
|
|
Err(e) => Err(format!("Error whilst decrypting keypair: {:?}", e)),
|
|
}
|
|
}
|