diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 9124047e45..9ce724c148 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -11,6 +11,7 @@ store = { path = "./store" } client = { path = "client" } version = { path = "version" } clap = "2.32.0" +rand = "0.7" slog = { version = "^2.2.3" , features = ["max_level_trace"] } slog-term = "^2.4.0" slog-async = "^2.3.0" diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index ea8186dbc9..e1464e5b43 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -1,4 +1,4 @@ -use crate::{Bootstrapper, Eth2Config}; +use crate::Bootstrapper; use clap::ArgMatches; use network::NetworkConfig; use serde_derive::{Deserialize, Serialize}; @@ -127,15 +127,6 @@ impl Config { self.data_dir = PathBuf::from(dir); }; - if let Some(default_spec) = args.value_of("default-spec") { - match default_spec { - "mainnet" => self.spec_constants = Eth2Config::mainnet().spec_constants, - "minimal" => self.spec_constants = Eth2Config::minimal().spec_constants, - "interop" => self.spec_constants = Eth2Config::interop().spec_constants, - _ => {} // not supported - } - } - if let Some(dir) = args.value_of("db") { self.db_type = dir.to_string(); }; diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs new file mode 100644 index 0000000000..959edbd607 --- /dev/null +++ b/beacon_node/src/config.rs @@ -0,0 +1,206 @@ +use clap::ArgMatches; +use client::{ClientConfig, Eth2Config}; +use eth2_config::{read_from_file, write_to_file}; +use rand::{distributions::Alphanumeric, Rng}; +use slog::{crit, info, Logger}; +use std::fs; +use std::path::PathBuf; + +pub const DEFAULT_DATA_DIR: &str = ".lighthouse"; +pub const CLIENT_CONFIG_FILENAME: &str = "beacon-node.toml"; +pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; + +type Result = std::result::Result; +type Config = (ClientConfig, Eth2Config); + +/// Gets the fully-initialized global client and eth2 configuration objects. +pub fn get_configs(matches: &ArgMatches, log: &Logger) -> Result { + let mut builder = ConfigBuilder::new(matches, log)?; + + match matches.subcommand() { + ("testnet", Some(sub_matches)) => { + if sub_matches.is_present("random-datadir") { + builder.set_random_datadir()?; + } + + info!( + log, + "Creating new datadir"; + "path" => format!("{:?}", builder.data_dir) + ); + + builder.update_spec_from_subcommand(&sub_matches)?; + builder.write_configs_to_new_datadir()?; + } + _ => { + info!( + log, + "Resuming from existing datadir"; + "path" => format!("{:?}", builder.data_dir) + ); + + // If the `testnet` command was not provided, attempt to load an existing datadir and + // continue with an existing chain. + builder.load_from_datadir()?; + } + }; + + builder.build() +} + +/// Allows for building a set of configurations based upon `clap` arguments. +struct ConfigBuilder<'a> { + matches: &'a ArgMatches<'a>, + log: &'a Logger, + pub data_dir: PathBuf, + eth2_config: Eth2Config, + client_config: ClientConfig, +} + +impl<'a> ConfigBuilder<'a> { + /// Create a new builder with default settings. + pub fn new(matches: &'a ArgMatches, log: &'a Logger) -> Result { + // Read the `--datadir` flag. + // + // If it's not present, try and find the home directory (`~`) and push the default data + // directory onto it. + let data_dir: PathBuf = matches + .value_of("datadir") + .map(|string| PathBuf::from(string)) + .or_else(|| { + dirs::home_dir().map(|mut home| { + home.push(DEFAULT_DATA_DIR); + home + }) + }) + .ok_or_else(|| "Unable to find a home directory for the datadir".to_string())?; + + Ok(Self { + matches, + log, + data_dir, + eth2_config: Eth2Config::minimal(), + client_config: ClientConfig::default(), + }) + } + + /// Consumes self, returning the configs. + pub fn build(mut self) -> Result { + self.eth2_config.apply_cli_args(&self.matches)?; + self.client_config + .apply_cli_args(&self.matches, &mut self.log.clone())?; + + if self.eth2_config.spec_constants != self.client_config.spec_constants { + crit!(self.log, "Specification constants do not match."; + "client_config" => format!("{}", self.client_config.spec_constants), + "eth2_config" => format!("{}", self.eth2_config.spec_constants) + ); + return Err("Specification constant mismatch".into()); + } + + self.client_config.data_dir = self.data_dir; + + Ok((self.client_config, self.eth2_config)) + } + + /// Set the config data_dir to be an random directory. + /// + /// Useful for easily spinning up ephemeral testnets. + pub fn set_random_datadir(&mut self) -> Result<()> { + let random = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .collect::(); + + let mut s = DEFAULT_DATA_DIR.to_string(); + s.push_str("_random_"); + s.push_str(&random); + + self.data_dir.pop(); + self.data_dir.push(s); + + Ok(()) + } + + /// Reads the subcommand and tries to update `self.eth2_config` based up on the `--spec` flag. + /// + /// Returns an error if the `--spec` flag is not present. + pub fn update_spec_from_subcommand(&mut self, sub_matches: &ArgMatches) -> Result<()> { + // Re-initialise the `Eth2Config`. + // + // If a CLI parameter is set, overwrite any config file present. + // If a parameter is not set, use either the config file present or default to minimal. + let eth2_config = match sub_matches.value_of("spec") { + Some("mainnet") => Eth2Config::mainnet(), + Some("minimal") => Eth2Config::minimal(), + Some("interop") => Eth2Config::interop(), + _ => return Err("Unable to determine specification type.".into()), + }; + + self.client_config.spec_constants = sub_matches + .value_of("spec") + .expect("Guarded by prior match statement") + .to_string(); + self.eth2_config = eth2_config; + + Ok(()) + } + + /// Writes the configs in `self` to `self.data_dir`. + /// + /// Returns an error if `self.data_dir` already exists. + pub fn write_configs_to_new_datadir(&mut self) -> Result<()> { + // Do not permit creating a new config when the datadir exists. + if self.data_dir.exists() { + return Err( + "Datadir already exists, will not overwrite. Remove the directory or use --datadir." + .into(), + ); + } + + // Create `datadir` and any non-existing parent directories. + fs::create_dir_all(&self.data_dir).map_err(|e| { + crit!(self.log, "Failed to initialize data dir"; "error" => format!("{}", e)); + format!("{}", e) + })?; + + // Write the client config to a TOML file in the datadir. + write_to_file( + self.data_dir.join(CLIENT_CONFIG_FILENAME), + &self.client_config, + ) + .map_err(|e| format!("Unable to write {} file: {:?}", CLIENT_CONFIG_FILENAME, e))?; + + // Write the eth2 config to a TOML file in the datadir. + write_to_file(self.data_dir.join(ETH2_CONFIG_FILENAME), &self.eth2_config) + .map_err(|e| format!("Unable to write {} file: {:?}", ETH2_CONFIG_FILENAME, e))?; + + Ok(()) + } + + /// Attempts to load the client and eth2 configs from `self.data_dir`. + /// + /// Returns an error if any files are not found or are invalid. + pub fn load_from_datadir(&mut self) -> Result<()> { + // Check to ensure the datadir exists. + // + // For now we return an error. In the future we may decide to boot a default (e.g., + // public testnet or mainnet). + if !self.data_dir.exists() { + return Err( + "No datadir found. Use the 'testnet' sub-command to select a testnet type.".into(), + ); + } + + self.eth2_config = read_from_file::(self.data_dir.join(ETH2_CONFIG_FILENAME)) + .map_err(|e| format!("Unable to parse {} file: {:?}", ETH2_CONFIG_FILENAME, e))? + .ok_or_else(|| format!("{} file does not exist", ETH2_CONFIG_FILENAME))?; + + self.client_config = + read_from_file::(self.data_dir.join(CLIENT_CONFIG_FILENAME)) + .map_err(|e| format!("Unable to parse {} file: {:?}", CLIENT_CONFIG_FILENAME, e))? + .ok_or_else(|| format!("{} file does not exist", ETH2_CONFIG_FILENAME))?; + + Ok(()) + } +} diff --git a/beacon_node/src/main.rs b/beacon_node/src/main.rs index 04366baa7a..12c9b8a017 100644 --- a/beacon_node/src/main.rs +++ b/beacon_node/src/main.rs @@ -1,12 +1,10 @@ +mod config; mod run; -use clap::{App, Arg}; -use client::{ClientConfig, Eth2Config}; +use clap::{App, Arg, SubCommand}; +use config::get_configs; use env_logger::{Builder, Env}; -use eth2_config::{read_from_file, write_to_file}; use slog::{crit, o, warn, Drain, Level}; -use std::fs; -use std::path::PathBuf; pub const DEFAULT_DATA_DIR: &str = ".lighthouse"; @@ -31,6 +29,7 @@ fn main() { .value_name("DIR") .help("Data directory for keys and databases.") .takes_value(true) + .global(true) ) .arg( Arg::with_name("logfile") @@ -45,6 +44,7 @@ fn main() { .value_name("NETWORK-DIR") .help("Data directory for network keys.") .takes_value(true) + .global(true) ) /* * Network parameters. @@ -163,24 +163,6 @@ fn main() { .possible_values(&["disk", "memory"]) .default_value("memory"), ) - /* - * Specification/testnet params. - */ - .arg( - Arg::with_name("default-spec") - .long("default-spec") - .value_name("TITLE") - .short("default-spec") - .help("Specifies the default eth2 spec to be used. This will override any spec written to disk and will therefore be used by default in future instances.") - .takes_value(true) - .possible_values(&["mainnet", "minimal", "interop"]) - ) - .arg( - Arg::with_name("recent-genesis") - .long("recent-genesis") - .short("r") - .help("When present, genesis will be within 30 minutes prior. Only for testing"), - ) /* * Logging. */ @@ -201,14 +183,68 @@ fn main() { .takes_value(true), ) /* - * Bootstrap. + * The "testnet" sub-command. + * + * Allows for creating a new datadir with testnet-specific configs. */ - .arg( - Arg::with_name("bootstrap") - .long("bootstrap") - .value_name("HTTP_SERVER") - .help("Load the genesis state and libp2p address from the HTTP API of another Lighthouse node.") - .takes_value(true) + .subcommand(SubCommand::with_name("testnet") + .about("Create a new Lighthouse datadir using a testnet strategy.") + .arg( + Arg::with_name("spec") + .short("s") + .long("spec") + .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"]) + ) + .arg( + Arg::with_name("random-datadir") + .long("random-datadir") + .short("r") + .help("If present, append a random string to the datadir path. Useful for fast development \ + iteration.") + ) + .arg( + Arg::with_name("force-create") + .long("force-create") + .short("f") + .help("If present, will delete any existing datadir before creating a new one. Cannot be \ + used when specifying --random-datadir (logic error).") + .conflicts_with("random-datadir") + ) + /* + * Testnet sub-commands. + */ + .subcommand(SubCommand::with_name("bootstrap") + .about("Connects to the given HTTP server, downloads a genesis state and attempts to peer with it.") + .arg(Arg::with_name("server") + .value_name("HTTP_SERVER") + .required(true) + .help("A HTTP server, with a http:// prefix")) + .arg(Arg::with_name("libp2p-port") + .short("p") + .long("port") + .value_name("TCP_PORT") + .help("A libp2p listen port used to peer with the bootstrap server")) + ) + .subcommand(SubCommand::with_name("recent") + .about("Creates a new genesis state where the genesis time was at the previous \ + 30-minute boundary (e.g., 12:00, 12:30, 13:00, etc.)") + .arg(Arg::with_name("validator_count") + .value_name("VALIDATOR_COUNT") + .required(true) + .help("The number of validators in the genesis state")) + ) + .subcommand(SubCommand::with_name("yaml-genesis-state") + .about("Creates a new datadir where the genesis state is read from YAML. Will fail to parse \ + a YAML state that was generated to a different spec than that specified by --spec.") + .arg(Arg::with_name("file") + .value_name("YAML_FILE") + .required(true) + .help("A YAML file from which to read the state")) + ) ) .get_matches(); @@ -235,143 +271,24 @@ fn main() { _ => drain.filter_level(Level::Trace), }; - let mut log = slog::Logger::root(drain.fuse(), o!()); + let log = slog::Logger::root(drain.fuse(), o!()); warn!( log, "Ethereum 2.0 is pre-release. This software is experimental." ); - 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 client_config_path = data_dir.join(CLIENT_CONFIG_FILENAME); - - // Attempt to load the `ClientConfig` from disk. + // Load the process-wide configuration. // - // If file doesn't exist, create a new, default one. - let mut client_config = match read_from_file::(client_config_path.clone()) { - Ok(Some(c)) => c, - Ok(None) => { - let default = ClientConfig::default(); - if let Err(e) = write_to_file(client_config_path, &default) { - crit!(log, "Failed to write default ClientConfig to file"; "error" => format!("{:?}", e)); - return; - } - default - } + // May load this from disk or create a new configuration, depending on the CLI flags supplied. + let (client_config, eth2_config) = match get_configs(&matches, &log) { + Ok(configs) => configs, Err(e) => { - crit!(log, "Failed to load a ChainConfig file"; "error" => format!("{:?}", e)); + crit!(log, "Failed to load configuration"; "error" => e); return; } }; - // Ensure the `data_dir` in the config matches that supplied to the CLI. - client_config.data_dir = data_dir.clone(); - - // Update the client config with any CLI args. - match client_config.apply_cli_args(&matches, &mut log) { - Ok(()) => (), - Err(s) => { - crit!(log, "Failed to parse ClientConfig CLI arguments"; "error" => s); - return; - } - }; - - let eth2_config_path = data_dir.join(ETH2_CONFIG_FILENAME); - - // Initialise the `Eth2Config`. - // - // If a CLI parameter is set, overwrite any config file present. - // If a parameter is not set, use either the config file present or default to minimal. - let cli_config = match matches.value_of("default-spec") { - Some("mainnet") => Some(Eth2Config::mainnet()), - Some("minimal") => Some(Eth2Config::minimal()), - Some("interop") => Some(Eth2Config::interop()), - _ => None, - }; - // if a CLI flag is specified, write the new config if it doesn't exist, - // otherwise notify the user that the file will not be written. - let eth2_config_from_file = match read_from_file::(eth2_config_path.clone()) { - Ok(config) => config, - Err(e) => { - crit!(log, "Failed to read the Eth2Config from file"; "error" => format!("{:?}", e)); - return; - } - }; - - let mut eth2_config = { - if let Some(cli_config) = cli_config { - if eth2_config_from_file.is_none() { - // write to file if one doesn't exist - if let Err(e) = write_to_file(eth2_config_path, &cli_config) { - crit!(log, "Failed to write default Eth2Config to file"; "error" => format!("{:?}", e)); - return; - } - } else { - warn!( - log, - "Eth2Config file exists. Configuration file is ignored, using default" - ); - } - cli_config - } else { - // CLI config not specified, read from disk - match eth2_config_from_file { - Some(config) => config, - None => { - // set default to minimal - let eth2_config = Eth2Config::minimal(); - if let Err(e) = write_to_file(eth2_config_path, ð2_config) { - crit!(log, "Failed to write default Eth2Config to file"; "error" => format!("{:?}", e)); - return; - } - eth2_config - } - } - } - }; - - // Update the eth2 config with any CLI flags. - match eth2_config.apply_cli_args(&matches) { - Ok(()) => (), - Err(s) => { - crit!(log, "Failed to parse Eth2Config CLI arguments"; "error" => s); - return; - } - }; - - // check to ensure the spec constants between the client and eth2_config match - if eth2_config.spec_constants != client_config.spec_constants { - crit!(log, "Specification constants do not match."; "client_config" => format!("{}", client_config.spec_constants), "eth2_config" => format!("{}", eth2_config.spec_constants)); - return; - } - // Start the node using a `tokio` executor. match run::run_beacon_node(client_config, eth2_config, &log) { Ok(_) => {} diff --git a/beacon_node/src/run.rs b/beacon_node/src/run.rs index f88cb7460b..e23b5bc72d 100644 --- a/beacon_node/src/run.rs +++ b/beacon_node/src/run.rs @@ -46,7 +46,6 @@ pub fn run_beacon_node( log, "BeaconNode init"; "p2p_listen_address" => format!("{:?}", &other_client_config.network.listen_address), - "data_dir" => format!("{:?}", other_client_config.data_dir()), "network_dir" => format!("{:?}", other_client_config.network.network_dir), "spec_constants" => &spec_constants, "db_type" => &other_client_config.db_type,