Refactor beacon node CLI

This commit is contained in:
Paul Hauner
2019-11-28 10:31:51 +11:00
parent 9bd0e13d9d
commit 73ead55235
14 changed files with 455 additions and 835 deletions

View File

@@ -35,3 +35,4 @@ futures = "0.1.29"
environment = { path = "../lighthouse/environment" } environment = { path = "../lighthouse/environment" }
genesis = { path = "genesis" } genesis = { path = "genesis" }
eth2_testnet = { path = "../eth2/utils/eth2_testnet" } eth2_testnet = { path = "../eth2/utils/eth2_testnet" }
eth2-libp2p = { path = "./eth2-libp2p" }

View File

@@ -41,8 +41,8 @@ impl Default for ClientGenesis {
pub struct Config { pub struct Config {
pub data_dir: PathBuf, pub data_dir: PathBuf,
pub db_type: String, pub db_type: String,
db_name: String, pub db_name: String,
freezer_db_path: Option<PathBuf>, pub freezer_db_path: Option<PathBuf>,
pub log_file: PathBuf, pub log_file: PathBuf,
pub spec_constants: String, pub spec_constants: String,
/// If true, the node will use co-ordinated junk for eth1 values. /// If true, the node will use co-ordinated junk for eth1 values.
@@ -69,7 +69,7 @@ impl Default for Config {
db_name: "chain_db".to_string(), db_name: "chain_db".to_string(),
freezer_db_path: None, freezer_db_path: None,
genesis: <_>::default(), genesis: <_>::default(),
network: NetworkConfig::new(), network: NetworkConfig::default(),
rest_api: <_>::default(), rest_api: <_>::default(),
websocket_server: <_>::default(), websocket_server: <_>::default(),
spec_constants: TESTNET_SPEC_CONSTANTS.into(), spec_constants: TESTNET_SPEC_CONSTANTS.into(),
@@ -135,26 +135,6 @@ impl Config {
.ok_or_else(|| "Unable to locate user home directory".to_string())?; .ok_or_else(|| "Unable to locate user home directory".to_string())?;
ensure_dir_exists(path) ensure_dir_exists(path)
} }
/// 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: &slog::Logger) -> Result<(), String> {
if let Some(dir) = args.value_of("datadir") {
self.data_dir = PathBuf::from(dir);
};
if let Some(freezer_dir) = args.value_of("freezer-dir") {
self.freezer_db_path = Some(PathBuf::from(freezer_dir));
}
self.network.apply_cli_args(args)?;
self.rest_api.apply_cli_args(args)?;
self.websocket_server.apply_cli_args(args)?;
Ok(())
}
} }
/// Ensure that the directory at `path` exists, by creating it and all parents if necessary. /// Ensure that the directory at `path` exists, by creating it and all parents if necessary.

View File

@@ -5,7 +5,6 @@ authors = ["Age Manning <Age@AgeManning.com>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
clap = "2.33.0"
hex = "0.3" hex = "0.3"
# rust-libp2p is presently being sourced from a Sigma Prime fork of the # rust-libp2p is presently being sourced from a Sigma Prime fork of the
# `libp2p/rust-libp2p` repository. # `libp2p/rust-libp2p` repository.

View File

@@ -1,4 +1,3 @@
use clap::ArgMatches;
use enr::Enr; use enr::Enr;
use libp2p::gossipsub::{GossipsubConfig, GossipsubConfigBuilder}; use libp2p::gossipsub::{GossipsubConfig, GossipsubConfigBuilder};
use libp2p::Multiaddr; use libp2p::Multiaddr;
@@ -97,84 +96,3 @@ impl Default for Config {
} }
} }
} }
/// Generates a default Config.
impl Config {
pub fn new() -> Self {
Config::default()
}
pub fn apply_cli_args(&mut self, args: &ArgMatches) -> Result<(), String> {
// If a `datadir` has been specified, set the network dir to be inside it.
if let Some(dir) = args.value_of("datadir") {
self.network_dir = PathBuf::from(dir).join("network");
};
// If a network dir has been specified, override the `datadir` definition.
if let Some(dir) = args.value_of("network-dir") {
self.network_dir = PathBuf::from(dir);
};
if let Some(listen_address_str) = args.value_of("listen-address") {
let listen_address = listen_address_str
.parse()
.map_err(|_| format!("Invalid listen address: {:?}", listen_address_str))?;
self.listen_address = listen_address;
self.discovery_address = listen_address;
}
if let Some(max_peers_str) = args.value_of("maxpeers") {
self.max_peers = max_peers_str
.parse::<usize>()
.map_err(|_| format!("Invalid number of max peers: {}", max_peers_str))?;
}
if let Some(port_str) = args.value_of("port") {
let port = port_str
.parse::<u16>()
.map_err(|_| format!("Invalid port: {}", port_str))?;
self.libp2p_port = port;
self.discovery_port = port;
}
if let Some(boot_enr_str) = args.value_of("boot-nodes") {
self.boot_nodes = boot_enr_str
.split(',')
.map(|enr| enr.parse().map_err(|_| format!("Invalid ENR: {}", enr)))
.collect::<Result<Vec<Enr>, _>>()?;
}
if let Some(libp2p_addresses_str) = args.value_of("libp2p-addresses") {
self.libp2p_nodes = libp2p_addresses_str
.split(',')
.map(|multiaddr| {
multiaddr
.parse()
.map_err(|_| format!("Invalid Multiaddr: {}", multiaddr))
})
.collect::<Result<Vec<Multiaddr>, _>>()?;
}
if let Some(topics_str) = args.value_of("topics") {
self.topics = topics_str.split(',').map(|s| s.into()).collect();
}
if let Some(discovery_address_str) = args.value_of("discovery-address") {
self.discovery_address = discovery_address_str
.parse()
.map_err(|_| format!("Invalid discovery address: {:?}", discovery_address_str))?
}
if let Some(disc_port_str) = args.value_of("disc-port") {
self.discovery_port = disc_port_str
.parse::<u16>()
.map_err(|_| format!("Invalid discovery port: {}", disc_port_str))?;
}
if let Some(p2p_priv_key) = args.value_of("p2p-priv-key") {
self.secret_key_hex = Some(p2p_priv_key.to_string());
}
Ok(())
}
}

View File

@@ -22,7 +22,6 @@ eth2_ssz = { path = "../../eth2/utils/ssz" }
eth2_ssz_derive = { path = "../../eth2/utils/ssz_derive" } eth2_ssz_derive = { path = "../../eth2/utils/ssz_derive" }
state_processing = { path = "../../eth2/state_processing" } state_processing = { path = "../../eth2/state_processing" }
types = { path = "../../eth2/types" } types = { path = "../../eth2/types" }
clap = "2.33"
http = "0.1" http = "0.1"
hyper = "0.12" hyper = "0.12"
exit-future = "0.1.4" exit-future = "0.1.4"

View File

@@ -1,4 +1,3 @@
use clap::ArgMatches;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
@@ -50,25 +49,3 @@ impl Default for Config {
} }
} }
} }
impl Config {
pub fn apply_cli_args(&mut self, args: &ArgMatches) -> Result<(), &'static str> {
if args.is_present("no-api") {
self.enabled = false;
}
if let Some(rpc_address) = args.value_of("api-address") {
self.listen_address = rpc_address
.parse::<Ipv4Addr>()
.map_err(|_| "api-address is not a valid IPv4 address.")?;
}
if let Some(rpc_port) = args.value_of("api-port") {
self.port = rpc_port
.parse::<u16>()
.map_err(|_| "api-port is not a valid u16.")?;
}
Ok(())
}
}

View File

