diff --git a/Cargo.lock b/Cargo.lock index e1555410d..7b4bb8a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "directory", "dirs 3.0.1", "environment", + "eth2", "eth2_keystore", "eth2_ssz", "eth2_ssz_derive", @@ -23,10 +24,13 @@ dependencies = [ "libc", "rand 0.7.3", "rayon", + "safe_arith", "slashing_protection", "slog", "slog-async", "slog-term", + "slot_clock", + "tempfile", "tokio 0.2.22", "types", "validator_dir", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 4145b19dc..4875b99ea 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -31,3 +31,9 @@ tokio = { version = "0.2.22", features = ["full"] } eth2_keystore = { path = "../crypto/eth2_keystore" } account_utils = { path = "../common/account_utils" } slashing_protection = { path = "../validator_client/slashing_protection" } +eth2 = {path = "../common/eth2"} +safe_arith = {path = "../consensus/safe_arith"} +slot_clock = { path = "../common/slot_clock" } + +[dev-dependencies] +tempfile = "3.1.0" diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 863cbf1c0..a779a2881 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -140,6 +140,7 @@ pub fn cli_run( ensure_dir_exists(&validator_dir)?; ensure_dir_exists(&secrets_dir)?; + eprintln!("validator-dir path: {:?}", validator_dir); eprintln!("secrets-dir path {:?}", secrets_dir); eprintln!("wallets-dir path {:?}", wallet_base_dir); diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs new file mode 100644 index 000000000..492081afb --- /dev/null +++ b/account_manager/src/validator/exit.rs @@ -0,0 +1,348 @@ +use crate::wallet::create::STDIN_INPUTS_FLAG; +use bls::{Keypair, PublicKey}; +use clap::{App, Arg, ArgMatches}; +use environment::Environment; +use eth2::{ + types::{GenesisData, StateId, ValidatorId, ValidatorStatus}, + BeaconNodeHttpClient, Url, +}; +use eth2_keystore::Keystore; +use eth2_testnet_config::Eth2TestnetConfig; +use safe_arith::SafeArith; +use slot_clock::{SlotClock, SystemTimeSlotClock}; +use std::path::PathBuf; +use std::time::Duration; +use types::{ChainSpec, Epoch, EthSpec, Fork, VoluntaryExit}; + +pub const CMD: &str = "exit"; +pub const KEYSTORE_FLAG: &str = "keystore"; +pub const PASSWORD_FILE_FLAG: &str = "password-file"; +pub const BEACON_SERVER_FLAG: &str = "beacon-node"; +pub const PASSWORD_PROMPT: &str = "Enter the keystore password"; + +pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; +pub const CONFIRMATION_PHRASE: &str = "Exit my validator"; +pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/voluntary-exit.html"; +pub const PROMPT: &str = "WARNING: WITHDRAWING STAKED ETH IS NOT CURRENTLY POSSIBLE"; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new("exit") + .about("Submits a VoluntaryExit to the beacon chain for a given validator keystore.") + .arg( + Arg::with_name(KEYSTORE_FLAG) + .long(KEYSTORE_FLAG) + .value_name("KEYSTORE_PATH") + .help("The path to the EIP-2335 voting keystore for the validator") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name(PASSWORD_FILE_FLAG) + .long(PASSWORD_FILE_FLAG) + .value_name("PASSWORD_FILE_PATH") + .help("The path to the password file which unlocks the validator voting keystore") + .takes_value(true), + ) + .arg( + Arg::with_name(BEACON_SERVER_FLAG) + .long(BEACON_SERVER_FLAG) + .value_name("NETWORK_ADDRESS") + .help("Address to a beacon node HTTP API") + .default_value(&DEFAULT_BEACON_NODE) + .takes_value(true), + ) + .arg( + Arg::with_name(STDIN_INPUTS_FLAG) + .long(STDIN_INPUTS_FLAG) + .help("If present, read all user inputs from stdin instead of tty."), + ) +} + +pub fn cli_run(matches: &ArgMatches, mut env: Environment) -> Result<(), String> { + let keystore_path: PathBuf = clap_utils::parse_required(matches, KEYSTORE_FLAG)?; + let password_file_path: Option = + clap_utils::parse_optional(matches, PASSWORD_FILE_FLAG)?; + let stdin_inputs = matches.is_present(STDIN_INPUTS_FLAG); + + let spec = env.eth2_config().spec.clone(); + let server_url: String = clap_utils::parse_required(matches, BEACON_SERVER_FLAG)?; + let client = BeaconNodeHttpClient::new( + Url::parse(&server_url) + .map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?, + ); + + let testnet_config = env + .testnet + .clone() + .expect("network should have a valid config"); + + env.runtime().block_on(publish_voluntary_exit::( + &keystore_path, + password_file_path.as_ref(), + &client, + &spec, + stdin_inputs, + &testnet_config, + ))?; + + Ok(()) +} + +/// Gets the keypair and validator_index for every validator and calls `publish_voluntary_exit` on it. +async fn publish_voluntary_exit( + keystore_path: &PathBuf, + password_file_path: Option<&PathBuf>, + client: &BeaconNodeHttpClient, + spec: &ChainSpec, + stdin_inputs: bool, + testnet_config: &Eth2TestnetConfig, +) -> Result<(), String> { + let genesis_data = get_geneisis_data(client).await?; + let testnet_genesis_root = testnet_config + .beacon_state::() + .as_ref() + .expect("network should have valid genesis state") + .genesis_validators_root; + + // Verify that the beacon node and validator being exited are on the same network. + if genesis_data.genesis_validators_root != testnet_genesis_root { + return Err( + "Invalid genesis state. Please ensure that your beacon node is on the same network \ + as the validator you are publishing an exit for" + .to_string(), + ); + } + + // Return immediately if beacon node is not synced + if is_syncing(client).await? { + return Err("Beacon node is still syncing".to_string()); + } + + let keypair = load_voting_keypair(keystore_path, password_file_path, stdin_inputs)?; + + let epoch = get_current_epoch::(genesis_data.genesis_time, spec) + .ok_or_else(|| "Failed to get current epoch. Please check your system time".to_string())?; + let validator_index = get_validator_index_for_exit(client, &keypair.pk, epoch, spec).await?; + + let fork = get_beacon_state_fork(client).await?; + let voluntary_exit = VoluntaryExit { + epoch, + validator_index, + }; + + eprintln!( + "Publishing a voluntary exit for validator: {} \n", + keypair.pk + ); + eprintln!("WARNING: THIS IS AN IRREVERSIBLE OPERATION\n"); + eprintln!("{}\n", PROMPT); + eprintln!( + "PLEASE VISIT {} TO MAKE SURE YOU UNDERSTAND THE IMPLICATIONS OF A VOLUNTARY EXIT.", + WEBSITE_URL + ); + eprintln!("Enter the exit phrase from the above URL to confirm the voluntary exit: "); + + let confirmation = account_utils::read_input_from_user(stdin_inputs)?; + if confirmation == CONFIRMATION_PHRASE { + // Sign and publish the voluntary exit to network + let signed_voluntary_exit = voluntary_exit.sign( + &keypair.sk, + &fork, + genesis_data.genesis_validators_root, + spec, + ); + client + .post_beacon_pool_voluntary_exits(&signed_voluntary_exit) + .await + .map_err(|e| format!("Failed to publish voluntary exit: {}", e))?; + tokio::time::delay_for(std::time::Duration::from_secs(1)).await; // Provides nicer UX. + eprintln!( + "Successfully validated and published voluntary exit for validator {}", + keypair.pk + ); + } else { + eprintln!( + "Did not publish voluntary exit for validator {}. Please check that you entered the correct exit phrase.", + keypair.pk + ); + } + + Ok(()) +} + +/// Get the validator index of a given the validator public key by querying the beacon node endpoint. +/// +/// Returns an error if the beacon endpoint returns an error or given validator is not eligible for an exit. +async fn get_validator_index_for_exit( + client: &BeaconNodeHttpClient, + validator_pubkey: &PublicKey, + epoch: Epoch, + spec: &ChainSpec, +) -> Result { + let validator_data = client + .get_beacon_states_validator_id( + StateId::Head, + &ValidatorId::PublicKey(validator_pubkey.into()), + ) + .await + .map_err(|e| format!("Failed to get validator details: {:?}", e))? + .ok_or_else(|| { + format!( + "Validator {} is not present in the beacon state. \ + Please ensure that your beacon node is synced and the validator has been deposited.", + validator_pubkey + ) + })? + .data; + + match validator_data.status { + ValidatorStatus::Active => { + let eligible_epoch = validator_data + .validator + .activation_epoch + .safe_add(spec.shard_committee_period) + .map_err(|e| format!("Failed to calculate eligible epoch, validator activation epoch too high: {:?}", e))?; + + if epoch >= eligible_epoch { + Ok(validator_data.index) + } else { + Err(format!( + "Validator {:?} is not eligible for exit. It will become eligible on epoch {}", + validator_pubkey, eligible_epoch + )) + } + } + status => Err(format!( + "Validator {:?} is not eligible for voluntary exit. Validator status: {:?}", + validator_pubkey, status + )), + } +} + +/// Get genesis data by querying the beacon node client. +async fn get_geneisis_data(client: &BeaconNodeHttpClient) -> Result { + Ok(client + .get_beacon_genesis() + .await + .map_err(|e| format!("Failed to get beacon genesis: {}", e))? + .data) +} + +/// Gets syncing status from beacon node client and returns true if syncing and false otherwise. +async fn is_syncing(client: &BeaconNodeHttpClient) -> Result { + Ok(client + .get_node_syncing() + .await + .map_err(|e| format!("Failed to get sync status: {:?}", e))? + .data + .is_syncing) +} + +/// Get fork object for the current state by querying the beacon node client. +async fn get_beacon_state_fork(client: &BeaconNodeHttpClient) -> Result { + Ok(client + .get_beacon_states_fork(StateId::Head) + .await + .map_err(|e| format!("Failed to get get fork: {:?}", e))? + .ok_or_else(|| "Failed to get fork, state not found".to_string())? + .data) +} + +/// Calculates the current epoch from the genesis time and current time. +fn get_current_epoch(genesis_time: u64, spec: &ChainSpec) -> Option { + let slot_clock = SystemTimeSlotClock::new( + spec.genesis_slot, + Duration::from_secs(genesis_time), + Duration::from_millis(spec.milliseconds_per_slot), + ); + slot_clock.now().map(|s| s.epoch(E::slots_per_epoch())) +} + +/// Load the voting keypair by loading and decrypting the keystore. +/// +/// If the `password_file_path` is Some, unlock keystore using password in given file +/// otherwise, prompts user for a password to unlock the keystore. +fn load_voting_keypair( + voting_keystore_path: &PathBuf, + password_file_path: Option<&PathBuf>, + stdin_inputs: bool, +) -> Result { + let keystore = Keystore::from_json_file(&voting_keystore_path).map_err(|e| { + format!( + "Unable to read keystore JSON {:?}: {:?}", + voting_keystore_path, e + ) + })?; + + // Get password from password file. + if let Some(password_file) = password_file_path { + validator_dir::unlock_keypair_from_password_path(voting_keystore_path, password_file) + .map_err(|e| format!("Error while decrypting keypair: {:?}", e)) + } else { + // Prompt password from user. + eprintln!(""); + eprintln!( + "{} for validator in {:?}: ", + PASSWORD_PROMPT, voting_keystore_path + ); + let password = account_utils::read_password_from_user(stdin_inputs)?; + match keystore.decrypt_keypair(password.as_ref()) { + Ok(keypair) => { + eprintln!("Password is correct."); + eprintln!(""); + std::thread::sleep(std::time::Duration::from_secs(1)); // Provides nicer UX. + Ok(keypair) + } + Err(eth2_keystore::Error::InvalidPassword) => Err("Invalid password".to_string()), + Err(e) => Err(format!("Error while decrypting keypair: {:?}", e)), + } + } +} + +#[cfg(test)] +#[cfg(not(debug_assertions))] +mod tests { + use super::*; + use eth2_keystore::KeystoreBuilder; + use std::fs::File; + use std::io::Write; + use tempfile::{tempdir, TempDir}; + + const PASSWORD: &str = "cats"; + const KEYSTORE_NAME: &str = "keystore-m_12381_3600_0_0_0-1595406747.json"; + const PASSWORD_FILE: &str = "password.pass"; + + fn create_and_save_keystore(dir: &TempDir, save_password: bool) -> PublicKey { + let keypair = Keypair::random(); + let keystore = KeystoreBuilder::new(&keypair, PASSWORD.as_bytes(), "".into()) + .unwrap() + .build() + .unwrap(); + + // Create a keystore. + File::create(dir.path().join(KEYSTORE_NAME)) + .map(|mut file| keystore.to_json_writer(&mut file).unwrap()) + .unwrap(); + if save_password { + File::create(dir.path().join(PASSWORD_FILE)) + .map(|mut file| file.write_all(PASSWORD.as_bytes()).unwrap()) + .unwrap(); + } + keystore.public_key().unwrap() + } + + #[test] + fn test_load_keypair_password_file() { + let dir = tempdir().unwrap(); + let expected_pk = create_and_save_keystore(&dir, true); + + let kp = load_voting_keypair( + &dir.path().join(KEYSTORE_NAME), + Some(&dir.path().join(PASSWORD_FILE)), + false, + ) + .unwrap(); + + assert_eq!(expected_pk, kp.pk.into()); + } +} diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 8b4a216e8..09459c578 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -86,6 +86,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin ) })?; + eprintln!("validator-dir path: {:?}", validator_dir); // Collect the paths for the keystores that should be imported. let keystore_paths = match (keystore, keystores_dir) { (Some(keystore), None) => vec![keystore], diff --git a/account_manager/src/validator/list.rs b/account_manager/src/validator/list.rs index dd97de156..600fa420f 100644 --- a/account_manager/src/validator/list.rs +++ b/account_manager/src/validator/list.rs @@ -10,6 +10,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { } pub fn cli_run(validator_dir: PathBuf) -> Result<(), String> { + eprintln!("validator-dir path: {:?}", validator_dir); let mgr = ValidatorManager::open(&validator_dir) .map_err(|e| format!("Unable to read --{}: {:?}", VALIDATOR_DIR_FLAG, e))?; diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index 6ac22093f..859b39a1f 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -1,4 +1,5 @@ pub mod create; +pub mod exit; pub mod import; pub mod list; pub mod recover; @@ -32,6 +33,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(list::cli_app()) .subcommand(recover::cli_app()) .subcommand(slashing_protection::cli_app()) + .subcommand(exit::cli_app()) } pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { @@ -51,6 +53,7 @@ pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result< (slashing_protection::CMD, Some(matches)) => { slashing_protection::cli_run(matches, env, validator_base_dir) } + (exit::CMD, Some(matches)) => exit::cli_run(matches, env), (unknown, _) => Err(format!( "{} does not have a {} command. See --help", CMD, unknown diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index ecea0efbd..b595cc640 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -88,6 +88,9 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; let stdin_inputs = matches.is_present(STDIN_INPUTS_FLAG); + eprintln!("validator-dir path: {:?}", validator_dir); + eprintln!("secrets-dir path: {:?}", secrets_dir); + ensure_dir_exists(&validator_dir)?; ensure_dir_exists(&secrets_dir)?; diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index 5ba5352c9..e80b81900 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -44,6 +44,7 @@ pub fn cli_run( env: Environment, validator_base_dir: PathBuf, ) -> Result<(), String> { + eprintln!("validator-dir path: {:?}", validator_base_dir); let slashing_protection_db_path = validator_base_dir.join(SLASHING_PROTECTION_FILENAME); let testnet_config = env diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 1b7ed6612..82046da68 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -15,6 +15,7 @@ * [Validator Management](./validator-management.md) * [Importing from the Eth2 Launchpad](./validator-import-launchpad.md) * [Slashing Protection](./slashing-protection.md) + * [Voluntary Exits](./voluntary-exit.md) * [APIs](./api.md) * [Beacon Node API](./api-bn.md) * [/lighthouse](./api-lighthouse.md) diff --git a/book/src/voluntary-exit.md b/book/src/voluntary-exit.md new file mode 100644 index 000000000..69e3da5ca --- /dev/null +++ b/book/src/voluntary-exit.md @@ -0,0 +1,68 @@ +# Voluntary exits + +A validator may chose to voluntarily stop performing duties (proposing blocks and attesting to blocks) by submitting +a voluntary exit transaction to the beacon chain. + +A validator can initiate a voluntary exit provided that the validator is currently active, has not been slashed and has been active for at least 256 epochs (~27 hours) since it has been activated. + +> Note: After initiating a voluntary exit, the validator will have to keep performing duties until it has successfully exited to avoid penalties. + +It takes at a minimum 5 epochs (32 minutes) for a validator to exit after initiating a voluntary exit. +This number can be much higher depending on how many other validators are queued to exit. + +## Withdrawal of exited funds + +Even though users can perform a voluntary exit in phase 0, they **cannot withdraw their exited funds at this point in time**. +This implies that the staked funds are effectively **frozen** until withdrawals are enabled in future phases. + +To understand the phased rollout strategy for Eth2, please visit . + + + +## Initiating a voluntary exit + +In order to initiate an exit, users can use the `lighthouse account validator exit` command. + +- The `--keystore` flag is used to specify the path to the EIP-2335 voting keystore for the validator. + +- The `--beacon-node` flag is used to specify a beacon chain HTTP endpoint that confirms to the [Eth2.0 Standard API](https://ethereum.github.io/eth2.0-APIs/) specifications. That beacon node will be used to validate and propagate the voluntary exit. The default value for this flag is `http://localhost:5052`. + +- The `--testnet` flag is used to specify a particular testnet (default is `medalla`). + +- The `--password-file` flag is used to specify the path to the file containing the password for the voting keystore. If this flag is not provided, the user will be prompted to enter the password. + + +After validating the password, the user will be prompted to enter a special exit phrase as a final confirmation after which the voluntary exit will be published to the beacon chain. + +The exit phrase is the following: +> Exit my validator + + + +Below is an example for initiating a voluntary exit on the zinken testnet. + +``` +$ lighthouse --testnet zinken account validator exit --keystore /path/to/keystore --beacon-node http://localhost:5052 + +Running account manager for zinken testnet +validator-dir path: ~/.lighthouse/zinken/validators + +Enter the keystore password for validator in 0xabcd + +Password is correct + +Publishing a voluntary exit for validator 0xabcd + +WARNING: WARNING: THIS IS AN IRREVERSIBLE OPERATION + +WARNING: WITHDRAWING STAKED ETH WILL NOT BE POSSIBLE UNTIL ETH1/ETH2 MERGE. + +PLEASE VISIT https://lighthouse-book.sigmaprime.io/voluntary-exit.html +TO MAKE SURE YOU UNDERSTAND THE IMPLICATIONS OF A VOLUNTARY EXIT. + +Enter the exit phrase from the above URL to confirm the voluntary exit: +Exit my validator + +Successfully published voluntary exit for validator 0xabcd +``` + diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 11cfa1c14..51552c930 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -290,7 +290,7 @@ pub fn recursively_find_voting_keystores>( } /// Returns `true` if we should consider the `file_name` to represent a voting keystore. -fn is_voting_keystore(file_name: &str) -> bool { +pub fn is_voting_keystore(file_name: &str) -> bool { // All formats end with `.json`. if !file_name.ends_with(".json") { return false; diff --git a/common/validator_dir/src/lib.rs b/common/validator_dir/src/lib.rs index 90def3d63..875901ded 100644 --- a/common/validator_dir/src/lib.rs +++ b/common/validator_dir/src/lib.rs @@ -12,7 +12,10 @@ pub mod insecure_keys; mod manager; mod validator_dir; -pub use crate::validator_dir::{Error, Eth1DepositData, ValidatorDir, ETH1_DEPOSIT_TX_HASH_FILE}; +pub use crate::validator_dir::{ + unlock_keypair_from_password_path, Error, Eth1DepositData, ValidatorDir, + ETH1_DEPOSIT_TX_HASH_FILE, +}; pub use builder::{ 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 109566f66..26783de01 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -143,7 +143,7 @@ impl ValidatorDir { /// /// If there is a filesystem error, a password is missing or the password is incorrect. pub fn voting_keypair>(&self, password_dir: P) -> Result { - unlock_keypair(&self.dir.clone(), VOTING_KEYSTORE_FILE, password_dir) + unlock_keypair(&self.dir.join(VOTING_KEYSTORE_FILE), password_dir) } /// Attempts to read the keystore in `self.dir` and decrypt the keypair using a password file @@ -155,7 +155,7 @@ impl ValidatorDir { /// /// If there is a file-system error, a password is missing or the password is incorrect. pub fn withdrawal_keypair>(&self, password_dir: P) -> Result { - unlock_keypair(&self.dir.clone(), WITHDRAWAL_KEYSTORE_FILE, password_dir) + unlock_keypair(&self.dir.join(WITHDRAWAL_KEYSTORE_FILE), password_dir) } /// Indicates if there is a file containing an eth1 deposit transaction. This can be used to @@ -250,17 +250,16 @@ impl Drop for ValidatorDir { } } -/// Attempts to load and decrypt a keystore. -fn unlock_keypair>( - keystore_dir: &PathBuf, - filename: &str, +/// Attempts to load and decrypt a Keypair given path to the keystore. +pub fn unlock_keypair>( + keystore_path: &PathBuf, password_dir: P, ) -> Result { let keystore = Keystore::from_json_reader( &mut OpenOptions::new() .read(true) .create(false) - .open(keystore_dir.clone().join(filename)) + .open(keystore_path) .map_err(Error::UnableToOpenKeystore)?, ) .map_err(Error::UnableToReadKeystore)?; @@ -271,7 +270,28 @@ fn unlock_keypair>( let password: PlainText = read(&password_path) .map_err(|_| Error::UnableToReadPassword(password_path))? .into(); - + keystore + .decrypt_keypair(password.as_bytes()) + .map_err(Error::UnableToDecryptKeypair) +} + +/// Attempts to load and decrypt a Keypair given path to the keystore and the password file. +pub fn unlock_keypair_from_password_path( + keystore_path: &PathBuf, + password_path: &PathBuf, +) -> Result { + let keystore = Keystore::from_json_reader( + &mut OpenOptions::new() + .read(true) + .create(false) + .open(keystore_path) + .map_err(Error::UnableToOpenKeystore)?, + ) + .map_err(Error::UnableToReadKeystore)?; + + let password: PlainText = read(password_path) + .map_err(|_| Error::UnableToReadPassword(password_path.clone()))? + .into(); keystore .decrypt_keypair(password.as_bytes()) .map_err(Error::UnableToDecryptKeypair)