mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-21 22:04:44 +00:00
Update account manager CLI
This commit is contained in:
@@ -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 <contact@sigmaprime.io>")
|
||||
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 <contact@sigmaprime.io>"),
|
||||
)
|
||||
.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 <contact@sigmaprime.io>")
|
||||
.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"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<T: EthSpec>(matches: &ArgMatches, context: RuntimeContext<T>) {
|
||||
let log = context.log.clone();
|
||||
match run_account_manager(matches, context) {
|
||||
@@ -23,13 +20,14 @@ pub fn run<T: EthSpec>(matches: &ArgMatches, context: RuntimeContext<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the account manager, returning an error if the operation did not succeed.
|
||||
fn run_account_manager<T: EthSpec>(
|
||||
matches: &ArgMatches,
|
||||
context: RuntimeContext<T>,
|
||||
) -> 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<T: EthSpec>(
|
||||
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<T: EthSpec>(matches: &ArgMatches, context: RuntimeContext<T>) {
|
||||
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<usize> = (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<T: EthSpec>(
|
||||
matches: &ArgMatches,
|
||||
datadir: PathBuf,
|
||||
context: RuntimeContext<T>,
|
||||
) -> Result<(), String> {
|
||||
let log = context.log.clone();
|
||||
|
||||
let methods: Vec<KeygenMethod> = match matches.subcommand() {
|
||||
("insecure", Some(matches)) => {
|
||||
let first = matches
|
||||
.value_of("first")
|
||||
.ok_or_else(|| "No first index".to_string())?
|
||||
.parse::<usize>()
|
||||
.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::<usize>()
|
||||
.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::<usize>()
|
||||
.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<Vec<ValidatorDirectory>, 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()
|
||||
}
|
||||
|
||||
@@ -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<Keypair, String> {
|
||||
let path = base_path.join(keypair_file(file_prefix));
|
||||
|
||||
@@ -70,6 +75,7 @@ fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result<Keypair, String
|
||||
.map_err(|e| format!("Unable to decode keypair: {:?}", e))
|
||||
}
|
||||
|
||||
/// Load eth1_deposit_data from file.
|
||||
fn load_eth1_deposit_data(base_path: PathBuf) -> Result<Vec<u8>, String> {
|
||||
let path = base_path.join(ETH1_DEPOSIT_DATA_FILE);
|
||||
|
||||
@@ -87,6 +93,7 @@ fn load_eth1_deposit_data(base_path: PathBuf) -> Result<Vec<u8>, 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<Keypair> for SszEncodableKeypair {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `ValidatorDirectory`, both in-memory and on-disk.
|
||||
#[derive(Default)]
|
||||
pub struct ValidatorDirectoryBuilder {
|
||||
directory: Option<PathBuf>,
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<E: EthSpec>(
|
||||
//
|
||||
// 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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user