@@ -29,20 +29,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
* Network parameters. * Network parameters.
*/ */
.arg( .arg(
Arg::with_name("port-bump") Arg::with_name("zero-ports")
.long("port-bump") .long("zero-ports")
.short("b") .short("z")
.value_name("INCREMENT") .help("Sets all listening TCP/UDP ports to 0, allowing the OS to choose some \
.help("Sets all listening TCP/UDP ports to default values, but with each port increased by \ arbitrary free port number.")
INCREMENT. Useful when starting multiple nodes on a single machine. Using increments \ .takes_value(false),
in multiples of 10 is recommended.")
.takes_value(true),
) )
.arg( .arg(
Arg::with_name("listen-address") Arg::with_name("listen-address")
.long("listen-address") .long("listen-address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("The address lighthouse will listen for UDP and TCP connections. (default 127.0.0.1).") .help("The address lighthouse will listen for UDP and TCP connections.")
.default_value("127.0.0.1")
.takes_value(true) .takes_value(true)
) )
.arg( .arg(
@@ -50,13 +49,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.long("port") .long("port")
.value_name("PORT") .value_name("PORT")
.help("The TCP/UDP port to listen on. The UDP port can be modified by the --discovery-port flag.") .help("The TCP/UDP port to listen on. The UDP port can be modified by the --discovery-port flag.")
.conflicts_with("port-bump") .default_value("9000")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name("maxpeers") Arg::with_name("maxpeers")
.long("maxpeers") .long("maxpeers")
.help("The maximum number of peers (default 10).") .help("The maximum number of peers.")
.default_value("10")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
@@ -72,64 +72,70 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.long("disc-port") .long("disc-port")
.value_name("PORT") .value_name("PORT")
.help("The discovery UDP port.") .help("The discovery UDP port.")
.conflicts_with("port-bump") .default_value("9000")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name("discovery-address") Arg::with_name("discovery-address")
.long("discovery-address") .long("discovery-address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("The IP address to broadcast to other peers on how to reach this node.") .help("The IP address to broadcast to other peers on how to reach this node. \
Default is determined automatically.")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name("topics") Arg::with_name("topics")
.long("topics") .long("topics")
.value_name("STRING") .value_name("STRING")
.help("One or more comma-delimited gossipsub topic strings to subscribe to.") .help("One or more comma-delimited gossipsub topic strings to subscribe to. Default \
is determined automatically.")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name("libp2p-addresses") Arg::with_name("libp2p-addresses")
.long("libp2p-addresses") .long("libp2p-addresses")
.value_name("MULTIADDR") .value_name("MULTIADDR")
.help("One or more comma-delimited multiaddrs to manually connect to a libp2p peer without an ENR.") .help("One or more comma-delimited multiaddrs to manually connect to a libp2p peer \
without an ENR.")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name("p2p-priv-key") Arg::with_name("p2p-priv-key")
.long("p2p-priv-key") .long("p2p-priv-key")
.value_name("HEX") .value_name("HEX")
.help("A secp256k1 secret key, represented as ASCII-encoded hex bytes (with or without 0x prefix).") .help("A secp256k1 secret key, represented as ASCII-encoded hex bytes (with or \
without 0x prefix). Default is either loaded from disk or generated \
automatically.")
.takes_value(true), .takes_value(true),
) )
/* REST API related arguments */ /* REST API related arguments */
.arg( .arg(
Arg::with_name("no-api") Arg::with_name("http")
.long("no-api") .long("http")
.help("Disable RESTful HTTP API server.") .help("Enable RESTful HTTP API server. Disabled by default.")
.takes_value(false), .takes_value(false),
) )
.arg( .arg(
Arg::with_name("api-address") Arg::with_name("http-address")
.long("api-address") .long("http-address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("Set the listen address for the RESTful HTTP API server.") .help("Set the listen address for the RESTful HTTP API server.")
.default_value("127.0.0.1")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
Arg::with_name("api-port") Arg::with_name("http-port")
.long("api-port") .long("http-port")
.value_name("PORT") .value_name("PORT")
.help("Set the listen TCP port for the RESTful HTTP API server.") .help("Set the listen TCP port for the RESTful HTTP API server.")
.conflicts_with("port-bump") .default_value("5052")
.takes_value(true), .takes_value(true),
) )
/* Websocket related arguments */ /* Websocket related arguments */
.arg( .arg(
Arg::with_name("no-ws") Arg::with_name("ws")
.long("no-ws") .long("ws")
.help("Disable websocket server.") .help("Enable the websocket server. Disabled by default.")
.takes_value(false), .takes_value(false),
) )
.arg( .arg(
@@ -137,7 +143,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.long("ws-address") .long("ws-address")
.value_name("ADDRESS") .value_name("ADDRESS")
.help("Set the listen address for the websocket server.") .help("Set the listen address for the websocket server.")
.conflicts_with_all(&["no-ws"]) .default_value("127.0.0.1")
.takes_value(true), .takes_value(true),
) )
.arg( .arg(
@@ -145,16 +151,24 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.long("ws-port") .long("ws-port")
.value_name("PORT") .value_name("PORT")
.help("Set the listen TCP port for the websocket server.") .help("Set the listen TCP port for the websocket server.")
.conflicts_with_all(&["no-ws", "port-bump"]) .default_value("5053")
.takes_value(true), .takes_value(true),
) )
/* /*
* Eth1 Integration * Eth1 Integration
*/ */
.arg(
Arg::with_name("eth1")
.long("eth1")
.help("If present the node will connect to an eth1 node. This is required for \
block production, you must use this flag if you wish to serve a validator.")
.takes_value(false),
)
.arg( .arg(
Arg::with_name("dummy-eth1") Arg::with_name("dummy-eth1")
.long("dummy-eth1") .long("dummy-eth1")
.conflicts_with("eth1")
.help("If present, uses an eth1 backend that generates static dummy data.\ .help("If present, uses an eth1 backend that generates static dummy data.\
Identical to the method used at the 2019 Canada interop.") Identical to the method used at the 2019 Canada interop.")
) )
@@ -166,32 +180,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.takes_value(true) .takes_value(true)
.default_value("http://localhost:8545") .default_value("http://localhost:8545")
) )
.arg(
Arg::with_name("eth1-follow")
.long("eth1-follow")
.value_name("BLOCK_COUNT")
.help("Specifies how many blocks we should cache behind the eth1 head. A larger number means a smaller cache.")
.takes_value(true)
// TODO: set this higher once we're not using testnets all the time.
.default_value("0")
)
.arg(
Arg::with_name("deposit-contract")
.long("deposit-contract")
.short("e")
.value_name("DEPOSIT-CONTRACT")
.help("Specifies the deposit contract address on the Eth1 chain.")
.takes_value(true)
)
.arg(
Arg::with_name("deposit-contract-deploy")
.long("deposit-contract-deploy")
.value_name("BLOCK_NUMBER")
.help("Specifies the block number that the deposit contract was deployed at.")
.takes_value(true)
// TODO: set this higher once we're not using testnets all the time.
.default_value("0")
)
/* /*
* The "testnet" sub-command. * The "testnet" sub-command.
* *
@@ -199,21 +187,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
*/ */
.subcommand(SubCommand::with_name("testnet") .subcommand(SubCommand::with_name("testnet")
.about("Create a new Lighthouse datadir using a testnet strategy.") .about("Create a new Lighthouse datadir using a testnet strategy.")
.arg(
Arg::with_name("eth2-config")
.long("eth2-config")
.value_name("TOML_FILE")
.help("A existing eth2_spec TOML file (e.g., eth2_spec.toml).")
.takes_value(true)
.conflicts_with("spec")
)
.arg(
Arg::with_name("client-config")
.long("client-config")
.value_name("TOML_FILE")
.help("An existing beacon_node TOML file (e.g., beacon_node.toml).")
.takes_value(true)
)
.arg( .arg(
Arg::with_name("random-datadir") Arg::with_name("random-datadir")
.long("random-datadir") .long("random-datadir")
@@ -221,13 +194,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.help("If present, append a random string to the datadir path. Useful for fast development \ .help("If present, append a random string to the datadir path. Useful for fast development \
iteration.") iteration.")
) )
.arg(
Arg::with_name("random-propagation")
.long("random-propagation")
.value_name("INTEGER")
.takes_value(true)
.help("Specifies (as a percentage) the likelihood of propagating blocks and attestations. This should only be used for testing networking elements. The value must like in the range 1-100.")
)
.arg( .arg(
Arg::with_name("force") Arg::with_name("force")
.long("force") .long("force")
@@ -236,33 +202,22 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
backup directory.") backup directory.")
.conflicts_with("random-datadir") .conflicts_with("random-datadir")
) )
.arg(
Arg::with_name("random-propagation")
.long("random-propagation")
.value_name("INTEGER")
.takes_value(true)
.help("Specifies (as a percentage) the likelihood of propagating blocks and \
attestations. This should only be used for testing networking elements. The \
value must like in the range 1-100. Default is 100.")
)
.arg( .arg(
Arg::with_name("slot-time") Arg::with_name("slot-time")
.long("slot-time") .long("slot-time")
.short("t") .short("t")
.value_name("MILLISECONDS") .value_name("MILLISECONDS")
.help("Defines the slot time when creating a new testnet.") .help("Defines the slot time when creating a new testnet. The default is \
) specified by the spec.")
/*
* `boostrap`
*
* Start a new node by downloading genesis and network info from another node via the
* HTTP API.
*/
.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)
.default_value("http://localhost:5052")
.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. This flag is useful \
when port-fowarding is used: you may connect using a different port than \
the one the server is immediately listening on."))
) )
/* /*
* `recent` * `recent`
@@ -282,7 +237,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.short("m") .short("m")
.value_name("MINUTES") .value_name("MINUTES")
.required(true) .required(true)
.default_value("0") .default_value("30")
.help("The maximum number of minutes that will have elapsed before genesis")) .help("The maximum number of minutes that will have elapsed before genesis"))
) )
/* /*
@@ -320,25 +275,14 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.required(true) .required(true)
.help("A file from which to read the state")) .help("A file from which to read the state"))
) )
/*
* `eth1`
*
* Connect to a Sigma Prime testnet.
*/
.subcommand(SubCommand::with_name("eth1")
.about("Connect to a testnet defined by an eth2_testnet directory.")
.arg(Arg::with_name("directory")
.value_name("DIRECTORY")
.index(1)
.help("A directory generated by lcli. Defaults to ~/.lighthouse/testnet. See lcli testnet --help"))
)
/* /*
* `prysm` * `prysm`
* *
* Connect to the Prysmatic Labs testnet. * Connect to the Prysmatic Labs testnet.
*/ */
.subcommand(SubCommand::with_name("prysm") .subcommand(SubCommand::with_name("prysm")
.about("Connect to the Prysmatic Labs testnet on Goerli.") .about("Connect to the Prysmatic Labs testnet on Goerli. Not guaranteed to be \
up-to-date or functioning.")
) )
) )
} }

