Add support for multiple testnet flags (#1396)

## Issue Addressed

NA

## Proposed Changes

Allows for multiple "hardcoded" testnets.

## Additional Info

This PR is incomplete.

## TODO

- [x] Add flag to CLI, integrate with rest of Lighthouse.


Co-authored-by: Pawan Dhananjay <pawandhananjay@gmail.com>
Co-authored-by: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
Paul Hauner
2020-07-29 06:39:29 +00:00
parent 395d99ce03
commit 36d3d37cb4
16 changed files with 353 additions and 138 deletions

View File

@@ -14,20 +14,24 @@ pub const BAD_TESTNET_DIR_MESSAGE: &str = "The hard-coded testnet directory was
/// Attempts to load the testnet dir at the path if `name` is in `matches`, returning an error if
/// the path cannot be found or the testnet dir is invalid.
///
/// If `name` is not in `matches`, attempts to return the "hard coded" testnet dir.
pub fn parse_testnet_dir_with_hardcoded_default<E: EthSpec>(
pub fn parse_testnet_dir<E: EthSpec>(
matches: &ArgMatches,
name: &'static str,
) -> Result<Option<Eth2TestnetConfig<E>>, String> {
if let Some(path) = parse_optional::<PathBuf>(matches, name)? {
Eth2TestnetConfig::load(path.clone())
.map_err(|e| format!("Unable to open testnet dir at {:?}: {}", path, e))
.map(Some)
} else {
Eth2TestnetConfig::hard_coded()
.map_err(|e| format!("{} Error : {}", BAD_TESTNET_DIR_MESSAGE, e))
}
let path = parse_required::<PathBuf>(matches, name)?;
Eth2TestnetConfig::load(path.clone())
.map_err(|e| format!("Unable to open testnet dir at {:?}: {}", path, e))
.map(Some)
}
/// Attempts to load a hardcoded network config if `name` is in `matches`, returning an error if
/// the name is not a valid network name.
pub fn parse_hardcoded_network<E: EthSpec>(
matches: &ArgMatches,
name: &str,
) -> Result<Option<Eth2TestnetConfig<E>>, String> {
let network_name = parse_required::<String>(matches, name)?;
Eth2TestnetConfig::constant(network_name.as_str())
}
/// If `name` is in `matches`, parses the value as a path. Otherwise, attempts to find the user's
@@ -52,7 +56,7 @@ pub fn parse_path_with_default_in_home_dir(
/// Returns the value of `name` or an error if it is not in `matches` or does not parse
/// successfully using `std::string::FromStr`.
pub fn parse_required<T>(matches: &ArgMatches, name: &'static str) -> Result<T, String>
pub fn parse_required<T>(matches: &ArgMatches, name: &str) -> Result<T, String>
where
T: FromStr,
<T as FromStr>::Err: std::fmt::Display,
@@ -62,7 +66,7 @@ where
/// Returns the value of `name` (if present) or an error if it does not parse successfully using
/// `std::string::FromStr`.
pub fn parse_optional<T>(matches: &ArgMatches, name: &'static str) -> Result<Option<T>, String>
pub fn parse_optional<T>(matches: &ArgMatches, name: &str) -> Result<Option<T>, String>
where
T: FromStr,
<T as FromStr>::Err: std::fmt::Display,

View File

@@ -1,4 +1,6 @@
use serde_derive::{Deserialize, Serialize};
use std::env;
use std::path::PathBuf;
use types::ChainSpec;
/// The core configuration of a Lighthouse beacon node.
@@ -41,6 +43,86 @@ impl Eth2Config {
}
}
/// A directory that can be built by downloading files via HTTP.
///
/// Used by the `eth2_testnet_config` crate to initialize testnet directories during build and
/// access them at runtime.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Eth2NetDirectory<'a> {
pub name: &'a str,
pub unique_id: &'a str,
pub commit: &'a str,
pub url_template: &'a str,
pub genesis_is_known: bool,
}
impl<'a> Eth2NetDirectory<'a> {
/// The directory that should be used to store files downloaded for this net.
pub fn dir(&self) -> PathBuf {
env::var("CARGO_MANIFEST_DIR")
.expect("should know manifest dir")
.parse::<PathBuf>()
.expect("should parse manifest dir as path")
.join(self.unique_id)
}
}
#[macro_export]
macro_rules! unique_id {
($name: tt, $commit: tt, $genesis_is_known: tt) => {
concat!("testnet_", $name, "_", $commit, "_", $genesis_is_known);
};
}
macro_rules! define_net {
($title: ident, $macro_title: tt, $name: tt, $commit: tt, $url_template: tt, $genesis_is_known: tt) => {
#[macro_use]
pub mod $title {
use super::*;
pub const ETH2_NET_DIR: Eth2NetDirectory = Eth2NetDirectory {
name: $name,
unique_id: unique_id!($name, $commit, $genesis_is_known),
commit: $commit,
url_template: $url_template,
genesis_is_known: $genesis_is_known,
};
// A wrapper around `std::include_bytes` which includes a file from a specific testnet
// directory. Used by upstream crates to import files at compile time.
#[macro_export]
macro_rules! $macro_title {
($base_dir: tt, $filename: tt) => {
include_bytes!(concat!(
$base_dir,
unique_id!($name, $commit, $genesis_is_known),
"/",
$filename
))
};
}
}
};
}
define_net!(
altona,
include_altona_file,
"altona",
"a94e00c1a03df851f960fcf44a79f2a6b1d29af1",
"https://raw.githubusercontent.com/sigp/witti/{{ commit }}/altona/lighthouse/{{ file }}",
true
);
define_net!(
medalla,
include_medalla_file,
"medalla",
"b21fef76ddf472c6cea62d5c98b678033a9b195a",
"https://raw.githubusercontent.com/sigp/witti/{{ commit }}/medalla/{{ file }}",
false
);
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -8,6 +8,9 @@ build = "build.rs"
[build-dependencies]
reqwest = { version = "0.10.4", features = ["blocking"] }
eth2_config = { path = "../eth2_config"}
handlebars = "3.3.0"
serde_json = "1.0.56"
[dev-dependencies]
tempdir = "0.3.7"
@@ -18,3 +21,4 @@ serde_yaml = "0.8.11"
types = { path = "../../consensus/types"}
enr = { version = "0.1.0", features = ["libsecp256k1", "ed25519"] }
eth2_ssz = "0.1.2"
eth2_config = { path = "../eth2_config"}

View File

@@ -1,48 +1,61 @@
//! Downloads a testnet configuration from Github.
use std::env;
use eth2_config::{altona, medalla, Eth2NetDirectory};
use handlebars::Handlebars;
use serde_json::json;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
const TESTNET_ID: &str = "altona-v3";
const ETH2_NET_DIRS: &[Eth2NetDirectory<'static>] = &[altona::ETH2_NET_DIR, medalla::ETH2_NET_DIR];
fn main() {
if !base_dir().exists() {
std::fs::create_dir_all(base_dir())
.unwrap_or_else(|_| panic!("Unable to create {:?}", base_dir()));
for testnet in ETH2_NET_DIRS {
let testnet_dir = testnet.dir();
match get_all_files() {
Ok(()) => (),
Err(e) => {
std::fs::remove_dir_all(base_dir()).unwrap_or_else(|_| panic!(
"{}. Failed to remove {:?}, please remove the directory manually because it may contains incomplete testnet data.",
e,
base_dir(),
));
panic!(e);
if !testnet_dir.exists() {
std::fs::create_dir_all(&testnet_dir)
.unwrap_or_else(|_| panic!("Unable to create {:?}", testnet_dir));
match get_all_files(testnet) {
Ok(()) => (),
Err(e) => {
std::fs::remove_dir_all(&testnet_dir).unwrap_or_else(|_| panic!(
"{}. Failed to remove {:?}, please remove the directory manually because it may contains incomplete testnet data.",
e,
testnet_dir,
));
panic!(e);
}
}
}
}
}
pub fn get_all_files() -> Result<(), String> {
get_file("boot_enr.yaml")?;
get_file("config.yaml")?;
get_file("deploy_block.txt")?;
get_file("deposit_contract.txt")?;
get_file("genesis.ssz")?;
fn get_all_files(testnet: &Eth2NetDirectory<'static>) -> Result<(), String> {
get_file(testnet, "boot_enr.yaml")?;
get_file(testnet, "config.yaml")?;
get_file(testnet, "deploy_block.txt")?;
get_file(testnet, "deposit_contract.txt")?;
if testnet.genesis_is_known {
get_file(testnet, "genesis.ssz")?;
} else {
File::create(testnet.dir().join("genesis.ssz")).unwrap();
}
Ok(())
}
pub fn get_file(filename: &str) -> Result<(), String> {
let url = format!(
"https://raw.githubusercontent.com/sigp/witti/a94e00c1a03df851f960fcf44a79f2a6b1d29af1/altona/lighthouse/{}",
filename
);
fn get_file(testnet: &Eth2NetDirectory, filename: &str) -> Result<(), String> {
let url = Handlebars::new()
.render_template(
testnet.url_template,
&json!({"commit": testnet.commit, "file": filename}),
)
.unwrap();
let path = testnet.dir().join(filename);
let path = base_dir().join(filename);
let mut file =
File::create(path).map_err(|e| format!("Failed to create {}: {:?}", filename, e))?;
@@ -65,11 +78,3 @@ pub fn get_file(filename: &str) -> Result<(), String> {
Ok(())
}
fn base_dir() -> PathBuf {
env::var("CARGO_MANIFEST_DIR")
.expect("should know manifest dir")
.parse::<PathBuf>()
.expect("should parse manifest dir as path")
.join(TESTNET_ID)
}

View File

@@ -6,6 +6,8 @@
//! others. We are unable to conform to the repo until we have the following PR merged:
//!
//! https://github.com/sigp/lighthouse/pull/605
//!
use eth2_config::{include_altona_file, include_medalla_file, unique_id};
use enr::{CombinedKey, Enr};
use ssz::{Decode, Encode};
@@ -20,16 +22,40 @@ pub const BOOT_ENR_FILE: &str = "boot_enr.yaml";
pub const GENESIS_STATE_FILE: &str = "genesis.ssz";
pub const YAML_CONFIG_FILE: &str = "config.yaml";
/// The name of the testnet to hardcode.
///
/// Should be set to `None` when no existing testnet is compatible with the codebase.
pub const HARDCODED_TESTNET: Option<&str> = Some("altona-v3");
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct HardcodedNet {
pub unique_id: &'static str,
pub name: &'static str,
pub genesis_is_known: bool,
pub yaml_config: &'static [u8],
pub deploy_block: &'static [u8],
pub boot_enr: &'static [u8],
pub deposit_contract_address: &'static [u8],
pub genesis_state: &'static [u8],
}
pub const HARDCODED_YAML_CONFIG: &[u8] = include_bytes!("../altona-v3/config.yaml");
pub const HARDCODED_DEPLOY_BLOCK: &[u8] = include_bytes!("../altona-v3/deploy_block.txt");
pub const HARDCODED_DEPOSIT_CONTRACT: &[u8] = include_bytes!("../altona-v3/deposit_contract.txt");
pub const HARDCODED_GENESIS_STATE: &[u8] = include_bytes!("../altona-v3/genesis.ssz");
pub const HARDCODED_BOOT_ENR: &[u8] = include_bytes!("../altona-v3/boot_enr.yaml");
macro_rules! define_net {
($mod: ident, $include_file: tt) => {{
use eth2_config::$mod::ETH2_NET_DIR;
HardcodedNet {
unique_id: ETH2_NET_DIR.unique_id,
name: ETH2_NET_DIR.name,
genesis_is_known: ETH2_NET_DIR.genesis_is_known,
yaml_config: $include_file!("../", "config.yaml"),
deploy_block: $include_file!("../", "deploy_block.txt"),
boot_enr: $include_file!("../", "boot_enr.yaml"),
deposit_contract_address: $include_file!("../", "deposit_contract.txt"),
genesis_state: $include_file!("../", "genesis.ssz"),
}
}};
}
const ALTONA: HardcodedNet = define_net!(altona, include_altona_file);
const MEDALLA: HardcodedNet = define_net!(medalla, include_medalla_file);
const HARDCODED_NETS: &[HardcodedNet] = &[ALTONA, MEDALLA];
pub const DEFAULT_HARDCODED_TESTNET: &str = "medalla";
/// Specifies an Eth2 testnet.
///
@@ -44,34 +70,46 @@ pub struct Eth2TestnetConfig<E: EthSpec> {
}
impl<E: EthSpec> Eth2TestnetConfig<E> {
/// Creates the `Eth2TestnetConfig` that was included in the binary at compile time. This can be
/// considered the default Lighthouse testnet.
///
/// Returns an error if those included bytes are invalid (this is unlikely).
/// Returns `None` if the hardcoded testnet is disabled.
pub fn hard_coded() -> Result<Option<Self>, String> {
if HARDCODED_TESTNET.is_some() {
Ok(Some(Self {
deposit_contract_address: serde_yaml::from_reader(HARDCODED_DEPOSIT_CONTRACT)
.map_err(|e| format!("Unable to parse contract address: {:?}", e))?,
deposit_contract_deploy_block: serde_yaml::from_reader(HARDCODED_DEPLOY_BLOCK)
.map_err(|e| format!("Unable to parse deploy block: {:?}", e))?,
boot_enr: Some(
serde_yaml::from_reader(HARDCODED_BOOT_ENR)
.map_err(|e| format!("Unable to parse boot enr: {:?}", e))?,
),
genesis_state: Some(
BeaconState::from_ssz_bytes(HARDCODED_GENESIS_STATE)
.map_err(|e| format!("Unable to parse genesis state: {:?}", e))?,
),
yaml_config: Some(
serde_yaml::from_reader(HARDCODED_YAML_CONFIG)
.map_err(|e| format!("Unable to parse genesis state: {:?}", e))?,
),
}))
/// Returns the default hard coded testnet.
pub fn hard_coded_default() -> Result<Option<Self>, String> {
Self::constant(DEFAULT_HARDCODED_TESTNET)
}
/// When Lighthouse is built it includes zero or more "hardcoded" network specifications. This
/// function allows for instantiating one of these nets by name.
pub fn constant(name: &str) -> Result<Option<Self>, String> {
HARDCODED_NETS
.iter()
.find(|net| net.name == name)
.map(Self::from_hardcoded_net)
.transpose()
}
/// Instantiates `Self` from a `HardcodedNet`.
fn from_hardcoded_net(net: &HardcodedNet) -> Result<Self, String> {
let genesis_state = if net.genesis_state.is_empty() {
None
} else {
Ok(None)
}
Some(
BeaconState::from_ssz_bytes(net.genesis_state)
.map_err(|e| format!("Unable to parse genesis state: {:?}", e))?,
)
};
Ok(Self {
deposit_contract_address: serde_yaml::from_reader(net.deposit_contract_address)
.map_err(|e| format!("Unable to parse contract address: {:?}", e))?,
deposit_contract_deploy_block: serde_yaml::from_reader(net.deploy_block)
.map_err(|e| format!("Unable to parse deploy block: {:?}", e))?,
boot_enr: Some(
serde_yaml::from_reader(net.boot_enr)
.map_err(|e| format!("Unable to parse boot enr: {:?}", e))?,
),
genesis_state,
yaml_config: Some(
serde_yaml::from_reader(net.yaml_config)
.map_err(|e| format!("Unable to parse yaml config: {:?}", e))?,
),
})
}
// Write the files to the directory.
@@ -215,13 +253,10 @@ mod tests {
type E = MainnetEthSpec;
#[test]
fn hard_coded_works() {
if let Some(dir) =
Eth2TestnetConfig::<E>::hard_coded().expect("should decode hard_coded params")
{
assert!(dir.boot_enr.is_some());
assert!(dir.genesis_state.is_some());
assert!(dir.yaml_config.is_some());
fn hard_coded_nets_work() {
for net in HARDCODED_NETS {
let config = Eth2TestnetConfig::<E>::from_hardcoded_net(net).unwrap();
assert_eq!(config.genesis_state.is_some(), net.genesis_is_known);
}
}