From cc7430d3cc25faaaadc4f214e084d2fc705b7c3f Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 13 Nov 2019 21:10:59 +1100 Subject: [PATCH 1/6] Move account_manager under `lighthouse` binary --- account_manager/Cargo.toml | 1 + account_manager/src/cli.rs | 54 +++++++++++++++++++ account_manager/src/{main.rs => lib.rs} | 71 ++++--------------------- lighthouse/Cargo.toml | 1 + lighthouse/src/main.rs | 11 ++++ 5 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 account_manager/src/cli.rs rename account_manager/src/{main.rs => lib.rs} (58%) diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index fc3df1e8da..2838b242de 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -13,3 +13,4 @@ slog-async = "2.3.0" validator_client = { path = "../validator_client" } types = { path = "../eth2/types" } dirs = "2.0.2" +environment = { path = "../lighthouse/environment" } diff --git a/account_manager/src/cli.rs b/account_manager/src/cli.rs new file mode 100644 index 0000000000..9eded83cc5 --- /dev/null +++ b/account_manager/src/cli.rs @@ -0,0 +1,54 @@ +use clap::{App, Arg, SubCommand}; + +pub fn cli_app<'a, 'b>() -> App<'a, 'b> { + App::new("Account Manager") + .visible_aliases(&["am", "accounts", "accounts_manager"]) + .version("0.0.1") + .author("Sigma Prime ") + .about("Eth 2.0 Accounts Manager") + .arg( + Arg::with_name("logfile") + .long("logfile") + .value_name("logfile") + .help("File path where output will be written.") + .takes_value(true), + ) + .arg( + Arg::with_name("datadir") + .long("datadir") + .short("d") + .value_name("DIR") + .help("Data directory for keys and databases.") + .takes_value(true), + ) + .subcommand( + SubCommand::with_name("generate") + .about("Generates a new validator private key") + .version("0.0.1") + .author("Sigma Prime "), + ) + .subcommand( + SubCommand::with_name("generate_deterministic") + .about("Generates a deterministic validator private key FOR TESTING") + .version("0.0.1") + .author("Sigma Prime ") + .arg( + Arg::with_name("validator index") + .long("index") + .short("i") + .value_name("index") + .help("The index of the validator, for which the test key is generated") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("validator count") + .long("validator_count") + .short("n") + .value_name("validator_count") + .help("If supplied along with `index`, generates keys `i..i + n`.") + .takes_value(true) + .default_value("1"), + ), + ) +} diff --git a/account_manager/src/main.rs b/account_manager/src/lib.rs similarity index 58% rename from account_manager/src/main.rs rename to account_manager/src/lib.rs index 095f65cbc5..90a80e6dd7 100644 --- a/account_manager/src/main.rs +++ b/account_manager/src/lib.rs @@ -1,72 +1,21 @@ +mod cli; + use bls::Keypair; -use clap::{App, Arg, SubCommand}; -use slog::{crit, debug, info, o, Drain}; +use clap::ArgMatches; +use environment::RuntimeContext; +use slog::{crit, debug, info}; use std::fs; use std::path::PathBuf; -use types::test_utils::generate_deterministic_keypair; +use types::{test_utils::generate_deterministic_keypair, EthSpec}; use validator_client::Config as ValidatorClientConfig; +pub use cli::cli_app; + pub const DEFAULT_DATA_DIR: &str = ".lighthouse-validator"; pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml"; -fn main() { - // Logging - let decorator = slog_term::TermDecorator::new().build(); - let drain = slog_term::CompactFormat::new(decorator).build().fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - let mut log = slog::Logger::root(drain, o!()); - - // CLI - let matches = App::new("Lighthouse Accounts Manager") - .version("0.0.1") - .author("Sigma Prime ") - .about("Eth 2.0 Accounts Manager") - .arg( - Arg::with_name("logfile") - .long("logfile") - .value_name("logfile") - .help("File path where output will be written.") - .takes_value(true), - ) - .arg( - Arg::with_name("datadir") - .long("datadir") - .short("d") - .value_name("DIR") - .help("Data directory for keys and databases.") - .takes_value(true), - ) - .subcommand( - SubCommand::with_name("generate") - .about("Generates a new validator private key") - .version("0.0.1") - .author("Sigma Prime "), - ) - .subcommand( - SubCommand::with_name("generate_deterministic") - .about("Generates a deterministic validator private key FOR TESTING") - .version("0.0.1") - .author("Sigma Prime ") - .arg( - Arg::with_name("validator index") - .long("index") - .short("i") - .value_name("index") - .help("The index of the validator, for which the test key is generated") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("validator count") - .long("validator_count") - .short("n") - .value_name("validator_count") - .help("If supplied along with `index`, generates keys `i..i + n`.") - .takes_value(true) - .default_value("1"), - ), - ) - .get_matches(); +pub fn run(matches: &ArgMatches, context: RuntimeContext) { + let mut log = context.log; let data_dir = match matches .value_of("datadir") diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 25a41ea6a9..ff6cc054bd 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -18,3 +18,4 @@ slog-async = "^2.3.0" environment = { path = "./environment" } futures = "0.1.25" validator_client = { "path" = "../validator_client" } +account_manager = { "path" = "../account_manager" } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 9125e9802a..7da8f991b4 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -53,6 +53,7 @@ fn main() { ) .subcommand(beacon_node::cli_app()) .subcommand(validator_client::cli_app()) + .subcommand(account_manager::cli_app()) .get_matches(); macro_rules! run_with_spec { @@ -115,6 +116,16 @@ fn run( // // Creating a command which can run both might be useful future works. + if let Some(sub_matches) = matches.subcommand_matches("Account Manager") { + let runtime_context = environment.core_context(); + + account_manager::run(sub_matches, runtime_context); + + // Exit early if the account manager was run. It does not used the tokio executor, so no + // need to wait for it to shutdown. + return Ok(()); + } + let beacon_node = if let Some(sub_matches) = matches.subcommand_matches("Beacon Node") { let runtime_context = environment.core_context(); From 5a9298d56748a1858dafd0c89a0cf5bbc056e693 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Thu, 14 Nov 2019 13:55:01 +1100 Subject: [PATCH 2/6] Unify logfile handling in `environment` crate. --- beacon_node/client/Cargo.toml | 1 - beacon_node/client/src/config.rs | 47 ++----------------------------- lighthouse/environment/Cargo.toml | 1 + lighthouse/environment/src/lib.rs | 27 +++++++++++++++++- lighthouse/src/main.rs | 8 ++++++ validator_client/Cargo.toml | 1 - validator_client/src/config.rs | 44 ++--------------------------- 7 files changed, 40 insertions(+), 89 deletions(-) diff --git a/beacon_node/client/Cargo.toml b/beacon_node/client/Cargo.toml index 1a82cd22be..d806f20070 100644 --- a/beacon_node/client/Cargo.toml +++ b/beacon_node/client/Cargo.toml @@ -27,7 +27,6 @@ error-chain = "0.12.1" serde_yaml = "0.8.11" slog = { version = "2.5.2", features = ["max_level_trace"] } slog-async = "2.3.0" -slog-json = "2.3.0" tokio = "0.1.22" clap = "2.33.0" dirs = "2.0.2" diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 331c905ccf..b88b2ba4c6 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -1,10 +1,8 @@ use clap::ArgMatches; use network::NetworkConfig; use serde_derive::{Deserialize, Serialize}; -use slog::{info, o, Drain}; -use std::fs::{self, OpenOptions}; +use std::fs; use std::path::PathBuf; -use std::sync::Mutex; /// The number initial validators when starting the `Minimal`. const TESTNET_SPEC_CONSTANTS: &str = "minimal"; @@ -95,47 +93,11 @@ impl Config { Some(path) } - // Update the logger to output in JSON to specified file - fn update_logger(&mut self, log: &mut slog::Logger) -> Result<(), &'static str> { - let file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&self.log_file); - - if file.is_err() { - return Err("Cannot open log file"); - } - let file = file.unwrap(); - - if let Some(file) = self.log_file.to_str() { - info!( - *log, - "Log file specified, output will now be written to {} in json.", file - ); - } else { - info!( - *log, - "Log file specified output will now be written in json" - ); - } - - let drain = Mutex::new(slog_json::Json::default(file)).fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - *log = slog::Logger::root(drain, o!()); - - Ok(()) - } - /// Apply the following arguments to `self`, replacing values if they are specified in `args`. /// /// Returns an error if arguments are obviously invalid. May succeed even if some values are /// invalid. - pub fn apply_cli_args( - &mut self, - args: &ArgMatches, - log: &mut slog::Logger, - ) -> Result<(), String> { + pub fn apply_cli_args(&mut self, args: &ArgMatches, _log: &slog::Logger) -> Result<(), String> { if let Some(dir) = args.value_of("datadir") { self.data_dir = PathBuf::from(dir); }; @@ -149,11 +111,6 @@ impl Config { self.rest_api.apply_cli_args(args)?; self.websocket_server.apply_cli_args(args)?; - if let Some(log_file) = args.value_of("logfile") { - self.log_file = PathBuf::from(log_file); - self.update_logger(log)?; - }; - Ok(()) } } diff --git a/lighthouse/environment/Cargo.toml b/lighthouse/environment/Cargo.toml index b5e21a4e88..01053ae9ac 100644 --- a/lighthouse/environment/Cargo.toml +++ b/lighthouse/environment/Cargo.toml @@ -17,3 +17,4 @@ slog-async = "^2.3.0" ctrlc = { version = "3.1.1", features = ["termination"] } futures = "0.1.25" parking_lot = "0.7" +slog-json = "2.3.0" diff --git a/lighthouse/environment/src/lib.rs b/lighthouse/environment/src/lib.rs index bde6c2dbb3..69002682a3 100644 --- a/lighthouse/environment/src/lib.rs +++ b/lighthouse/environment/src/lib.rs @@ -9,9 +9,12 @@ use eth2_config::Eth2Config; use futures::{sync::oneshot, Future}; -use slog::{o, Drain, Level, Logger}; +use slog::{info, o, Drain, Level, Logger}; use sloggers::{null::NullLoggerBuilder, Build}; use std::cell::RefCell; +use std::fs::OpenOptions; +use std::path::PathBuf; +use std::sync::Mutex; use tokio::runtime::{Builder as RuntimeBuilder, Runtime, TaskExecutor}; use types::{EthSpec, InteropEthSpec, MainnetEthSpec, MinimalEthSpec}; @@ -224,6 +227,28 @@ impl Environment { .map_err(|e| format!("Tokio runtime shutdown returned an error: {:?}", e)) } + /// Sets the logger (and all child loggers) to log to a file. + pub fn log_to_json_file(&mut self, path: PathBuf) -> Result<(), String> { + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&path) + .map_err(|e| format!("Unable to open logfile: {:?}", e))?; + + let drain = Mutex::new(slog_json::Json::default(file)).fuse(); + let drain = slog_async::Async::new(drain).build().fuse(); + self.log = slog::Logger::root(drain, o!()); + + info!( + self.log, + "Logging to JSON file"; + "path" => format!("{:?}", path) + ); + + Ok(()) + } + pub fn eth_spec_instance(&self) -> &E { &self.eth_spec_instance } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 7da8f991b4..a55345c858 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -6,6 +6,7 @@ use clap::{App, Arg, ArgMatches}; use env_logger::{Builder, Env}; use environment::EnvironmentBuilder; use slog::{crit, info, warn}; +use std::path::PathBuf; use std::process::exit; use types::EthSpec; use validator_client::ProductionValidatorClient; @@ -94,6 +95,13 @@ fn run( let log = environment.core_context().log; + if let Some(log_path) = matches.value_of("logfile") { + let path = log_path + .parse::() + .map_err(|e| format!("Failed to parse log path: {:?}", e))?; + environment.log_to_json_file(path)?; + } + if std::mem::size_of::() != 8 { crit!( log, diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 038bbd3c35..c3e9cb8561 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -25,7 +25,6 @@ serde_derive = "1.0.102" serde_json = "1.0.41" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } slog-async = "2.3.0" -slog-json = "2.3.0" slog-term = "2.4.2" tokio = "0.1.22" tokio-timer = "0.2.11" diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index d56487616a..749a5813cd 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -2,12 +2,11 @@ use bincode; use bls::Keypair; use clap::ArgMatches; use serde_derive::{Deserialize, Serialize}; -use slog::{error, info, o, warn, Drain}; -use std::fs::{self, File, OpenOptions}; +use slog::{error, warn}; +use std::fs::{self, File}; use std::io::{Error, ErrorKind}; use std::ops::Range; use std::path::PathBuf; -use std::sync::Mutex; use types::{ test_utils::{generate_deterministic_keypair, load_keypairs_from_yaml}, EthSpec, MainnetEthSpec, @@ -94,17 +93,12 @@ impl Config { pub fn apply_cli_args( &mut self, args: &ArgMatches, - log: &mut slog::Logger, + _log: &slog::Logger, ) -> Result<(), &'static str> { if let Some(datadir) = args.value_of("datadir") { self.data_dir = PathBuf::from(datadir); }; - if let Some(log_file) = args.value_of("logfile") { - self.log_file = PathBuf::from(log_file); - self.update_logger(log)?; - }; - if let Some(srv) = args.value_of("server") { self.server = srv.to_string(); }; @@ -112,38 +106,6 @@ impl Config { Ok(()) } - // Update the logger to output in JSON to specified file - fn update_logger(&mut self, log: &mut slog::Logger) -> Result<(), &'static str> { - let file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&self.log_file); - - if file.is_err() { - return Err("Cannot open log file"); - } - let file = file.unwrap(); - - if let Some(file) = self.log_file.to_str() { - info!( - *log, - "Log file specified, output will now be written to {} in json.", file - ); - } else { - info!( - *log, - "Log file specified output will now be written in json" - ); - } - - let drain = Mutex::new(slog_json::Json::default(file)).fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - *log = slog::Logger::root(drain, o!()); - - Ok(()) - } - /// Reads a single keypair from the given `path`. /// /// `path` should be the path to a directory containing a private key. The file name of `path` From 8561a95ee9f9c2220624f19b5f1f708fcbe0cf9f Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 18 Nov 2019 16:08:19 +1100 Subject: [PATCH 3/6] Add `deposit_contract` crate --- Cargo.toml | 1 + eth2/utils/deposit_contract/.gitignore | 1 + eth2/utils/deposit_contract/Cargo.toml | 17 ++++++ .../utils/deposit_contract}/build.rs | 0 eth2/utils/deposit_contract/src/lib.rs | 56 +++++++++++++++++++ tests/eth1_test_rig/Cargo.toml | 7 +-- tests/eth1_test_rig/src/lib.rs | 39 +++++++------ 7 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 eth2/utils/deposit_contract/.gitignore create mode 100644 eth2/utils/deposit_contract/Cargo.toml rename {tests/eth1_test_rig => eth2/utils/deposit_contract}/build.rs (100%) create mode 100644 eth2/utils/deposit_contract/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index f7abd8ae2b..6fc81d267f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "eth2/utils/bls", "eth2/utils/compare_fields", "eth2/utils/compare_fields_derive", + "eth2/utils/deposit_contract", "eth2/utils/eth2_config", "eth2/utils/eth2_interop_keypairs", "eth2/utils/logging", diff --git a/eth2/utils/deposit_contract/.gitignore b/eth2/utils/deposit_contract/.gitignore new file mode 100644 index 0000000000..81b46ff033 --- /dev/null +++ b/eth2/utils/deposit_contract/.gitignore @@ -0,0 +1 @@ +contract/ diff --git a/eth2/utils/deposit_contract/Cargo.toml b/eth2/utils/deposit_contract/Cargo.toml new file mode 100644 index 0000000000..1d28546dab --- /dev/null +++ b/eth2/utils/deposit_contract/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "deposit_contract" +version = "0.1.0" +authors = ["Paul Hauner "] +edition = "2018" + +build = "build.rs" + +[build-dependencies] +reqwest = "0.9.20" +serde_json = "1.0" + +[dependencies] +types = { path = "../../types"} +eth2_ssz = { path = "../ssz"} +tree_hash = { path = "../tree_hash"} +ethabi = "9.0" diff --git a/tests/eth1_test_rig/build.rs b/eth2/utils/deposit_contract/build.rs similarity index 100% rename from tests/eth1_test_rig/build.rs rename to eth2/utils/deposit_contract/build.rs diff --git a/eth2/utils/deposit_contract/src/lib.rs b/eth2/utils/deposit_contract/src/lib.rs new file mode 100644 index 0000000000..f9f0a3b203 --- /dev/null +++ b/eth2/utils/deposit_contract/src/lib.rs @@ -0,0 +1,56 @@ +use ethabi::{Contract, Token}; +use ssz::Encode; +use types::{ChainSpec, DepositData, SecretKey}; + +pub use ethabi::Error; + +pub const CONTRACT_DEPLOY_GAS: usize = 4_000_000; +pub const DEPOSIT_GAS: usize = 4_000_000; +pub const ABI: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.json"); +pub const BYTECODE: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.bytecode"); + +pub fn eth1_tx_data(deposit_data: &DepositData) -> Result, Error> { + let params = vec![ + Token::Bytes(deposit_data.pubkey.as_ssz_bytes()), + Token::Bytes(deposit_data.withdrawal_credentials.as_ssz_bytes()), + Token::Bytes(deposit_data.signature.as_ssz_bytes()), + ]; + + let abi = Contract::load(ABI)?; + let function = abi.function("deposit")?; + function.encode_input(¶ms) +} + +#[cfg(test)] +mod tests { + use super::*; + use types::{ + test_utils::generate_deterministic_keypair, EthSpec, Hash256, Keypair, MinimalEthSpec, + Signature, + }; + + type E = MinimalEthSpec; + + fn get_deposit(keypair: Keypair, spec: &ChainSpec) -> DepositData { + let mut deposit_data = DepositData { + pubkey: keypair.pk.into(), + withdrawal_credentials: Hash256::from_slice(&[42; 32]), + amount: u64::max_value(), + signature: Signature::empty_signature().into(), + }; + deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec); + deposit_data + } + + #[test] + fn basic() { + let spec = &E::default_spec(); + + let keypair = generate_deterministic_keypair(42); + let deposit = get_deposit(keypair.clone(), spec); + + let data = eth1_tx_data(&deposit).expect("should produce tx data"); + + assert_eq!(data.len(), 388, "bytes should be correct length"); + } +} diff --git a/tests/eth1_test_rig/Cargo.toml b/tests/eth1_test_rig/Cargo.toml index e2815db984..725a8402d5 100644 --- a/tests/eth1_test_rig/Cargo.toml +++ b/tests/eth1_test_rig/Cargo.toml @@ -4,12 +4,6 @@ version = "0.1.0" authors = ["Paul Hauner "] edition = "2018" -build = "build.rs" - -[build-dependencies] -reqwest = "0.9.20" -serde_json = "1.0" - [dependencies] web3 = "0.8.0" tokio = "0.1.17" @@ -17,3 +11,4 @@ futures = "0.1.25" types = { path = "../../eth2/types"} eth2_ssz = { path = "../../eth2/utils/ssz"} serde_json = "1.0" +deposit_contract = { path = "../../eth2/utils/deposit_contract"} diff --git a/tests/eth1_test_rig/src/lib.rs b/tests/eth1_test_rig/src/lib.rs index f137468b40..c424c0fe43 100644 --- a/tests/eth1_test_rig/src/lib.rs +++ b/tests/eth1_test_rig/src/lib.rs @@ -7,6 +7,7 @@ //! some initial issues. mod ganache; +use deposit_contract::{eth1_tx_data, ABI, BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS}; use futures::{stream, Future, IntoFuture, Stream}; use ganache::GanacheInstance; use ssz::Encode; @@ -16,19 +17,12 @@ use types::DepositData; use types::{EthSpec, Hash256, Keypair, Signature}; use web3::contract::{Contract, Options}; use web3::transports::Http; -use web3::types::{Address, U256}; +use web3::types::{Address, TransactionRequest, U256}; use web3::{Transport, Web3}; pub const DEPLOYER_ACCOUNTS_INDEX: usize = 0; pub const DEPOSIT_ACCOUNTS_INDEX: usize = 0; -const CONTRACT_DEPLOY_GAS: usize = 4_000_000; -const DEPOSIT_GAS: usize = 4_000_000; - -// Deposit contract -pub const ABI: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.json"); -pub const BYTECODE: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.bytecode"); - /// Provides a dedicated ganache-cli instance with the deposit contract already deployed. pub struct GanacheEth1Instance { pub ganache: GanacheInstance, @@ -138,6 +132,7 @@ impl DepositContract { deposit_data: DepositData, ) -> impl Future { let contract = self.contract.clone(); + let web3_1 = self.web3.clone(); self.web3 .eth() @@ -149,19 +144,27 @@ impl DepositContract { .cloned() .ok_or_else(|| "Insufficient accounts for deposit".to_string()) }) - .and_then(move |from_address| { - let params = ( - deposit_data.pubkey.as_ssz_bytes(), - deposit_data.withdrawal_credentials.as_ssz_bytes(), - deposit_data.signature.as_ssz_bytes(), - ); - let options = Options { + .and_then(move |from| { + let tx_request = TransactionRequest { + from, + to: Some(contract.address()), gas: Some(U256::from(DEPOSIT_GAS)), + gas_price: None, value: Some(from_gwei(deposit_data.amount)), - ..Options::default() + // Note: the reason we use this `TransactionRequest` instead of just using the + // function in `self.contract` is so that the `eth1_tx_data` function gets used + // during testing. + // + // It's important that `eth1_tx_data` stays correct and does not suffer from + // code-rot. + data: eth1_tx_data(&deposit_data).map(Into::into).ok(), + nonce: None, + condition: None, }; - contract - .call("deposit", params, from_address, options) + + web3_1 + .eth() + .send_transaction(tx_request) .map_err(|e| format!("Failed to call deposit fn: {:?}", e)) }) .map(|_| ()) From c1b541867e5c4aa16ecc68199ffebb26ad4cdc4b Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 18 Nov 2019 18:07:59 +1100 Subject: [PATCH 4/6] 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; From 1e67663c0cc18c6a780b98c1fda3025dea324e3a Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 18 Nov 2019 21:30:26 +1100 Subject: [PATCH 5/6] Update account manager CLI --- account_manager/src/cli.rs | 68 ++++++---- account_manager/src/lib.rs | 220 ++++++++++++++----------------- account_manager/src/validator.rs | 81 +++++++++++- lighthouse/src/main.rs | 3 +- validator_client/src/cli.rs | 20 --- 5 files changed, 218 insertions(+), 174 deletions(-) diff --git a/account_manager/src/cli.rs b/account_manager/src/cli.rs index 9eded83cc5..ad962a14f8 100644 --- a/account_manager/src/cli.rs +++ b/account_manager/src/cli.rs @@ -1,10 +1,8 @@ use clap::{App, Arg, SubCommand}; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { - App::new("Account Manager") - .visible_aliases(&["am", "accounts", "accounts_manager"]) - .version("0.0.1") - .author("Sigma Prime ") + App::new("account_manager") + .visible_aliases(&["am", "account", "account_manager"]) .about("Eth 2.0 Accounts Manager") .arg( Arg::with_name("logfile") @@ -22,33 +20,45 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true), ) .subcommand( - SubCommand::with_name("generate") - .about("Generates a new validator private key") - .version("0.0.1") - .author("Sigma Prime "), - ) - .subcommand( - SubCommand::with_name("generate_deterministic") - .about("Generates a deterministic validator private key FOR TESTING") + SubCommand::with_name("validator") + .about("Eth2 validator managment commands.") .version("0.0.1") .author("Sigma Prime ") - .arg( - Arg::with_name("validator index") - .long("index") - .short("i") - .value_name("index") - .help("The index of the validator, for which the test key is generated") - .takes_value(true) - .required(true), + .subcommand( + SubCommand::with_name("new") + .about("Create a new validator.") + .subcommand( + SubCommand::with_name("insecure") + .about("Uses the insecure deterministic keypairs. Do not store value in these.") + .arg( + Arg::with_name("first") + .index(1) + .value_name("INDEX") + .help("Index of the first validator") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("last") + .index(2) + .value_name("INDEX") + .help("Index of the first validator") + .takes_value(true) + .required(true), + ), + ) + .subcommand( + SubCommand::with_name("random") + .about("Uses the Rust rand crate ThreadRandom to generate keys.") + .arg( + Arg::with_name("validator_count") + .index(1) + .value_name("INTEGER") + .help("The number of new validators to generate.") + .takes_value(true) + .default_value("1"), + ), + ) ) - .arg( - Arg::with_name("validator count") - .long("validator_count") - .short("n") - .value_name("validator_count") - .help("If supplied along with `index`, generates keys `i..i + n`.") - .takes_value(true) - .default_value("1"), - ), ) } diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 8687e3d09c..58d2427f2d 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -1,20 +1,17 @@ mod cli; pub mod validator; -use bls::Keypair; use clap::ArgMatches; use environment::RuntimeContext; -use slog::{crit, debug, info}; +use slog::{crit, info}; use std::fs; use std::path::PathBuf; -use types::{test_utils::generate_deterministic_keypair, EthSpec}; -use validator_client::Config as ValidatorClientConfig; +use types::{ChainSpec, EthSpec}; +use validator::{ValidatorDirectory, ValidatorDirectoryBuilder}; pub use cli::cli_app; -pub const DEFAULT_DATA_DIR: &str = ".lighthouse-validator"; -pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml"; - +/// Run the account manager, logging an error if the operation did not succeed. pub fn run(matches: &ArgMatches, context: RuntimeContext) { let log = context.log.clone(); match run_account_manager(matches, context) { @@ -23,13 +20,14 @@ pub fn run(matches: &ArgMatches, context: RuntimeContext) { } } +/// Run the account manager, returning an error if the operation did not succeed. fn run_account_manager( matches: &ArgMatches, context: RuntimeContext, ) -> Result<(), String> { let log = context.log.clone(); - let data_dir = matches + let datadir = matches .value_of("datadir") .map(PathBuf::from) .unwrap_or_else(|| { @@ -39,128 +37,114 @@ fn run_account_manager( panic!("Failed to find a home directory"); } }; - default_dir.push(DEFAULT_DATA_DIR); + default_dir.push(".lighthouse"); + default_dir.push("validator"); default_dir }); - fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to initialize data dir: {}", e))?; + fs::create_dir_all(&datadir).map_err(|e| format!("Failed to initialize datadir: {}", 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 - .value_of("datadir") - .and_then(|v| Some(PathBuf::from(v))) - { - Some(v) => v, - None => { - // use the default - let mut default_dir = match dirs::home_dir() { - Some(v) => v, - None => { - crit!(log, "Failed to find a home directory"); - return; - } - }; - default_dir.push(DEFAULT_DATA_DIR); - default_dir - } - }; - - // create the directory if needed - match fs::create_dir_all(&data_dir) { - Ok(_) => {} - Err(e) => { - crit!(log, "Failed to initialize data dir"; "error" => format!("{}", e)); - return; - } - } - - let mut client_config = ValidatorClientConfig::default(); - - // Ensure the `data_dir` in the config matches that supplied to the CLI. - client_config.data_dir = data_dir.clone(); - - if let Err(e) = client_config.apply_cli_args(&matches, &mut log) { - crit!(log, "Failed to parse ClientConfig CLI arguments"; "error" => format!("{:?}", e)); - return; - }; - - // Log configuration - info!(log, ""; - "data_dir" => &client_config.data_dir.to_str()); + info!( + log, + "Located data directory"; + "path" => format!("{:?}", datadir) + ); match matches.subcommand() { - ("generate", Some(_)) => generate_random(&client_config, &log), - ("generate_deterministic", Some(m)) => { - if let Some(string) = m.value_of("validator index") { - let i: usize = string.parse().expect("Invalid validator index"); - if let Some(string) = m.value_of("validator count") { - let n: usize = string.parse().expect("Invalid end validator count"); - - let indices: Vec = (i..i + n).collect(); - generate_deterministic_multiple(&indices, &client_config, &log) - } else { - generate_deterministic(i, &client_config, &log) - } + ("validator", Some(matches)) => match matches.subcommand() { + ("new", Some(matches)) => new_validator_subcommand(matches, datadir, context)?, + _ => { + return Err("Invalid 'validator new' command. See --help.".to_string()); } + }, + _ => { + return Err("Invalid 'validator' command. See --help.".to_string()); + } + } + + Ok(()) +} + +/// Describes the crypto key generation methods for a validator. +enum KeygenMethod { + /// Produce an insecure "deterministic" keypair. Used only for interop and testing. + Insecure(usize), + /// Generate a new key from the `rand` thread random RNG. + ThreadRandom, +} + +/// Process the subcommand for creating new validators. +fn new_validator_subcommand( + matches: &ArgMatches, + datadir: PathBuf, + context: RuntimeContext, +) -> Result<(), String> { + let log = context.log.clone(); + + let methods: Vec = match matches.subcommand() { + ("insecure", Some(matches)) => { + let first = matches + .value_of("first") + .ok_or_else(|| "No first index".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse first index: {}", e))?; + let last = matches + .value_of("last") + .ok_or_else(|| "No last index".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse first index: {}", e))?; + + (first..last).map(KeygenMethod::Insecure).collect() + } + ("random", Some(matches)) => { + let count = matches + .value_of("validator_count") + .ok_or_else(|| "No validator count".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse validator count: {}", e))?; + + (0..count).map(|_| KeygenMethod::ThreadRandom).collect() } _ => { - crit!( - log, - "The account manager must be run with a subcommand. See help for more information." - ); + return Err("Invalid 'validator' command. See --help.".to_string()); } - } -} + }; -fn generate_random(config: &ValidatorClientConfig, log: &slog::Logger) { - save_key(&Keypair::random(), config, log) -} + let validators = make_validators(datadir.clone(), &methods, context.eth2_config.spec)?; -fn generate_deterministic_multiple( - validator_indices: &[usize], - config: &ValidatorClientConfig, - log: &slog::Logger, -) { - for validator_index in validator_indices { - generate_deterministic(*validator_index, config, log) - } -} - -fn generate_deterministic( - validator_index: usize, - config: &ValidatorClientConfig, - log: &slog::Logger, -) { - save_key( - &generate_deterministic_keypair(validator_index), - config, + info!( log, - ) -} - -fn save_key(keypair: &Keypair, config: &ValidatorClientConfig, log: &slog::Logger) { - let key_path: PathBuf = config - .save_key(&keypair) - .expect("Unable to save newly generated private key."); - debug!( - log, - "Keypair generated {:?}, saved to: {:?}", - keypair.identifier(), - key_path.to_string_lossy() + "Generated validator directories"; + "base_path" => format!("{:?}", datadir), + "count" => validators.len(), ); + + Ok(()) +} + +/// Produces a validator directory for each of the key generation methods provided in `methods`. +fn make_validators( + datadir: PathBuf, + methods: &[KeygenMethod], + spec: ChainSpec, +) -> Result, String> { + methods + .iter() + .map(|method| { + let mut builder = ValidatorDirectoryBuilder::default() + .spec(spec.clone()) + .full_deposit_amount()?; + + builder = match method { + KeygenMethod::Insecure(index) => builder.insecure_keypairs(*index), + KeygenMethod::ThreadRandom => builder.thread_random_keypairs(), + }; + + builder + .create_directory(datadir.clone())? + .write_keypair_files()? + .write_eth1_data_file()? + .build() + }) + .collect() } diff --git a/account_manager/src/validator.rs b/account_manager/src/validator.rs index 6a68a98725..6a67a0c443 100644 --- a/account_manager/src/validator.rs +++ b/account_manager/src/validator.rs @@ -14,12 +14,16 @@ use types::{ const VOTING_KEY_PREFIX: &str = "voting"; const WITHDRAWAL_KEY_PREFIX: &str = "withdrawal"; -const ETH1_DEPOSIT_DATA_FILE: &str = "eth1_deposit_data_{}.rlp"; +const ETH1_DEPOSIT_DATA_FILE: &str = "eth1_deposit_data.rlp"; +/// Returns the filename of a keypair file. fn keypair_file(prefix: &str) -> String { format!("{}_keypair", prefix) } +/// Represents the files/objects for each dedicated lighthouse validator directory. +/// +/// Generally lives in `~/.lighthouse/validators/`. #[derive(Debug, Clone, PartialEq)] pub struct ValidatorDirectory { pub directory: PathBuf, @@ -51,6 +55,7 @@ impl ValidatorDirectory { } } +/// Load a `Keypair` from a file. fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result { let path = base_path.join(keypair_file(file_prefix)); @@ -70,6 +75,7 @@ fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result Result, String> { let path = base_path.join(ETH1_DEPOSIT_DATA_FILE); @@ -87,6 +93,7 @@ fn load_eth1_deposit_data(base_path: PathBuf) -> Result, String> { Ok(bytes) } +/// A helper struct to allow SSZ enc/dec for a `Keypair`. #[derive(Encode, Decode)] struct SszEncodableKeypair { pk: PublicKey, @@ -111,6 +118,7 @@ impl From for SszEncodableKeypair { } } +/// Builds a `ValidatorDirectory`, both in-memory and on-disk. #[derive(Default)] pub struct ValidatorDirectoryBuilder { directory: Option, @@ -141,13 +149,13 @@ impl ValidatorDirectoryBuilder { self } - pub fn random_keypairs(mut self) -> Self { + pub fn thread_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 { + pub fn insecure_keypairs(mut self, index: usize) -> Self { let keypair = generate_deterministic_keypair(index); self.voting_keypair = Some(keypair.clone()); self.withdrawal_keypair = Some(keypair); @@ -292,7 +300,7 @@ mod tests { type E = MinimalEthSpec; #[test] - fn round_trip() { + fn random_keypairs_round_trip() { let spec = E::default_spec(); let temp_dir = TempDir::new("acc_manager").expect("should create test dir"); @@ -300,7 +308,7 @@ mod tests { .spec(spec) .full_deposit_amount() .expect("should set full deposit amount") - .deterministic_keypairs(42) + .thread_random_keypairs() .create_directory(temp_dir.path().into()) .expect("should create directory") .write_keypair_files() @@ -318,4 +326,67 @@ mod tests { "the directory created should match the one loaded" ); } + + #[test] + fn deterministic_keypairs_round_trip() { + let spec = E::default_spec(); + let temp_dir = TempDir::new("acc_manager").expect("should create test dir"); + let index = 42; + + let created_dir = ValidatorDirectoryBuilder::default() + .spec(spec) + .full_deposit_amount() + .expect("should set full deposit amount") + .insecure_keypairs(index) + .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"); + + assert!( + created_dir.directory.exists(), + "should have created directory" + ); + + let mut parent = created_dir.directory.clone(); + parent.pop(); + assert_eq!( + parent, + PathBuf::from(temp_dir.path()), + "should have created directory ontop of base dir" + ); + + let expected_keypair = generate_deterministic_keypair(index); + assert_eq!( + created_dir.voting_keypair, + Some(expected_keypair.clone()), + "voting keypair should be as expected" + ); + assert_eq!( + created_dir.withdrawal_keypair, + Some(expected_keypair), + "withdrawal keypair should be as expected" + ); + assert!( + created_dir + .deposit_data + .clone() + .expect("should have data") + .len() + > 0, + "should have some deposit data" + ); + + 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/lighthouse/src/main.rs b/lighthouse/src/main.rs index a55345c858..74c261840c 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -31,7 +31,6 @@ fn main() { .value_name("TITLE") .help("Specifies the default eth2 spec type. Only effective when creating a new datadir.") .takes_value(true) - .required(true) .possible_values(&["mainnet", "minimal", "interop"]) .global(true) .default_value("minimal") @@ -124,7 +123,7 @@ fn run( // // Creating a command which can run both might be useful future works. - if let Some(sub_matches) = matches.subcommand_matches("Account Manager") { + if let Some(sub_matches) = matches.subcommand_matches("account_manager") { let runtime_context = environment.core_context(); account_manager::run(sub_matches, runtime_context); diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index 623d1b349b..6b6956f786 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -22,16 +22,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("File path where output will be written.") .takes_value(true), ) - .arg( - Arg::with_name("spec") - .long("spec") - .value_name("TITLE") - .help("Specifies the default eth2 spec type.") - .takes_value(true) - .possible_values(&["mainnet", "minimal", "interop"]) - .conflicts_with("eth2-config") - .global(true) - ) .arg( Arg::with_name("eth2-config") .long("eth2-config") @@ -66,16 +56,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .default_value(DEFAULT_SERVER_HTTP_PORT) .takes_value(true), ) - .arg( - Arg::with_name("debug-level") - .long("debug-level") - .value_name("LEVEL") - .short("s") - .help("The title of the spec constants for chain config.") - .takes_value(true) - .possible_values(&["info", "debug", "trace", "warn", "error", "crit"]) - .default_value("trace"), - ) /* * The "testnet" sub-command. * From ad5fed74cb9804dc4d90fb20dbb4ff2f708a8819 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 18 Nov 2019 21:55:23 +1100 Subject: [PATCH 6/6] Write eth1 data file as hex string --- account_manager/Cargo.toml | 1 + account_manager/src/validator.rs | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 6ec3390bf2..55221ba522 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -21,3 +21,4 @@ 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" } +hex = "0.4" diff --git a/account_manager/src/validator.rs b/account_manager/src/validator.rs index 6a67a0c443..02e2f048d2 100644 --- a/account_manager/src/validator.rs +++ b/account_manager/src/validator.rs @@ -1,5 +1,6 @@ use bls::get_withdrawal_credentials; use deposit_contract::eth1_tx_data; +use hex; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::fs; @@ -90,7 +91,13 @@ fn load_eth1_deposit_data(base_path: PathBuf) -> Result, String> { .read_to_end(&mut bytes) .map_err(|e| format!("Unable to read eth1 deposit data file: {}", e))?; - Ok(bytes) + let string = String::from_utf8_lossy(&bytes); + if string.starts_with("0x") { + hex::decode(&string[2..]) + .map_err(|e| format!("Unable to decode eth1 data file as hex: {}", e)) + } else { + Err(format!("String did not start with 0x: {}", string)) + } } /// A helper struct to allow SSZ enc/dec for a `Keypair`. @@ -273,7 +280,7 @@ impl ValidatorDirectoryBuilder { File::create(&path) .map_err(|e| format!("Unable to create file: {}", e))? - .write_all(&deposit_data) + .write_all(&format!("0x{}", hex::encode(&deposit_data)).as_bytes()) .map_err(|e| format!("Unable to write eth1 data file: {}", e))?; self.deposit_data = Some(deposit_data);