From c1b541867e5c4aa16ecc68199ffebb26ad4cdc4b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 18 Nov 2019 18:07:59 +1100 Subject: [PATCH] Add progress on validator onboarding --- account_manager/Cargo.toml | 7 + account_manager/src/lib.rs | 44 ++++ account_manager/src/validator.rs | 321 +++++++++++++++++++++++++ eth2/utils/deposit_contract/src/lib.rs | 6 +- 4 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 account_manager/src/validator.rs diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 2838b242de..6ec3390bf2 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -4,6 +4,9 @@ version = "0.0.1" authors = ["Luke Anderson "] edition = "2018" +[dev-dependencies] +tempdir = "0.3" + [dependencies] bls = { path = "../eth2/utils/bls" } clap = "2.33.0" @@ -14,3 +17,7 @@ validator_client = { path = "../validator_client" } types = { path = "../eth2/types" } dirs = "2.0.2" environment = { path = "../lighthouse/environment" } +deposit_contract = { path = "../eth2/utils/deposit_contract" } +libc = "0.2.65" +eth2_ssz = { path = "../eth2/utils/ssz" } +eth2_ssz_derive = { path = "../eth2/utils/ssz_derive" } diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 90a80e6dd7..8687e3d09c 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -1,4 +1,5 @@ mod cli; +pub mod validator; use bls::Keypair; use clap::ArgMatches; @@ -15,6 +16,49 @@ pub const DEFAULT_DATA_DIR: &str = ".lighthouse-validator"; pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml"; pub fn run(matches: &ArgMatches, context: RuntimeContext) { + let log = context.log.clone(); + match run_account_manager(matches, context) { + Ok(()) => (), + Err(e) => crit!(log, "Account manager failed"; "error" => e), + } +} + +fn run_account_manager( + matches: &ArgMatches, + context: RuntimeContext, +) -> Result<(), String> { + let log = context.log.clone(); + + let data_dir = matches + .value_of("datadir") + .map(PathBuf::from) + .unwrap_or_else(|| { + let mut default_dir = match dirs::home_dir() { + Some(v) => v, + None => { + panic!("Failed to find a home directory"); + } + }; + default_dir.push(DEFAULT_DATA_DIR); + default_dir + }); + + fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to initialize data dir: {}", e))?; + + let mut client_config = ValidatorClientConfig::default(); + client_config.data_dir = data_dir.clone(); + client_config + .apply_cli_args(&matches, &log) + .map_err(|e| format!("Failed to parse ClientConfig CLI arguments: {:?}", e))?; + + info!(log, "Located data directory"; + "path" => &client_config.data_dir.to_str()); + + panic!() + // +} + +pub fn run_old(matches: &ArgMatches, context: RuntimeContext) { let mut log = context.log; let data_dir = match matches diff --git a/account_manager/src/validator.rs b/account_manager/src/validator.rs new file mode 100644 index 0000000000..6a68a98725 --- /dev/null +++ b/account_manager/src/validator.rs @@ -0,0 +1,321 @@ +use bls::get_withdrawal_credentials; +use deposit_contract::eth1_tx_data; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::fs; +use std::fs::File; +use std::io::prelude::*; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use types::{ + test_utils::generate_deterministic_keypair, ChainSpec, DepositData, Hash256, Keypair, + PublicKey, SecretKey, Signature, +}; + +const VOTING_KEY_PREFIX: &str = "voting"; +const WITHDRAWAL_KEY_PREFIX: &str = "withdrawal"; +const ETH1_DEPOSIT_DATA_FILE: &str = "eth1_deposit_data_{}.rlp"; + +fn keypair_file(prefix: &str) -> String { + format!("{}_keypair", prefix) +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ValidatorDirectory { + pub directory: PathBuf, + pub voting_keypair: Option, + pub withdrawal_keypair: Option, + pub deposit_data: Option>, +} + +impl ValidatorDirectory { + /// Attempts to load a validator from the given directory, requiring only components necessary + /// for signing messages. + pub fn load_for_signing(directory: PathBuf) -> Result { + if !directory.exists() { + return Err(format!( + "Validator directory does not exist: {:?}", + directory + )); + } + + Ok(Self { + voting_keypair: Some( + load_keypair(directory.clone(), VOTING_KEY_PREFIX) + .map_err(|e| format!("Unable to get voting keypair: {}", e))?, + ), + withdrawal_keypair: load_keypair(directory.clone(), WITHDRAWAL_KEY_PREFIX).ok(), + deposit_data: load_eth1_deposit_data(directory.clone()).ok(), + directory, + }) + } +} + +fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result { + let path = base_path.join(keypair_file(file_prefix)); + + if !path.exists() { + return Err(format!("Keypair file does not exist: {:?}", path)); + } + + let mut bytes = vec![]; + + File::open(&path) + .map_err(|e| format!("Unable to open keypair file: {}", e))? + .read_to_end(&mut bytes) + .map_err(|e| format!("Unable to read keypair file: {}", e))?; + + SszEncodableKeypair::from_ssz_bytes(&bytes) + .map(Into::into) + .map_err(|e| format!("Unable to decode keypair: {:?}", e)) +} + +fn load_eth1_deposit_data(base_path: PathBuf) -> Result, String> { + let path = base_path.join(ETH1_DEPOSIT_DATA_FILE); + + if !path.exists() { + return Err(format!("Eth1 deposit data file does not exist: {:?}", path)); + } + + let mut bytes = vec![]; + + File::open(&path) + .map_err(|e| format!("Unable to open eth1 deposit data file: {}", e))? + .read_to_end(&mut bytes) + .map_err(|e| format!("Unable to read eth1 deposit data file: {}", e))?; + + Ok(bytes) +} + +#[derive(Encode, Decode)] +struct SszEncodableKeypair { + pk: PublicKey, + sk: SecretKey, +} + +impl Into for SszEncodableKeypair { + fn into(self) -> Keypair { + Keypair { + sk: self.sk, + pk: self.pk, + } + } +} + +impl From for SszEncodableKeypair { + fn from(kp: Keypair) -> Self { + Self { + sk: kp.sk, + pk: kp.pk, + } + } +} + +#[derive(Default)] +pub struct ValidatorDirectoryBuilder { + directory: Option, + voting_keypair: Option, + withdrawal_keypair: Option, + amount: Option, + deposit_data: Option>, + spec: Option, +} + +impl ValidatorDirectoryBuilder { + pub fn spec(mut self, spec: ChainSpec) -> Self { + self.spec = Some(spec); + self + } + + pub fn full_deposit_amount(mut self) -> Result { + let spec = self + .spec + .as_ref() + .ok_or_else(|| "full_deposit_amount requires a spec")?; + self.amount = Some(spec.max_effective_balance); + Ok(self) + } + + pub fn custom_deposit_amount(mut self, gwei: u64) -> Self { + self.amount = Some(gwei); + self + } + + pub fn random_keypairs(mut self) -> Self { + self.voting_keypair = Some(Keypair::random()); + self.withdrawal_keypair = Some(Keypair::random()); + self + } + + pub fn deterministic_keypairs(mut self, index: usize) -> Self { + let keypair = generate_deterministic_keypair(index); + self.voting_keypair = Some(keypair.clone()); + self.withdrawal_keypair = Some(keypair); + self + } + + /// Creates a validator directory in the given `base_path` (e.g., `~/.lighthouse/validators/`). + pub fn create_directory(mut self, base_path: PathBuf) -> Result { + let voting_keypair = self + .voting_keypair + .as_ref() + .ok_or_else(|| "directory requires a voting_keypair")?; + + let directory = base_path.join(voting_keypair.identifier()); + + if directory.exists() { + return Err(format!( + "Validator directory already exists: {:?}", + directory + )); + } + + fs::create_dir_all(&directory) + .map_err(|e| format!("Unable to create validator directory: {}", e))?; + + self.directory = Some(directory); + + Ok(self) + } + + pub fn write_keypair_files(self) -> Result { + let voting_keypair = self + .voting_keypair + .clone() + .ok_or_else(|| "build requires a voting_keypair")?; + let withdrawal_keypair = self + .withdrawal_keypair + .clone() + .ok_or_else(|| "build requires a withdrawal_keypair")?; + + self.save_keypair(voting_keypair, VOTING_KEY_PREFIX)?; + self.save_keypair(withdrawal_keypair, WITHDRAWAL_KEY_PREFIX)?; + Ok(self) + } + + fn save_keypair(&self, keypair: Keypair, file_prefix: &str) -> Result<(), String> { + let path = self + .directory + .as_ref() + .map(|directory| directory.join(keypair_file(file_prefix))) + .ok_or_else(|| "save_keypair requires a directory")?; + + if path.exists() { + return Err(format!("Keypair file already exists at: {:?}", path)); + } + + let mut file = File::create(&path).map_err(|e| format!("Unable to create file: {}", e))?; + + // Ensure file has correct permissions. + let mut perm = file + .metadata() + .map_err(|e| format!("Unable to get file metadata: {}", e))? + .permissions(); + perm.set_mode((libc::S_IWUSR | libc::S_IRUSR) as u32); + file.set_permissions(perm) + .map_err(|e| format!("Unable to set file permissions: {}", e))?; + + file.write_all(&SszEncodableKeypair::from(keypair).as_ssz_bytes()) + .map_err(|e| format!("Unable to write keypair to file: {}", e))?; + + Ok(()) + } + + pub fn write_eth1_data_file(mut self) -> Result { + let voting_keypair = self + .voting_keypair + .as_ref() + .ok_or_else(|| "write_eth1_data_file requires a voting_keypair")?; + let withdrawal_keypair = self + .withdrawal_keypair + .as_ref() + .ok_or_else(|| "write_eth1_data_file requires a withdrawal_keypair")?; + let amount = self + .amount + .ok_or_else(|| "write_eth1_data_file requires an amount")?; + let spec = self.spec.as_ref().ok_or_else(|| "build requires a spec")?; + let path = self + .directory + .as_ref() + .map(|directory| directory.join(ETH1_DEPOSIT_DATA_FILE)) + .ok_or_else(|| "write_eth1_data_filer requires a directory")?; + + let deposit_data = { + let withdrawal_credentials = Hash256::from_slice(&get_withdrawal_credentials( + &withdrawal_keypair.pk, + spec.bls_withdrawal_prefix_byte, + )); + + let mut deposit_data = DepositData { + pubkey: voting_keypair.pk.clone().into(), + withdrawal_credentials, + amount, + signature: Signature::empty_signature().into(), + }; + + deposit_data.signature = deposit_data.create_signature(&voting_keypair.sk, &spec); + + eth1_tx_data(&deposit_data) + .map_err(|e| format!("Unable to encode eth1 deposit tx data: {:?}", e))? + }; + + if path.exists() { + return Err(format!("Eth1 data file already exists at: {:?}", path)); + } + + File::create(&path) + .map_err(|e| format!("Unable to create file: {}", e))? + .write_all(&deposit_data) + .map_err(|e| format!("Unable to write eth1 data file: {}", e))?; + + self.deposit_data = Some(deposit_data); + + Ok(self) + } + + pub fn build(self) -> Result { + Ok(ValidatorDirectory { + directory: self.directory.ok_or_else(|| "build requires a directory")?, + voting_keypair: self.voting_keypair, + withdrawal_keypair: self.withdrawal_keypair, + deposit_data: self.deposit_data, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + use types::{EthSpec, MinimalEthSpec}; + + type E = MinimalEthSpec; + + #[test] + fn round_trip() { + let spec = E::default_spec(); + let temp_dir = TempDir::new("acc_manager").expect("should create test dir"); + + let created_dir = ValidatorDirectoryBuilder::default() + .spec(spec) + .full_deposit_amount() + .expect("should set full deposit amount") + .deterministic_keypairs(42) + .create_directory(temp_dir.path().into()) + .expect("should create directory") + .write_keypair_files() + .expect("should write keypair files") + .write_eth1_data_file() + .expect("should write eth1 data file") + .build() + .expect("should build dir"); + + let loaded_dir = ValidatorDirectory::load_for_signing(created_dir.directory.clone()) + .expect("should load directory"); + + assert_eq!( + created_dir, loaded_dir, + "the directory created should match the one loaded" + ); + } +} diff --git a/eth2/utils/deposit_contract/src/lib.rs b/eth2/utils/deposit_contract/src/lib.rs index f9f0a3b203..c0bfef7dca 100644 --- a/eth2/utils/deposit_contract/src/lib.rs +++ b/eth2/utils/deposit_contract/src/lib.rs @@ -1,6 +1,6 @@ use ethabi::{Contract, Token}; use ssz::Encode; -use types::{ChainSpec, DepositData, SecretKey}; +use types::{DepositData, SecretKey}; pub use ethabi::Error; @@ -25,8 +25,8 @@ pub fn eth1_tx_data(deposit_data: &DepositData) -> Result, Error> { mod tests { use super::*; use types::{ - test_utils::generate_deterministic_keypair, EthSpec, Hash256, Keypair, MinimalEthSpec, - Signature, + test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Hash256, Keypair, + MinimalEthSpec, Signature, }; type E = MinimalEthSpec;