Files
lighthouse/account_manager/src/validator/import.rs
chonghe 522bd9e9c6 Update Rust Edition to 2024 (#7766)
* #7749

Thanks @dknopik and @michaelsproul for your help!
2025-08-13 03:04:31 +00:00

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)),
}
}