View File

@@ -1,19 +1,22 @@
use clap::ArgMatches; use clap::ArgMatches;
use client::{ClientConfig, ClientGenesis, Eth2Config}; use client::{ClientConfig, ClientGenesis, Eth2Config};
use eth2_config::{read_from_file, write_to_file}; use eth2_config::{read_from_file, write_to_file};
use eth2_libp2p::{Enr, Multiaddr};
use eth2_testnet::Eth2TestnetDir; use eth2_testnet::Eth2TestnetDir;
use genesis::recent_genesis_time; use genesis::recent_genesis_time;
use lighthouse_bootstrap::Bootstrapper;
use rand::{distributions::Alphanumeric, Rng}; use rand::{distributions::Alphanumeric, Rng};
use slog::{crit, info, warn, Logger}; use slog::{crit, info, Logger};
use std::fs; use std::fs;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use types::{Address, Epoch, Fork}; use types::{Epoch, Fork};
pub const DEFAULT_DATA_DIR: &str = ".lighthouse";
pub const CLIENT_CONFIG_FILENAME: &str = "beacon-node.toml"; pub const CLIENT_CONFIG_FILENAME: &str = "beacon-node.toml";
pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml"; pub const ETH2_CONFIG_FILENAME: &str = "eth2-spec.toml";
pub const ETH2_TESTNET_DIR: &str = "testnet";
pub const BEACON_NODE_DIR: &str = "beacon";
pub const SECONDS_PER_ETH1_BLOCK: u64 = 15;
type Result<T> = std::result::Result<T, String>; type Result<T> = std::result::Result<T, String>;
type Config = (ClientConfig, Eth2Config, Logger); type Config = (ClientConfig, Eth2Config, Logger);
@@ -25,89 +28,375 @@ type Config = (ClientConfig, Eth2Config, Logger);
/// The output of this function depends primarily upon the given `cli_args`, however it's behaviour /// The output of this function depends primarily upon the given `cli_args`, however it's behaviour
/// may be influenced by other external services like the contents of the file system or the /// may be influenced by other external services like the contents of the file system or the
/// response of some remote server. /// response of some remote server.
pub fn get_configs(cli_args: &ArgMatches, core_log: Logger) -> Result<Config> { pub fn get_configs(
cli_args: &ArgMatches,
mut eth2_config: Eth2Config,
core_log: Logger,
) -> Result<Config> {
let log = core_log.clone(); let log = core_log.clone();
let mut builder = ConfigBuilder::new(cli_args, core_log)?; let mut client_config = ClientConfig::default();
// Read the `--datadir` flag.
//
// If it's not present, try and find the home directory (`~`) and push the default data
// directory onto it.
//
// Note: the `config.data_dir` defines the _global_ lighthouse datadir, not just the beacon
// node specific datadir.
let data_dir: PathBuf = cli_args
.value_of("datadir")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".lighthouse").join(BEACON_NODE_DIR)))
.ok_or_else(|| "Unable to find a home directory for the datadir".to_string())?;
client_config.data_dir = data_dir;
// When present, use an eth1 backend that generates deterministic junk.
//
// Useful for running testnets without the overhead of a deposit contract.
if cli_args.is_present("dummy-eth1") { if cli_args.is_present("dummy-eth1") {
builder.client_config.dummy_eth1_backend = true; client_config.dummy_eth1_backend = true;
} }
// When present, attempt to sync to an eth1 node.
//
// Required for block production.
if cli_args.is_present("eth1") {
client_config.sync_eth1_chain = true;
} else {
client_config.sync_eth1_chain = false;
}
/*
* Networking
*/
// If a `datadir` has been specified, set the network dir to be inside it.
if let Some(dir) = cli_args.value_of("datadir") {
client_config.network.network_dir = PathBuf::from(dir).join("network");
};
// If a network dir has been specified, override the `datadir` definition.
if let Some(dir) = cli_args.value_of("network-dir") {
client_config.network.network_dir = PathBuf::from(dir);
};
if let Some(listen_address_str) = cli_args.value_of("listen-address") {
let listen_address = listen_address_str
.parse()
.map_err(|_| format!("Invalid listen address: {:?}", listen_address_str))?;
client_config.network.listen_address = listen_address;
client_config.network.discovery_address = listen_address;
}
if let Some(max_peers_str) = cli_args.value_of("maxpeers") {
client_config.network.max_peers = max_peers_str
.parse::<usize>()
.map_err(|_| format!("Invalid number of max peers: {}", max_peers_str))?;
}
if let Some(port_str) = cli_args.value_of("port") {
let port = port_str
.parse::<u16>()
.map_err(|_| format!("Invalid port: {}", port_str))?;
client_config.network.libp2p_port = port;
client_config.network.discovery_port = port;
}
if let Some(boot_enr_str) = cli_args.value_of("boot-nodes") {
client_config.network.boot_nodes = boot_enr_str
.split(',')
.map(|enr| enr.parse().map_err(|_| format!("Invalid ENR: {}", enr)))
.collect::<Result<Vec<Enr>>>()?;
}
if let Some(libp2p_addresses_str) = cli_args.value_of("libp2p-addresses") {
client_config.network.libp2p_nodes = libp2p_addresses_str
.split(',')
.map(|multiaddr| {
multiaddr
.parse()
.map_err(|_| format!("Invalid Multiaddr: {}", multiaddr))
})
.collect::<Result<Vec<Multiaddr>>>()?;
}
if let Some(topics_str) = cli_args.value_of("topics") {
client_config.network.topics = topics_str.split(',').map(|s| s.into()).collect();
}
if let Some(discovery_address_str) = cli_args.value_of("discovery-address") {
client_config.network.discovery_address = discovery_address_str
.parse()
.map_err(|_| format!("Invalid discovery address: {:?}", discovery_address_str))?
}
if let Some(disc_port_str) = cli_args.value_of("disc-port") {
client_config.network.discovery_port = disc_port_str
.parse::<u16>()
.map_err(|_| format!("Invalid discovery port: {}", disc_port_str))?;
}
if let Some(p2p_priv_key) = cli_args.value_of("p2p-priv-key") {
client_config.network.secret_key_hex = Some(p2p_priv_key.to_string());
}
/*
* Http server
*/
if cli_args.is_present("http") {
client_config.rest_api.enabled = true;
} else {
client_config.rest_api.enabled = false;
}
if let Some(address) = cli_args.value_of("api-address") {
client_config.rest_api.listen_address = address
.parse::<Ipv4Addr>()
.map_err(|_| "api-address is not a valid IPv4 address.")?;
}
if let Some(port) = cli_args.value_of("api-port") {
client_config.rest_api.port = port
.parse::<u16>()
.map_err(|_| "api-port is not a valid u16.")?;
}
/*
* Websocket server
*/
if cli_args.is_present("ws") {
client_config.websocket_server.enabled = true;
} else {
client_config.websocket_server.enabled = false;
}
if let Some(address) = cli_args.value_of("ws-address") {
client_config.websocket_server.listen_address = address
.parse::<Ipv4Addr>()
.map_err(|_| "ws-address is not a valid IPv4 address.")?;
}
if let Some(port) = cli_args.value_of("ws-port") {
client_config.websocket_server.port = port
.parse::<u16>()
.map_err(|_| "ws-port is not a valid u16.")?;
}
/*
* Eth1
*/
// Defines the URL to reach the eth1 node.
if let Some(val) = cli_args.value_of("eth1-endpoint") { if let Some(val) = cli_args.value_of("eth1-endpoint") {
builder.set_eth1_endpoint(val) client_config.eth1.endpoint = val.to_string();
}
if let Some(val) = cli_args.value_of("deposit-contract") {
builder.set_deposit_contract(
val.parse::<Address>()
.map_err(|e| format!("Unable to parse deposit-contract address: {:?}", e))?,
)
}
if let Some(val) = cli_args.value_of("deposit-contract-deploy") {
builder.set_deposit_contract_deploy_block(
val.parse::<u64>()
.map_err(|e| format!("Unable to parse deposit-contract-deploy: {:?}", e))?,
)
}
if let Some(val) = cli_args.value_of("eth1-follow") {
builder.set_eth1_follow(
val.parse::<u64>()
.map_err(|e| format!("Unable to parse follow distance: {:?}", e))?,
)
} }
match cli_args.subcommand() { match cli_args.subcommand() {
("testnet", Some(sub_cmd_args)) => { ("testnet", Some(sub_cmd_args)) => {
process_testnet_subcommand(&mut builder, sub_cmd_args, &log)? process_testnet_subcommand(&mut client_config, &mut eth2_config, sub_cmd_args)?
} }
// No sub-command assumes a resume operation. // No sub-command assumes a resume operation.
_ => { _ => {
info!( info!(
log, log,
"Resuming from existing datadir"; "Resuming from existing datadir";
"path" => format!("{:?}", builder.client_config.data_dir) "path" => format!("{:?}", client_config.data_dir)
); );
// If no primary subcommand was given, start the beacon chain from an existing // If no primary subcommand was given, start the beacon chain from an existing
// database. // database.
builder.set_genesis(ClientGenesis::Resume); client_config.genesis = ClientGenesis::Resume;
// Whilst there is no large testnet or mainnet force the user to specify how they want // Whilst there is no large testnet or mainnet force the user to specify how they want
// to start a new chain (e.g., from a genesis YAML file, another node, etc). // to start a new chain (e.g., from a genesis YAML file, another node, etc).
if !builder.client_config.data_dir.exists() { if !client_config.data_dir.exists() {
return Err( init_new_client(&mut client_config, &mut eth2_config)?
"No datadir found. To start a new beacon chain, see `testnet --help`. \ } else {
Use `--datadir` to specify a different directory" // If the `testnet` command was not provided, attempt to load an existing datadir and
.into(), // continue with an existing chain.
); load_from_datadir(&mut client_config, &mut eth2_config)?
} }
// 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(cli_args) if let Some(freezer_dir) = cli_args.value_of("freezer-dir") {
client_config.freezer_db_path = Some(PathBuf::from(freezer_dir));
}
if eth2_config.spec_constants != client_config.spec_constants {
crit!(log, "Specification constants do not match.";
"client_config" => client_config.spec_constants.to_string(),
"eth2_config" => eth2_config.spec_constants.to_string()
);
return Err("Specification constant mismatch".into());
}
/*
* Zero-ports
*
* Replaces previously set flags.
*/
if cli_args.is_present("zero-ports") {
client_config.network.libp2p_port = 0;
client_config.network.discovery_port = 0;
client_config.rest_api.port = 0;
client_config.websocket_server.port = 0;
}
Ok((client_config, eth2_config, log))
}
/// Load from an existing database.
fn load_from_datadir(client_config: &mut ClientConfig, eth2_config: &mut Eth2Config) -> 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 !client_config.get_data_dir().map_or(false, |d| d.exists()) {
return Err(
"No datadir found. Either create a new testnet or specify a different `--datadir`."
.into(),
);
}
// If there is a path to a database in the config, ensure it exists.
if !client_config
.get_db_path()
.map_or(false, |path| path.exists())
{
return Err(
"No database found in datadir. Use 'testnet -f' to overwrite the existing \
datadir, or specify a different `--datadir`."
.into(),
);
}
let path = client_config.data_dir.join(CLIENT_CONFIG_FILENAME);
*client_config = read_from_file(path.clone())
.map_err(|e| format!("Unable to parse {:?} file: {:?}", path, e))?
.ok_or_else(|| format!("{:?} file does not exist", path))?;
let path = client_config.data_dir.join(ETH2_CONFIG_FILENAME);
*eth2_config = read_from_file(path.clone())
.map_err(|e| format!("Unable to parse {:?} file: {:?}", path, e))?
.ok_or_else(|| format!("{:?} file does not exist", path))?;
Ok(())
}
/// Create a new client with the default configuration.
fn init_new_client(client_config: &mut ClientConfig, eth2_config: &mut Eth2Config) -> Result<()> {
let spec = &mut eth2_config.spec;
spec.min_deposit_amount = 100;
spec.max_effective_balance = 3_200_000_000;
spec.ejection_balance = 1_600_000_000;
spec.effective_balance_increment = 100_000_000;
spec.genesis_fork = Fork {
previous_version: [0; 4],
current_version: [0, 0, 0, 42],
epoch: Epoch::new(0),
};
let eth2_testnet_dir = Eth2TestnetDir::load(client_config.data_dir.join(ETH2_TESTNET_DIR))
.map_err(|e| format!("Unable to open testnet dir: {}", e))?;
client_config.eth1.deposit_contract_address =
format!("{:?}", eth2_testnet_dir.deposit_contract_address()?);
client_config.eth1.deposit_contract_deploy_block =
eth2_testnet_dir.deposit_contract_deploy_block;
spec.min_genesis_time = eth2_testnet_dir.min_genesis_time;
client_config.eth1.follow_distance = 16;
client_config.dummy_eth1_backend = false;
client_config.sync_eth1_chain = true;
client_config.eth1.lowest_cached_block_number = client_config
.eth1
.deposit_contract_deploy_block
.saturating_sub(client_config.eth1.follow_distance * 2);
// Note: these constants _should_ only be used during genesis to determine the genesis
// time. This allows the testnet to start shortly after the time + validator count
// conditions are satisfied, not 1-2 days.
//
// This value must be at least 2x the `ETH1_FOLLOW_DISTANCE` otherwise `all_eth1_data`
// can become a subset of `new_eth1_data`.
//
// See:
// https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/validator/0_beacon-chain-validator.md#eth1-data
// TODO: set from 10 back to 2
spec.seconds_per_day = SECONDS_PER_ETH1_BLOCK * spec.eth1_follow_distance * 2;
client_config.genesis = ClientGenesis::DepositContract;
create_new_datadir(&client_config, &eth2_config)?;
Ok(())
}
/// Writes the configs in `self` to `self.data_dir`.
///
/// Returns an error if `self.data_dir` already exists.
pub fn create_new_datadir(client_config: &ClientConfig, eth2_config: &Eth2Config) -> Result<()> {
if client_config.data_dir.exists() {
return Err(format!(
"Data dir already exists at {:?}",
client_config.data_dir
))?;
}
// Create `datadir` and any non-existing parent directories.
fs::create_dir_all(&client_config.data_dir)
.map_err(|e| format!("Failed to create data dir: {}", e))?;
macro_rules! write_to_file {
($file: ident, $variable: ident) => {
let file = client_config.data_dir.join($file);
if file.exists() {
return Err(format!("Datadir is not clean, {} exists.", $file));
} else {
// Write the onfig to a TOML file in the datadir.
write_to_file(client_config.data_dir.join($file), $variable)
.map_err(|e| format!("Unable to write {} file: {:?}", $file, e))?;
}
};
}
write_to_file!(CLIENT_CONFIG_FILENAME, client_config);
write_to_file!(ETH2_CONFIG_FILENAME, eth2_config);
Ok(())
} }
/// Process the `testnet` CLI subcommand arguments, updating the `builder`. /// Process the `testnet` CLI subcommand arguments, updating the `builder`.
fn process_testnet_subcommand( fn process_testnet_subcommand(
builder: &mut ConfigBuilder, client_config: &mut ClientConfig,
eth2_config: &mut Eth2Config,
cli_args: &ArgMatches, cli_args: &ArgMatches,
log: &Logger,
) -> Result<()> { ) -> Result<()> {
// Specifies that a random datadir should be used.
if cli_args.is_present("random-datadir") { if cli_args.is_present("random-datadir") {
builder.set_random_datadir()?; client_config
.data_dir
.push(format!("random_{}", random_string(6)));
client_config.network.network_dir = client_config.data_dir.join("network");
} }
// Deletes the existing datadir.
if cli_args.is_present("force") { if cli_args.is_present("force") {
builder.clean_datadir()?; if client_config.data_dir.exists() {
fs::remove_dir_all(&client_config.data_dir)
.map_err(|e| format!("Unable to delete existing datadir: {:?}", e))?;
}
} }
// Define a percentage of messages that should be propogated, useful for simulating bad network
// conditions.
//
// WARNING: setting this to anything less than 100 will cause bad behaviour.
if let Some(propagation_percentage_string) = cli_args.value_of("random-propagation") { if let Some(propagation_percentage_string) = cli_args.value_of("random-propagation") {
let percentage = propagation_percentage_string let percentage = propagation_percentage_string
.parse::<u8>() .parse::<u8>()
@@ -115,72 +404,20 @@ fn process_testnet_subcommand(
if percentage > 100 { if percentage > 100 {
return Err(format!("Propagation percentage greater than 100")); return Err(format!("Propagation percentage greater than 100"));
} }
builder.client_config.network.propagation_percentage = Some(percentage); client_config.network.propagation_percentage = Some(percentage);
}
let is_bootstrap = cli_args.subcommand_name() == Some("bootstrap");
if let Some(path_string) = cli_args.value_of("eth2-config") {
if is_bootstrap {
return Err("Cannot supply --eth2-config when using bootstrap".to_string());
}
let path = path_string
.parse::<PathBuf>()
.map_err(|e| format!("Unable to parse eth2-config path: {:?}", e))?;
builder.load_eth2_config(path)?;
} else {
builder.update_spec_from_subcommand(&cli_args)?;
} }
// Modify the `SECONDS_PER_SLOT` "constant".
if let Some(slot_time) = cli_args.value_of("slot-time") { if let Some(slot_time) = cli_args.value_of("slot-time") {
if is_bootstrap {
return Err("Cannot supply --slot-time flag whilst using bootstrap.".into());
}
let slot_time = slot_time let slot_time = slot_time
.parse::<u64>() .parse::<u64>()
.map_err(|e| format!("Unable to parse slot-time: {:?}", e))?; .map_err(|e| format!("Unable to parse slot-time: {:?}", e))?;
builder.set_slot_time(slot_time); eth2_config.spec.milliseconds_per_slot = slot_time;
} }
if let Some(path_string) = cli_args.value_of("client-config") {
let path = path_string
.parse::<PathBuf>()
.map_err(|e| format!("Unable to parse client config path: {:?}", e))?;
builder.load_client_config(path)?;
}
info!(
log,
"Creating new datadir";
"path" => format!("{:?}", builder.client_config.data_dir)
);
// When using the testnet command we listen on all addresses.
builder.set_listen_addresses("0.0.0.0".into())?;
warn!(log, "All services listening on 0.0.0.0");
// Start matching on the second subcommand (e.g., `testnet bootstrap ...`). // Start matching on the second subcommand (e.g., `testnet bootstrap ...`).
match cli_args.subcommand() { match cli_args.subcommand() {
("bootstrap", Some(cli_args)) => {
let server = cli_args
.value_of("server")
.ok_or_else(|| "No bootstrap server specified")?;
let port: Option<u16> = cli_args
.value_of("libp2p-port")
.and_then(|s| s.parse::<u16>().ok());
builder.import_bootstrap_libp2p_address(server, port)?;
builder.import_bootstrap_enr_address(server)?;
builder.import_bootstrap_eth2_config(server)?;
builder.set_genesis(ClientGenesis::RemoteNode {
server: server.to_string(),
port,
})
}
("recent", Some(cli_args)) => { ("recent", Some(cli_args)) => {
let validator_count = cli_args let validator_count = cli_args
.value_of("validator_count") .value_of("validator_count")
@@ -194,12 +431,12 @@ fn process_testnet_subcommand(
.parse::<u64>() .parse::<u64>()
.map_err(|e| format!("Unable to parse minutes: {:?}", e))?; .map_err(|e| format!("Unable to parse minutes: {:?}", e))?;
builder.client_config.dummy_eth1_backend = true; client_config.dummy_eth1_backend = true;
builder.set_genesis(ClientGenesis::Interop { client_config.genesis = ClientGenesis::Interop {
validator_count, validator_count,
genesis_time: recent_genesis_time(minutes), genesis_time: recent_genesis_time(minutes),
}) };
} }
("quick", Some(cli_args)) => { ("quick", Some(cli_args)) => {
let validator_count = cli_args let validator_count = cli_args
@@ -214,12 +451,12 @@ fn process_testnet_subcommand(
.parse::<u64>() .parse::<u64>()
.map_err(|e| format!("Unable to parse genesis time: {:?}", e))?; .map_err(|e| format!("Unable to parse genesis time: {:?}", e))?;
builder.client_config.dummy_eth1_backend = true; client_config.dummy_eth1_backend = true;
builder.set_genesis(ClientGenesis::Interop { client_config.genesis = ClientGenesis::Interop {
validator_count, validator_count,
genesis_time, genesis_time,
}) };
} }
("file", Some(cli_args)) => { ("file", Some(cli_args)) => {
let path = cli_args let path = cli_args
@@ -237,11 +474,10 @@ fn process_testnet_subcommand(
other => return Err(format!("Unknown genesis file format: {}", other)), other => return Err(format!("Unknown genesis file format: {}", other)),
}; };
builder.set_genesis(start_method) client_config.genesis = start_method;
} }
("prysm", Some(_)) => { ("prysm", Some(_)) => {
let mut spec = &mut builder.eth2_config.spec; let mut spec = &mut eth2_config.spec;
let mut client_config = &mut builder.client_config;
spec.min_deposit_amount = 100; spec.min_deposit_amount = 100;
spec.max_effective_balance = 3_200_000_000; spec.max_effective_balance = 3_200_000_000;
@@ -260,58 +496,7 @@ fn process_testnet_subcommand(
client_config.eth1.follow_distance = 16; client_config.eth1.follow_distance = 16;
client_config.dummy_eth1_backend = false; client_config.dummy_eth1_backend = false;
builder.set_genesis(ClientGenesis::DepositContract) client_config.genesis = ClientGenesis::DepositContract;
}
("eth1", Some(_)) => {
let mut spec = &mut builder.eth2_config.spec;
let mut client_config = &mut builder.client_config;
let directory: PathBuf = cli_args
.value_of("datadir")
.map(PathBuf::from)
.unwrap_or_else(|| client_config.data_dir.join("testnet"));
let eth2_testnet_dir = Eth2TestnetDir::load(directory)
.map_err(|e| format!("Unable to open testnet dir: {}", e))?;
spec.min_deposit_amount = 100;
spec.max_effective_balance = 3_200_000_000;
spec.ejection_balance = 1_600_000_000;
spec.effective_balance_increment = 100_000_000;
spec.genesis_fork = Fork {
previous_version: [0; 4],
current_version: [0, 0, 0, 42],
epoch: Epoch::new(0),
};
client_config.eth1.deposit_contract_address =
format!("{:?}", eth2_testnet_dir.deposit_contract_address()?);
client_config.eth1.deposit_contract_deploy_block =
eth2_testnet_dir.deposit_contract_deploy_block;
spec.min_genesis_time = eth2_testnet_dir.min_genesis_time;
client_config.eth1.follow_distance = 16;
client_config.dummy_eth1_backend = false;
client_config.sync_eth1_chain = true;
client_config.eth1.lowest_cached_block_number = client_config
.eth1
.deposit_contract_deploy_block
.saturating_sub(client_config.eth1.follow_distance * 2);
// Note: these constants _should_ only be used during genesis to determine the genesis
// time. This allows the testnet to start shortly after the time + validator count
// conditions are satisfied, not 1-2 days.
//
// This value must be at least 2x the `ETH1_FOLLOW_DISTANCE` otherwise `all_eth1_data`
// can become a subset of `new_eth1_data`.
//
// See:
// https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/validator/0_beacon-chain-validator.md#eth1-data
const SECONDS_PER_ETH1_BLOCK: u64 = 15;
// TODO: set from 10 back to 2
spec.seconds_per_day = SECONDS_PER_ETH1_BLOCK * spec.eth1_follow_distance * 2;
builder.set_genesis(ClientGenesis::DepositContract)
} }
(cmd, Some(_)) => { (cmd, Some(_)) => {
return Err(format!( return Err(format!(
@@ -322,369 +507,11 @@ fn process_testnet_subcommand(
_ => return Err("No testnet method specified. See 'testnet --help'.".into()), _ => return Err("No testnet method specified. See 'testnet --help'.".into()),
}; };
builder.write_configs_to_new_datadir()?; create_new_datadir(&client_config, &eth2_config)?;
Ok(()) Ok(())
} }
/// Allows for building a set of configurations based upon `clap` arguments.
struct ConfigBuilder {
log: Logger,
pub eth2_config: Eth2Config,
pub client_config: ClientConfig,
}
impl ConfigBuilder {
/// Create a new builder with default settings.
pub fn new(cli_args: &ArgMatches, log: Logger) -> Result<Self> {
// 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 = cli_args
.value_of("datadir")
.map(PathBuf::from)
.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())?;
let mut client_config = ClientConfig::default();
client_config.data_dir = data_dir;
Ok(Self {
log,
eth2_config: Eth2Config::minimal(),
client_config,
})
}
/// Clears any configuration files that would interfere with writing new configs.
///
/// Moves the following files in `data_dir` into a backup directory:
///
/// - Client config
/// - Eth2 config
/// - All database directories
pub fn clean_datadir(&mut self) -> Result<()> {
let backup_dir = {
let mut s = String::from("backup_");
s.push_str(&random_string(6));
self.client_config.data_dir.join(s)
};
fs::create_dir_all(&backup_dir)
.map_err(|e| format!("Unable to create config backup dir: {:?}", e))?;
let move_to_backup_dir = |path: &Path| -> Result<()> {
let file_name = path
.file_name()
.ok_or_else(|| "Invalid path found during datadir clean (no filename).")?;
let mut new = path.to_path_buf();
new.pop();
new.push(backup_dir.clone());
new.push(file_name);
let _ = fs::rename(path, new);
Ok(())
};
move_to_backup_dir(&self.client_config.data_dir.join(CLIENT_CONFIG_FILENAME))?;
move_to_backup_dir(&self.client_config.data_dir.join(ETH2_CONFIG_FILENAME))?;
move_to_backup_dir(&self.client_config.create_db_path()?)?;
move_to_backup_dir(&self.client_config.create_freezer_db_path()?)?;
Ok(())
}
pub fn set_eth1_endpoint(&mut self, endpoint: &str) {
self.client_config.eth1.endpoint = endpoint.to_string();
}
pub fn set_deposit_contract(&mut self, deposit_contract: Address) {
self.client_config.eth1.deposit_contract_address = format!("{:?}", deposit_contract);
}
pub fn set_deposit_contract_deploy_block(&mut self, eth1_block_number: u64) {
self.client_config.eth1.deposit_contract_deploy_block = eth1_block_number;
}
pub fn set_eth1_follow(&mut self, distance: u64) {
self.client_config.eth1.follow_distance = distance;
}
pub fn set_genesis(&mut self, method: ClientGenesis) {
self.client_config.genesis = method;
}
/// Import the libp2p address for `server` into the list of libp2p nodes to connect with.
///
/// If `port` is `Some`, it is used as the port for the `Multiaddr`. If `port` is `None`,
/// attempts to connect to the `server` via HTTP and retrieve it's libp2p listen port.
pub fn import_bootstrap_libp2p_address(
&mut self,
server: &str,
port: Option<u16>,
) -> Result<()> {
let bootstrapper = Bootstrapper::connect(server.to_string(), &self.log)?;
if let Some(server_multiaddr) = bootstrapper.best_effort_multiaddr(port) {
info!(
self.log,
"Estimated bootstrapper libp2p address";
"multiaddr" => format!("{:?}", server_multiaddr)
);
self.client_config
.network
.libp2p_nodes
.push(server_multiaddr);
} else {
warn!(
self.log,
"Unable to estimate a bootstrapper libp2p address, this node may not find any peers."
);
};
Ok(())
}
/// Import the enr address for `server` into the list of initial enrs (boot nodes).
pub fn import_bootstrap_enr_address(&mut self, server: &str) -> Result<()> {
let bootstrapper = Bootstrapper::connect(server.to_string(), &self.log)?;
if let Ok(enr) = bootstrapper.enr() {
info!(
self.log,
"Loaded bootstrapper libp2p address";
"enr" => format!("{:?}", enr)
);
self.client_config.network.boot_nodes.push(enr);
} else {
warn!(
self.log,
"Unable to estimate a bootstrapper enr address, this node may not find any peers."
);
};
Ok(())
}
/// 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<()> {
self.client_config
.data_dir
.push(format!("random_{}", random_string(6)));
self.client_config.network.network_dir = self.client_config.data_dir.join("network");
Ok(())
}
/// Imports an `Eth2Config` from `server`, returning an error if this fails.
pub fn import_bootstrap_eth2_config(&mut self, server: &str) -> Result<()> {
let bootstrapper = Bootstrapper::connect(server.to_string(), &self.log)?;
self.update_eth2_config(bootstrapper.eth2_config()?);
Ok(())
}
fn update_eth2_config(&mut self, eth2_config: Eth2Config) {
self.eth2_config = eth2_config;
}
fn set_slot_time(&mut self, milliseconds_per_slot: u64) {
self.eth2_config.spec.milliseconds_per_slot = milliseconds_per_slot;
}
/// 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 in the given `cli_args`.
pub fn update_spec_from_subcommand(&mut self, cli_args: &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 cli_args.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 = cli_args
.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<()> {
let db_exists = self
.client_config
.get_db_path()
.map(|d| d.exists())
.unwrap_or_else(|| false);
// Do not permit creating a new config when the datadir exists.
if db_exists {
return Err("Database already exists. See `-f` or `-r` in `testnet --help`".into());
}
// Create `datadir` and any non-existing parent directories.
fs::create_dir_all(&self.client_config.data_dir).map_err(|e| {
crit!(self.log, "Failed to initialize data dir"; "error" => format!("{}", e));
format!("{}", e)
})?;
let client_config_file = self.client_config.data_dir.join(CLIENT_CONFIG_FILENAME);
if client_config_file.exists() {
return Err(format!(
"Datadir is not clean, {} exists. See `-f` in `testnet --help`.",
CLIENT_CONFIG_FILENAME
));
} else {
// Write the onfig to a TOML file in the datadir.
write_to_file(
self.client_config.data_dir.join(CLIENT_CONFIG_FILENAME),
&self.client_config,
)
.map_err(|e| format!("Unable to write {} file: {:?}", CLIENT_CONFIG_FILENAME, e))?;
}
let eth2_config_file = self.client_config.data_dir.join(ETH2_CONFIG_FILENAME);
if eth2_config_file.exists() {
return Err(format!(
"Datadir is not clean, {} exists. See `-f` in `testnet --help`.",
ETH2_CONFIG_FILENAME
));
} else {
// Write the config to a TOML file in the datadir.
write_to_file(
self.client_config.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
.client_config
.get_data_dir()
.map_or(false, |d| d.exists())
{
return Err(
"No datadir found. Either create a new testnet or specify a different `--datadir`."
.into(),
);
}
// If there is a path to a database in the config, ensure it exists.
if !self
.client_config
.get_db_path()
.map_or(false, |path| path.exists())
{
return Err(
"No database found in datadir. Use 'testnet -f' to overwrite the existing \
datadir, or specify a different `--datadir`."
.into(),
);
}
self.load_eth2_config(self.client_config.data_dir.join(ETH2_CONFIG_FILENAME))?;
self.load_client_config(self.client_config.data_dir.join(CLIENT_CONFIG_FILENAME))?;
Ok(())
}
/// Attempts to load the client config from `path`.
///
/// Returns an error if any files are not found or are invalid.
pub fn load_client_config(&mut self, path: PathBuf) -> Result<()> {
self.client_config = read_from_file::<ClientConfig>(path.clone())
.map_err(|e| format!("Unable to parse {:?} file: {:?}", path, e))?
.ok_or_else(|| format!("{:?} file does not exist", path))?;
Ok(())
}
/// Attempts to load the eth2 config from `path`.
///
/// Returns an error if any files are not found or are invalid.
pub fn load_eth2_config(&mut self, path: PathBuf) -> Result<()> {
self.eth2_config = read_from_file::<Eth2Config>(path.clone())
.map_err(|e| format!("Unable to parse {:?} file: {:?}", path, e))?
.ok_or_else(|| format!("{:?} file does not exist", path))?;
Ok(())
}
/// Sets all listening addresses to the given `addr`.
pub fn set_listen_addresses(&mut self, addr: String) -> Result<()> {
let addr = addr
.parse::<Ipv4Addr>()
.map_err(|e| format!("Unable to parse default listen address: {:?}", e))?;
self.client_config.network.listen_address = addr.into();
self.client_config.rest_api.listen_address = addr;
Ok(())
}
/// Consumes self, returning the configs.
///
/// The supplied `cli_args` should be the base-level `clap` cli_args (i.e., not a subcommand
/// cli_args).
pub fn build(mut self, cli_args: &ArgMatches) -> Result<Config> {
self.client_config.apply_cli_args(cli_args, &mut self.log)?;
if let Some(bump) = cli_args.value_of("port-bump") {
let bump = bump
.parse::<u16>()
.map_err(|e| format!("Unable to parse port bump: {}", e))?;
self.client_config.network.libp2p_port += bump;
self.client_config.network.discovery_port += bump;
self.client_config.rest_api.port += bump;
self.client_config.websocket_server.port += bump;
}
if self.eth2_config.spec_constants != self.client_config.spec_constants {
crit!(self.log, "Specification constants do not match.";
"client_config" => self.client_config.spec_constants.to_string(),
"eth2_config" => self.eth2_config.spec_constants.to_string()
);
return Err("Specification constant mismatch".into());
}
Ok((self.client_config, self.eth2_config, self.log))
}
}
fn random_string(len: usize) -> String { fn random_string(len: usize) -> String {
rand::thread_rng() rand::thread_rng()
.sample_iter(&Alphanumeric) .sample_iter(&Alphanumeric)

View File

@@ -57,15 +57,15 @@ impl<E: EthSpec> ProductionBeaconNode<E> {
) -> impl Future<Item = Self, Error = String> + 'a { ) -> impl Future<Item = Self, Error = String> + 'a {
let log = context.log.clone(); let log = context.log.clone();
// TODO: the eth2 config in the env is being completely ignored. // TODO: the eth2 config in the env is being modified.
// //
// See https://github.com/sigp/lighthouse/issues/602 // See https://github.com/sigp/lighthouse/issues/602
get_configs(&matches, log).into_future().and_then( get_configs(&matches, context.eth2_config.clone(), log)
move |(client_config, eth2_config, _log)| { .into_future()
.and_then(move |(client_config, eth2_config, _log)| {
context.eth2_config = eth2_config; context.eth2_config = eth2_config;
Self::new(context, client_config) Self::new(context, client_config)
}, })
)
} }
/// Starts a new beacon node `Client` in the given `environment`. /// Starts a new beacon node `Client` in the given `environment`.

View File

@@ -7,7 +7,6 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = "2.33.0"
exit-future = "0.1.4" exit-future = "0.1.4"
futures = "0.1.29" futures = "0.1.29"
serde = "1.0.102" serde = "1.0.102"

View File

@@ -1,5 +1,4 @@
use clap::ArgMatches; use serde_derive::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
/// The core configuration of a Lighthouse beacon node. /// The core configuration of a Lighthouse beacon node.
@@ -21,25 +20,3 @@ impl Default for Config {
} }
} }
} }
impl Config {
pub fn apply_cli_args(&mut self, args: &ArgMatches) -> Result<(), &'static str> {
if args.is_present("no-ws") {
self.enabled = false;
}
if let Some(rpc_address) = args.value_of("ws-address") {
self.listen_address = rpc_address
.parse::<Ipv4Addr>()
.map_err(|_| "ws-address is not a valid IPv4 address.")?;
}
if let Some(rpc_port) = args.value_of("ws-port") {
self.port = rpc_port
.parse::<u16>()
.map_err(|_| "ws-port is not a valid u16.")?;
}
Ok(())
}
}

View File

@@ -13,3 +13,4 @@ tempdir = "0.3"
serde = "1.0" serde = "1.0"
serde_json = "^1.0" serde_json = "^1.0"
types = { path = "../../types"} types = { path = "../../types"}
eth2-libp2p = { path = "../../../beacon_node/eth2-libp2p"}

View File

@@ -7,6 +7,7 @@
//! //!
//! https://github.com/sigp/lighthouse/pull/605 //! https://github.com/sigp/lighthouse/pull/605
use eth2_libp2p::Enr;
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::path::PathBuf; use std::path::PathBuf;
use types::Address; use types::Address;
@@ -14,12 +15,14 @@ use types::Address;
pub const ADDRESS_FILE: &str = "deposit_contract.txt"; pub const ADDRESS_FILE: &str = "deposit_contract.txt";
pub const DEPLOY_BLOCK_FILE: &str = "deploy_block.txt"; pub const DEPLOY_BLOCK_FILE: &str = "deploy_block.txt";
pub const MIN_GENESIS_TIME_FILE: &str = "min_genesis_time.txt"; pub const MIN_GENESIS_TIME_FILE: &str = "min_genesis_time.txt";
pub const BOOT_NODES_FILE: &str = "boot_nodes.json";
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub struct Eth2TestnetDir { pub struct Eth2TestnetDir {
deposit_contract_address: String, deposit_contract_address: String,
pub deposit_contract_deploy_block: u64, pub deposit_contract_deploy_block: u64,
pub min_genesis_time: u64, pub min_genesis_time: u64,
pub boot_nodes: Vec<Enr>,
} }
impl Eth2TestnetDir { impl Eth2TestnetDir {
@@ -28,6 +31,7 @@ impl Eth2TestnetDir {
deposit_contract_address: String, deposit_contract_address: String,
deposit_contract_deploy_block: u64, deposit_contract_deploy_block: u64,
min_genesis_time: u64, min_genesis_time: u64,
boot_nodes: Vec<Enr>,
) -> Result<Self, String> { ) -> Result<Self, String> {
if base_dir.exists() { if base_dir.exists() {
return Err("Testnet directory already exists".to_string()); return Err("Testnet directory already exists".to_string());
@@ -36,60 +40,52 @@ impl Eth2TestnetDir {
create_dir_all(&base_dir) create_dir_all(&base_dir)
.map_err(|e| format!("Unable to create testnet directory: {:?}", e))?; .map_err(|e| format!("Unable to create testnet directory: {:?}", e))?;
File::create(base_dir.join(ADDRESS_FILE)) macro_rules! write_to_file {
.map_err(|e| format!("Unable to create {}: {:?}", ADDRESS_FILE, e)) ($file: ident, $variable: ident) => {
.and_then(|file| { File::create(base_dir.join($file))
serde_json::to_writer(file, &deposit_contract_address) .map_err(|e| format!("Unable to create {}: {:?}", $file, e))
.map_err(|e| format!("Unable to write {}: {:?}", ADDRESS_FILE, e)) .and_then(|file| {
})?; serde_json::to_writer(file, &$variable)
.map_err(|e| format!("Unable to write {}: {:?}", $file, e))
})?;
};
}
File::create(base_dir.join(DEPLOY_BLOCK_FILE)) write_to_file!(ADDRESS_FILE, deposit_contract_address);
.map_err(|e| format!("Unable to create {}: {:?}", DEPLOY_BLOCK_FILE, e)) write_to_file!(DEPLOY_BLOCK_FILE, deposit_contract_deploy_block);
.and_then(|file| { write_to_file!(MIN_GENESIS_TIME_FILE, min_genesis_time);
serde_json::to_writer(file, &deposit_contract_deploy_block) write_to_file!(BOOT_NODES_FILE, boot_nodes);
.map_err(|e| format!("Unable to write {}: {:?}", DEPLOY_BLOCK_FILE, e))
})?;
File::create(base_dir.join(MIN_GENESIS_TIME_FILE))
.map_err(|e| format!("Unable to create {}: {:?}", MIN_GENESIS_TIME_FILE, e))
.and_then(|file| {
serde_json::to_writer(file, &min_genesis_time)
.map_err(|e| format!("Unable to write {}: {:?}", MIN_GENESIS_TIME_FILE, e))
})?;
Ok(Self { Ok(Self {
deposit_contract_address, deposit_contract_address,
deposit_contract_deploy_block, deposit_contract_deploy_block,
min_genesis_time, min_genesis_time,
boot_nodes,
}) })
} }
pub fn load(base_dir: PathBuf) -> Result<Self, String> { pub fn load(base_dir: PathBuf) -> Result<Self, String> {
let deposit_contract_address = File::open(base_dir.join(ADDRESS_FILE)) macro_rules! load_from_file {
.map_err(|e| format!("Unable to open {}: {:?}", ADDRESS_FILE, e)) ($file: ident) => {
.and_then(|file| { File::open(base_dir.join($file))
serde_json::from_reader(file) .map_err(|e| format!("Unable to open {}: {:?}", $file, e))
.map_err(|e| format!("Unable to parse {}: {:?}", ADDRESS_FILE, e)) .and_then(|file| {
})?; serde_json::from_reader(file)
.map_err(|e| format!("Unable to parse {}: {:?}", $file, e))
})?;
};
}
let deposit_contract_deploy_block = File::open(base_dir.join(DEPLOY_BLOCK_FILE)) let deposit_contract_address = load_from_file!(ADDRESS_FILE);
.map_err(|e| format!("Unable to open {}: {:?}", DEPLOY_BLOCK_FILE, e)) let deposit_contract_deploy_block = load_from_file!(DEPLOY_BLOCK_FILE);
.and_then(|file| { let min_genesis_time = load_from_file!(MIN_GENESIS_TIME_FILE);
serde_json::from_reader(file) let boot_nodes = load_from_file!(BOOT_NODES_FILE);
.map_err(|e| format!("Unable to parse {}: {:?}", DEPLOY_BLOCK_FILE, e))
})?;
let min_genesis_time = File::open(base_dir.join(MIN_GENESIS_TIME_FILE))
.map_err(|e| format!("Unable to open {}: {:?}", MIN_GENESIS_TIME_FILE, e))
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|e| format!("Unable to parse {}: {:?}", MIN_GENESIS_TIME_FILE, e))
})?;
Ok(Self { Ok(Self {
deposit_contract_address, deposit_contract_address,
deposit_contract_deploy_block, deposit_contract_deploy_block,
min_genesis_time, min_genesis_time,
boot_nodes,
}) })
} }
@@ -122,6 +118,8 @@ mod tests {
deposit_contract_address.clone(), deposit_contract_address.clone(),
deposit_contract_deploy_block, deposit_contract_deploy_block,
min_genesis_time, min_genesis_time,
// TODO: add some Enr for testing.
vec![],
) )
.expect("should create struct"); .expect("should create struct");

View File

@@ -57,7 +57,7 @@ fn main() {
.short("d") .short("d")
.value_name("DIR") .value_name("DIR")
.global(true) .global(true)
.help("Data directory for keys and databases.") .help("Data directory for lighthouse keys and databases.")
.takes_value(true), .takes_value(true),
) )
.subcommand(beacon_node::cli_app()) .subcommand(beacon_node::cli_app())