Mnemonic key recovery (#1579)
## Issue Addressed N/A ## Proposed Changes Add a `lighthouse am wallet recover` command that recreates a wallet from a mnemonic but no validator keys. Add a `lighthouse am validator recover` command which would directly create keys from a mnemonic for a given index and count. ## Additional Info Co-authored-by: Paul Hauner <paul@paulhauner.com>
This commit is contained in:
parent
00cdc4bb35
commit
9cf8f45192
@ -1,6 +1,15 @@
|
|||||||
|
use account_utils::PlainText;
|
||||||
|
use account_utils::{read_mnemonic_from_user, strip_off_newlines};
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
|
use eth2_wallet::bip39::{Language, Mnemonic};
|
||||||
|
use std::fs;
|
||||||
use std::fs::create_dir_all;
|
use std::fs::create_dir_all;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::str::from_utf8;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub const MNEMONIC_PROMPT: &str = "Enter the mnemonic phrase:";
|
||||||
|
|
||||||
pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> Result<(), String> {
|
pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> Result<(), String> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
@ -19,3 +28,43 @@ pub fn base_wallet_dir(matches: &ArgMatches, arg: &'static str) -> Result<PathBu
|
|||||||
PathBuf::new().join(".lighthouse").join("wallets"),
|
PathBuf::new().join(".lighthouse").join("wallets"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_mnemonic_from_cli(
|
||||||
|
mnemonic_path: Option<PathBuf>,
|
||||||
|
stdin_password: bool,
|
||||||
|
) -> Result<Mnemonic, String> {
|
||||||
|
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_mnemonic_from_user(stdin_password)?;
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ pub mod create;
|
|||||||
pub mod deposit;
|
pub mod deposit;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
pub mod recover;
|
||||||
|
|
||||||
use crate::common::base_wallet_dir;
|
use crate::common::base_wallet_dir;
|
||||||
use clap::{App, Arg, ArgMatches};
|
use clap::{App, Arg, ArgMatches};
|
||||||
@ -24,6 +25,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.subcommand(deposit::cli_app())
|
.subcommand(deposit::cli_app())
|
||||||
.subcommand(import::cli_app())
|
.subcommand(import::cli_app())
|
||||||
.subcommand(list::cli_app())
|
.subcommand(list::cli_app())
|
||||||
|
.subcommand(recover::cli_app())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<(), String> {
|
pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<(), String> {
|
||||||
@ -34,6 +36,7 @@ pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<
|
|||||||
(deposit::CMD, Some(matches)) => deposit::cli_run::<T>(matches, env),
|
(deposit::CMD, Some(matches)) => deposit::cli_run::<T>(matches, env),
|
||||||
(import::CMD, Some(matches)) => import::cli_run(matches),
|
(import::CMD, Some(matches)) => import::cli_run(matches),
|
||||||
(list::CMD, Some(matches)) => list::cli_run(matches),
|
(list::CMD, Some(matches)) => list::cli_run(matches),
|
||||||
|
(recover::CMD, Some(matches)) => recover::cli_run(matches),
|
||||||
(unknown, _) => Err(format!(
|
(unknown, _) => Err(format!(
|
||||||
"{} does not have a {} command. See --help",
|
"{} does not have a {} command. See --help",
|
||||||
CMD, unknown
|
CMD, unknown
|
||||||
|
156
account_manager/src/validator/recover.rs
Normal file
156
account_manager/src/validator/recover.rs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
use super::create::STORE_WITHDRAW_FLAG;
|
||||||
|
use super::import::STDIN_PASSWORD_FLAG;
|
||||||
|
use crate::common::{ensure_dir_exists, read_mnemonic_from_cli};
|
||||||
|
use crate::validator::create::COUNT_FLAG;
|
||||||
|
use crate::{SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG};
|
||||||
|
use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder};
|
||||||
|
use account_utils::random_password;
|
||||||
|
use clap::{App, Arg, ArgMatches};
|
||||||
|
use eth2_wallet::bip39::Seed;
|
||||||
|
use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use validator_dir::Builder as ValidatorDirBuilder;
|
||||||
|
pub const CMD: &str = "recover";
|
||||||
|
pub const FIRST_INDEX_FLAG: &str = "first-index";
|
||||||
|
pub const MNEMONIC_FLAG: &str = "mnemonic-path";
|
||||||
|
|
||||||
|
pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
||||||
|
App::new(CMD)
|
||||||
|
.about(
|
||||||
|
"Recovers validator private keys given a BIP-39 mnemonic phrase. \
|
||||||
|
If you did not specify a `--first-index` or count `--count`, by default this will \
|
||||||
|
only recover the keys associated with the validator at index 0 for an HD wallet \
|
||||||
|
in accordance with the EIP-2333 spec.")
|
||||||
|
.arg(
|
||||||
|
Arg::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("COUNT")
|
||||||
|
.help("The number of validator keys you wish to recover. Counted consecutively from the provided `--first_index`.")
|
||||||
|
.takes_value(true)
|
||||||
|
.required(false)
|
||||||
|
.default_value("1"),
|
||||||
|
)
|
||||||
|
.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(VALIDATOR_DIR_FLAG)
|
||||||
|
.long(VALIDATOR_DIR_FLAG)
|
||||||
|
.value_name("VALIDATOR_DIRECTORY")
|
||||||
|
.help(
|
||||||
|
"The path where the validator directories will be created. \
|
||||||
|
Defaults to ~/.lighthouse/validators",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(SECRETS_DIR_FLAG)
|
||||||
|
.long(SECRETS_DIR_FLAG)
|
||||||
|
.value_name("SECRETS_DIR")
|
||||||
|
.help(
|
||||||
|
"The path where the validator keystore passwords will be stored. \
|
||||||
|
Defaults to ~/.lighthouse/secrets",
|
||||||
|
)
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(STORE_WITHDRAW_FLAG)
|
||||||
|
.long(STORE_WITHDRAW_FLAG)
|
||||||
|
.help(
|
||||||
|
"If present, the withdrawal keystore will be stored alongside the voting \
|
||||||
|
keypair. It is generally recommended to *not* store the withdrawal key and \
|
||||||
|
instead generate them from the wallet seed when required.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(STDIN_PASSWORD_FLAG)
|
||||||
|
.long(STDIN_PASSWORD_FLAG)
|
||||||
|
.help("If present, read passwords from stdin instead of tty."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cli_run(matches: &ArgMatches) -> Result<(), String> {
|
||||||
|
let validator_dir = clap_utils::parse_path_with_default_in_home_dir(
|
||||||
|
matches,
|
||||||
|
VALIDATOR_DIR_FLAG,
|
||||||
|
PathBuf::new().join(".lighthouse").join("validators"),
|
||||||
|
)?;
|
||||||
|
let secrets_dir = clap_utils::parse_path_with_default_in_home_dir(
|
||||||
|
matches,
|
||||||
|
SECRETS_DIR_FLAG,
|
||||||
|
PathBuf::new().join(".lighthouse").join("secrets"),
|
||||||
|
)?;
|
||||||
|
let first_index: u32 = clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?;
|
||||||
|
let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?;
|
||||||
|
let mnemonic_path: Option<PathBuf> = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?;
|
||||||
|
let stdin_password = matches.is_present(STDIN_PASSWORD_FLAG);
|
||||||
|
|
||||||
|
ensure_dir_exists(&validator_dir)?;
|
||||||
|
ensure_dir_exists(&secrets_dir)?;
|
||||||
|
|
||||||
|
eprintln!("");
|
||||||
|
eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING.");
|
||||||
|
eprintln!("");
|
||||||
|
|
||||||
|
let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_password)?;
|
||||||
|
|
||||||
|
let seed = Seed::new(&mnemonic, "");
|
||||||
|
|
||||||
|
for index in first_index..first_index + count {
|
||||||
|
let voting_password = random_password();
|
||||||
|
let withdrawal_password = random_password();
|
||||||
|
|
||||||
|
let derive = |key_type: KeyType, password: &[u8]| -> Result<Keystore, String> {
|
||||||
|
let (secret, path) =
|
||||||
|
recover_validator_secret_from_mnemonic(seed.as_bytes(), index, key_type)
|
||||||
|
.map_err(|e| format!("Unable to recover validator keys: {:?}", e))?;
|
||||||
|
|
||||||
|
let keypair = keypair_from_secret(secret.as_bytes())
|
||||||
|
.map_err(|e| format!("Unable build keystore: {:?}", e))?;
|
||||||
|
|
||||||
|
KeystoreBuilder::new(&keypair, password, format!("{}", path))
|
||||||
|
.map_err(|e| format!("Unable build keystore: {:?}", e))?
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Unable build keystore: {:?}", e))
|
||||||
|
};
|
||||||
|
|
||||||
|
let keystores = ValidatorKeystores {
|
||||||
|
voting: derive(KeyType::Voting, voting_password.as_bytes())?,
|
||||||
|
withdrawal: derive(KeyType::Withdrawal, withdrawal_password.as_bytes())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let voting_pubkey = keystores.voting.pubkey().to_string();
|
||||||
|
|
||||||
|
ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone())
|
||||||
|
.voting_keystore(keystores.voting, voting_password.as_bytes())
|
||||||
|
.withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes())
|
||||||
|
.store_withdrawal_keystore(matches.is_present(STORE_WITHDRAW_FLAG))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Unable to build validator directory: {:?}", e))?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}/{}\tIndex: {}\t0x{}",
|
||||||
|
index - first_index,
|
||||||
|
count - first_index,
|
||||||
|
index,
|
||||||
|
voting_pubkey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -5,7 +5,7 @@ use eth2_wallet::{
|
|||||||
bip39::{Language, Mnemonic, MnemonicType},
|
bip39::{Language, Mnemonic, MnemonicType},
|
||||||
PlainText,
|
PlainText,
|
||||||
};
|
};
|
||||||
use eth2_wallet_manager::{WalletManager, WalletType};
|
use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
@ -70,46 +70,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> {
|
pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> {
|
||||||
let name: String = clap_utils::parse_required(matches, NAME_FLAG)?;
|
|
||||||
let wallet_password_path: PathBuf = clap_utils::parse_required(matches, PASSWORD_FLAG)?;
|
|
||||||
let mnemonic_output_path: Option<PathBuf> = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?;
|
let mnemonic_output_path: Option<PathBuf> = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?;
|
||||||
let type_field: String = clap_utils::parse_required(matches, TYPE_FLAG)?;
|
|
||||||
|
|
||||||
let wallet_type = match type_field.as_ref() {
|
|
||||||
HD_TYPE => WalletType::Hd,
|
|
||||||
unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mgr = WalletManager::open(&base_dir)
|
|
||||||
.map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?;
|
|
||||||
|
|
||||||
// Create a new random mnemonic.
|
// Create a new random mnemonic.
|
||||||
//
|
//
|
||||||
// The `tiny-bip39` crate uses `thread_rng()` for this entropy.
|
// The `tiny-bip39` crate uses `thread_rng()` for this entropy.
|
||||||
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
|
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
|
||||||
|
|
||||||
// Create a random password if the file does not exist.
|
let wallet = create_wallet_from_mnemonic(matches, &base_dir.as_path(), &mnemonic)?;
|
||||||
if !wallet_password_path.exists() {
|
|
||||||
// To prevent users from accidentally supplying their password to the PASSWORD_FLAG and
|
|
||||||
// create a file with that name, we require that the password has a .pass suffix.
|
|
||||||
if wallet_password_path.extension() != Some(&OsStr::new("pass")) {
|
|
||||||
return Err(format!(
|
|
||||||
"Only creates a password file if that file ends in .pass: {:?}",
|
|
||||||
wallet_password_path
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
create_with_600_perms(&wallet_password_path, random_password().as_bytes())
|
|
||||||
.map_err(|e| format!("Unable to write to {:?}: {:?}", wallet_password_path, e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wallet_password = fs::read(&wallet_password_path)
|
|
||||||
.map_err(|e| format!("Unable to read {:?}: {:?}", wallet_password_path, e))
|
|
||||||
.map(|bytes| PlainText::from(strip_off_newlines(bytes)))?;
|
|
||||||
|
|
||||||
let wallet = mgr
|
|
||||||
.create_wallet(name, wallet_type, &mnemonic, wallet_password.as_bytes())
|
|
||||||
.map_err(|e| format!("Unable to create wallet: {:?}", e))?;
|
|
||||||
|
|
||||||
if let Some(path) = mnemonic_output_path {
|
if let Some(path) = mnemonic_output_path {
|
||||||
create_with_600_perms(&path, mnemonic.phrase().as_bytes())
|
create_with_600_perms(&path, mnemonic.phrase().as_bytes())
|
||||||
@ -140,6 +108,48 @@ pub fn cli_run(matches: &ArgMatches, base_dir: PathBuf) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_wallet_from_mnemonic(
|
||||||
|
matches: &ArgMatches,
|
||||||
|
base_dir: &Path,
|
||||||
|
mnemonic: &Mnemonic,
|
||||||
|
) -> Result<LockedWallet, String> {
|
||||||
|
let name: String = clap_utils::parse_required(matches, NAME_FLAG)?;
|
||||||
|
let wallet_password_path: PathBuf = clap_utils::parse_required(matches, PASSWORD_FLAG)?;
|
||||||
|
let type_field: String = clap_utils::parse_required(matches, TYPE_FLAG)?;
|
||||||
|
|
||||||
|
let wallet_type = match type_field.as_ref() {
|
||||||
|
HD_TYPE => WalletType::Hd,
|
||||||
|
unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mgr = WalletManager::open(&base_dir)
|
||||||
|
.map_err(|e| format!("Unable to open --{}: {:?}", BASE_DIR_FLAG, e))?;
|
||||||
|
|
||||||
|
// Create a random password if the file does not exist.
|
||||||
|
if !wallet_password_path.exists() {
|
||||||
|
// To prevent users from accidentally supplying their password to the PASSWORD_FLAG and
|
||||||
|
// create a file with that name, we require that the password has a .pass suffix.
|
||||||
|
if wallet_password_path.extension() != Some(&OsStr::new("pass")) {
|
||||||
|
return Err(format!(
|
||||||
|
"Only creates a password file if that file ends in .pass: {:?}",
|
||||||
|
wallet_password_path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
create_with_600_perms(&wallet_password_path, random_password().as_bytes())
|
||||||
|
.map_err(|e| format!("Unable to write to {:?}: {:?}", wallet_password_path, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet_password = fs::read(&wallet_password_path)
|
||||||
|
.map_err(|e| format!("Unable to read {:?}: {:?}", wallet_password_path, e))
|
||||||
|
.map(|bytes| PlainText::from(strip_off_newlines(bytes)))?;
|
||||||
|
|
||||||
|
let wallet = mgr
|
||||||
|
.create_wallet(name, wallet_type, &mnemonic, wallet_password.as_bytes())
|
||||||
|
.map_err(|e| format!("Unable to create wallet: {:?}", e))?;
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a file with `600 (-rw-------)` permissions.
|
/// Creates a file with `600 (-rw-------)` permissions.
|
||||||
pub fn create_with_600_perms<P: AsRef<Path>>(path: P, bytes: &[u8]) -> Result<(), String> {
|
pub fn create_with_600_perms<P: AsRef<Path>>(path: P, bytes: &[u8]) -> Result<(), String> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
pub mod recover;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{base_wallet_dir, ensure_dir_exists},
|
common::{base_wallet_dir, ensure_dir_exists},
|
||||||
@ -21,6 +22,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
|||||||
)
|
)
|
||||||
.subcommand(create::cli_app())
|
.subcommand(create::cli_app())
|
||||||
.subcommand(list::cli_app())
|
.subcommand(list::cli_app())
|
||||||
|
.subcommand(recover::cli_app())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli_run(matches: &ArgMatches) -> Result<(), String> {
|
pub fn cli_run(matches: &ArgMatches) -> Result<(), String> {
|
||||||
@ -30,6 +32,7 @@ pub fn cli_run(matches: &ArgMatches) -> Result<(), String> {
|
|||||||
match matches.subcommand() {
|
match matches.subcommand() {
|
||||||
(create::CMD, Some(matches)) => create::cli_run(matches, base_dir),
|
(create::CMD, Some(matches)) => create::cli_run(matches, base_dir),
|
||||||
(list::CMD, Some(_)) => list::cli_run(base_dir),
|
(list::CMD, Some(_)) => list::cli_run(base_dir),
|
||||||
|
(recover::CMD, Some(matches)) => recover::cli_run(matches, base_dir),
|
||||||
(unknown, _) => Err(format!(
|
(unknown, _) => Err(format!(
|
||||||
"{} does not have a {} command. See --help",
|
"{} does not have a {} command. See --help",
|
||||||
CMD, unknown
|
CMD, unknown
|
||||||
|
87
account_manager/src/wallet/recover.rs
Normal file
87
account_manager/src/wallet/recover.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use crate::common::read_mnemonic_from_cli;
|
||||||
|
use crate::wallet::create::create_wallet_from_mnemonic;
|
||||||
|
use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG};
|
||||||
|
use clap::{App, Arg, ArgMatches};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const CMD: &str = "recover";
|
||||||
|
pub const MNEMONIC_FLAG: &str = "mnemonic-path";
|
||||||
|
pub const STDIN_PASSWORD_FLAG: &str = "stdin-passwords";
|
||||||
|
|
||||||
|
pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
||||||
|
App::new(CMD)
|
||||||
|
.about("Recovers an EIP-2386 wallet from a given a BIP-39 mnemonic phrase.")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(NAME_FLAG)
|
||||||
|
.long(NAME_FLAG)
|
||||||
|
.value_name("WALLET_NAME")
|
||||||
|
.help(
|
||||||
|
"The wallet will be created with this name. It is not allowed to \
|
||||||
|
create two wallets with the same name for the same --base-dir.",
|
||||||
|
)
|
||||||
|
.takes_value(true)
|
||||||
|
.required(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(PASSWORD_FLAG)
|
||||||
|
.long(PASSWORD_FLAG)
|
||||||
|
.value_name("PASSWORD_FILE_PATH")
|
||||||
|
.help(
|
||||||
|
"This will be the new password for your recovered wallet. \
|
||||||
|
A path to a file containing the password which will unlock the wallet. \
|
||||||
|
If the file does not exist, a random password will be generated and \
|
||||||
|
saved at that path. To avoid confusion, if the file does not already \
|
||||||
|
exist it must include a '.pass' suffix.",
|
||||||
|
)
|
||||||
|
.takes_value(true)
|
||||||
|
.required(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(TYPE_FLAG)
|
||||||
|
.long(TYPE_FLAG)
|
||||||
|
.value_name("WALLET_TYPE")
|
||||||
|
.help(
|
||||||
|
"The type of wallet to create. Only HD (hierarchical-deterministic) \
|
||||||
|
wallets are supported presently..",
|
||||||
|
)
|
||||||
|
.takes_value(true)
|
||||||
|
.possible_values(&[HD_TYPE])
|
||||||
|
.default_value(HD_TYPE),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name(STDIN_PASSWORD_FLAG)
|
||||||
|
.long(STDIN_PASSWORD_FLAG)
|
||||||
|
.help("If present, read passwords from stdin instead of tty."),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> {
|
||||||
|
let mnemonic_path: Option<PathBuf> = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?;
|
||||||
|
let stdin_password = matches.is_present(STDIN_PASSWORD_FLAG);
|
||||||
|
|
||||||
|
eprintln!("");
|
||||||
|
eprintln!("WARNING: KEY RECOVERY CAN LEAD TO DUPLICATING VALIDATORS KEYS, WHICH CAN LEAD TO SLASHING.");
|
||||||
|
eprintln!("");
|
||||||
|
|
||||||
|
let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_password)?;
|
||||||
|
|
||||||
|
let wallet = create_wallet_from_mnemonic(matches, &wallet_base_dir.as_path(), &mnemonic)
|
||||||
|
.map_err(|e| format!("Unable to create wallet: {:?}", e))?;
|
||||||
|
|
||||||
|
println!("Your wallet has been successfully recovered.");
|
||||||
|
println!();
|
||||||
|
println!("Your wallet's UUID is:");
|
||||||
|
println!();
|
||||||
|
println!("\t{}", wallet.wallet().uuid());
|
||||||
|
println!();
|
||||||
|
println!("You do not need to backup your UUID or keep it secret.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
* [Key Management](./key-management.md)
|
* [Key Management](./key-management.md)
|
||||||
* [Create a wallet](./wallet-create.md)
|
* [Create a wallet](./wallet-create.md)
|
||||||
* [Create a validator](./validator-create.md)
|
* [Create a validator](./validator-create.md)
|
||||||
|
* [Key recovery](./key-recovery.md)
|
||||||
* [Validator Management](./validator-management.md)
|
* [Validator Management](./validator-management.md)
|
||||||
* [Importing from the Eth2 Launchpad](./validator-import-launchpad.md)
|
* [Importing from the Eth2 Launchpad](./validator-import-launchpad.md)
|
||||||
* [Local Testnets](./local-testnets.md)
|
* [Local Testnets](./local-testnets.md)
|
||||||
|
65
book/src/key-recovery.md
Normal file
65
book/src/key-recovery.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Key recovery
|
||||||
|
|
||||||
|
|
||||||
|
Generally, validator keystore files are generated alongside a *mnemonic*. If
|
||||||
|
the keystore and/or the keystore password are lost this mnemonic can
|
||||||
|
regenerate a new, equivalent keystore with a new password.
|
||||||
|
|
||||||
|
There are two ways to recover keys using the `lighthouse` CLI:
|
||||||
|
|
||||||
|
- `lighthouse account validator recover`: recover one or more EIP-2335 keystores from a mnemonic.
|
||||||
|
These keys can be used directly in a validator client.
|
||||||
|
- `lighthouse account wallet recover`: recover an EIP-2386 wallet from a
|
||||||
|
mnemonic.
|
||||||
|
|
||||||
|
## ⚠️ Warning
|
||||||
|
|
||||||
|
**Recovering validator keys from a mnemonic should only be used as a last
|
||||||
|
resort.** Key recovery entails significant risks:
|
||||||
|
|
||||||
|
- Exposing your mnemonic to a computer at any time puts it at risk of being
|
||||||
|
compromised. Your mnemonic is **not encrypted** and is a target for theft.
|
||||||
|
- It's completely possible to regenerate a validator keypairs that is already active
|
||||||
|
on some other validator client. Running the same keypairs on two different
|
||||||
|
validator clients is very likely to result in slashing.
|
||||||
|
|
||||||
|
## Recover EIP-2335 validator keystores
|
||||||
|
|
||||||
|
A single mnemonic can generate a practically unlimited number of validator
|
||||||
|
keystores using an *index*. Generally, the first time you generate a keystore
|
||||||
|
you'll use index 0, the next time you'll use index 1, and so on. Using the same
|
||||||
|
index on the same mnemonic always results in the same validator keypair being
|
||||||
|
generated (see [EIP-2334](https://eips.ethereum.org/EIPS/eip-2334) for more
|
||||||
|
detail).
|
||||||
|
|
||||||
|
|
||||||
|
Using the `lighthouse account validator recover` command you can generate the
|
||||||
|
keystores that correspond to one or more indices in the mnemonic:
|
||||||
|
|
||||||
|
- `lighthouse account validator recover`: recover only index `0`.
|
||||||
|
- `lighthouse account validator recover --count 2`: recover indices `0, 1`.
|
||||||
|
- `lighthouse account validator recover --first-index 1`: recover only index `1`.
|
||||||
|
- `lighthouse account validator recover --first-index 1 --count 2`: recover indices `1, 2`.
|
||||||
|
|
||||||
|
|
||||||
|
For each of the indices recovered in the above commands, a directory will be
|
||||||
|
created in the `--validator-dir` location (default `~/.lighthouse/validator`)
|
||||||
|
which contains all the information necessary to run a validator using the
|
||||||
|
`lighthouse vc` command. The password to this new keystore will be placed in
|
||||||
|
the `--secrets-dir` (default `~/.lighthouse/secrets`).
|
||||||
|
|
||||||
|
## Recover a EIP-2386 wallet
|
||||||
|
|
||||||
|
Instead of creating EIP-2335 keystores directly, an EIP-2386 wallet can be
|
||||||
|
generated from the mnemonic. This wallet can then be used to generate validator
|
||||||
|
keystores, if desired. For example, the following command will create an
|
||||||
|
encrypted wallet named `wally-recovered` from a mnemonic:
|
||||||
|
|
||||||
|
```
|
||||||
|
lighthouse account wallet recover --name wally-recovered
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Warning:** the wallet will be created with a `nextaccount` value of `0`.
|
||||||
|
This means that if you have already generated `n` validators, then the next `n`
|
||||||
|
validators generated by this wallet will be duplicates. As mentioned
|
||||||
|
previously, running duplicate validators is likely to result in slashing.
|
@ -107,6 +107,23 @@ pub fn read_password_from_user(use_stdin: bool) -> Result<ZeroizeString, String>
|
|||||||
result.map(ZeroizeString::from)
|
result.map(ZeroizeString::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads a mnemonic phrase from TTY or stdin if `use_stdin == true`.
|
||||||
|
pub fn read_mnemonic_from_user(use_stdin: bool) -> Result<String, String> {
|
||||||
|
let mut input = String::new();
|
||||||
|
if use_stdin {
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut input)
|
||||||
|
.map_err(|e| format!("Error reading from stdin: {}", e))?;
|
||||||
|
} else {
|
||||||
|
let tty = File::open("/dev/tty").map_err(|e| format!("Error opening tty: {}", e))?;
|
||||||
|
let mut buf_reader = io::BufReader::new(tty);
|
||||||
|
buf_reader
|
||||||
|
.read_line(&mut input)
|
||||||
|
.map_err(|e| format!("Error reading from tty: {}", e))?;
|
||||||
|
}
|
||||||
|
Ok(input)
|
||||||
|
}
|
||||||
|
|
||||||
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
|
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
|
||||||
///
|
///
|
||||||
/// Useful for ensuring that password memory is zeroed-out on drop.
|
/// Useful for ensuring that password memory is zeroed-out on drop.
|
||||||
|
@ -6,6 +6,6 @@ pub mod json_wallet;
|
|||||||
pub use bip39;
|
pub use bip39;
|
||||||
pub use validator_path::{KeyType, ValidatorPath, COIN_TYPE, PURPOSE};
|
pub use validator_path::{KeyType, ValidatorPath, COIN_TYPE, PURPOSE};
|
||||||
pub use wallet::{
|
pub use wallet::{
|
||||||
recover_validator_secret, DerivedKey, Error, KeystoreError, PlainText, Uuid,
|
recover_validator_secret, recover_validator_secret_from_mnemonic, DerivedKey, Error,
|
||||||
ValidatorKeystores, Wallet, WalletBuilder,
|
KeystoreError, PlainText, Uuid, ValidatorKeystores, Wallet, WalletBuilder,
|
||||||
};
|
};
|
||||||
|
@ -285,3 +285,19 @@ pub fn recover_validator_secret(
|
|||||||
|
|
||||||
Ok((destination.secret().to_vec().into(), path))
|
Ok((destination.secret().to_vec().into(), path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `(secret, path)` for the `key_type` for the validator at `index`.
|
||||||
|
///
|
||||||
|
/// This function should only be used for key recovery since it can easily lead to key duplication.
|
||||||
|
pub fn recover_validator_secret_from_mnemonic(
|
||||||
|
secret: &[u8],
|
||||||
|
index: u32,
|
||||||
|
key_type: KeyType,
|
||||||
|
) -> Result<(PlainText, ValidatorPath), Error> {
|
||||||
|
let path = ValidatorPath::new(index, key_type);
|
||||||
|
let master = DerivedKey::from_seed(secret).map_err(|()| Error::EmptyPassword)?;
|
||||||
|
|
||||||
|
let destination = path.iter_nodes().fold(master, |dk, i| dk.child(*i));
|
||||||
|
|
||||||
|
Ok((destination.secret().to_vec().into(), path))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user