Add "new-testnet" command to lcli (#853)

* Add new command to lcli

* Add lcli to dockerfile

* Add min validator count param

* Fix bug in arg parsing

* Fix 0x address prefix issue

* Add effective balance increment

* Add ejection balance

* Fix PR comments
This commit is contained in:
Paul Hauner 2020-03-04 14:28:02 +11:00 committed by GitHub
parent 871163aecc
commit ca0314ee55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 257 additions and 102 deletions

View File

@ -1,6 +1,7 @@
FROM rust:1.41.0 AS builder
COPY . lighthouse
RUN cd lighthouse && make && cargo clean
RUN cd lighthouse && make
RUN cd lighthouse && cargo install --path lcli
FROM debian:buster-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
@ -9,3 +10,4 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/lighthouse /usr/local/bin/lighthouse
COPY --from=builder /usr/local/cargo/bin/lcli /usr/local/bin/lcli

View File

@ -1,44 +1,18 @@
use clap::ArgMatches;
use environment::Environment;
use eth1_test_rig::DepositContract;
use eth2_testnet_config::Eth2TestnetConfig;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use types::{ChainSpec, EthSpec, YamlConfig};
use types::EthSpec;
use web3::{transports::Http, Web3};
pub const SECONDS_PER_ETH1_BLOCK: u64 = 15;
pub fn run<T: EthSpec>(mut env: Environment<T>, matches: &ArgMatches) -> Result<(), String> {
let min_genesis_time = matches
.value_of("min-genesis-time")
.ok_or_else(|| "min_genesis_time not specified")?
.parse::<u64>()
.map_err(|e| format!("Failed to parse min_genesis_time: {}", e))?;
let min_genesis_active_validator_count = matches
.value_of("min-genesis-active-validator-count")
.ok_or_else(|| "min-genesis-active-validator-count not specified")?
.parse::<u64>()
.map_err(|e| format!("Failed to parse min-genesis-active-validator-count: {}", e))?;
let confirmations = matches
.value_of("confirmations")
.ok_or_else(|| "Confirmations not specified")?
.parse::<usize>()
.map_err(|e| format!("Failed to parse confirmations: {}", e))?;
let output_dir = matches
.value_of("output")
.ok_or_else(|| ())
.and_then(|output| output.parse::<PathBuf>().map_err(|_| ()))
.unwrap_or_else(|_| {
dirs::home_dir()
.map(|home| home.join(".lighthouse").join("testnet"))
.expect("should locate home directory")
});
let password = parse_password(matches)?;
let endpoint = matches
@ -53,10 +27,6 @@ pub fn run<T: EthSpec>(mut env: Environment<T>, matches: &ArgMatches) -> Result<
})?;
let web3 = Web3::new(transport);
if output_dir.exists() {
return Err("Output directory already exists".to_string());
}
// It's unlikely that this will be the _actual_ deployment block, however it'll be close
// enough to serve our purposes.
//
@ -86,55 +56,14 @@ pub fn run<T: EthSpec>(mut env: Environment<T>, matches: &ArgMatches) -> Result<
.map_err(|e| format!("Failed to deploy contract: {}", e))?;
info!(
"Deposit contract deployed. address: {}, min_genesis_time: {}, deploy_block: {}",
"Deposit contract deployed. address: {}, deploy_block: {}",
deposit_contract.address(),
min_genesis_time,
deploy_block
);
info!("Writing config to {:?}", output_dir);
let mut spec = lighthouse_testnet_spec(env.core_context().eth2_config.spec);
spec.min_genesis_time = min_genesis_time;
spec.min_genesis_active_validator_count = min_genesis_active_validator_count;
let testnet_config: Eth2TestnetConfig<T> = Eth2TestnetConfig {
deposit_contract_address: deposit_contract.address(),
deposit_contract_deploy_block: deploy_block.as_u64(),
boot_enr: None,
genesis_state: None,
yaml_config: Some(YamlConfig::from_spec::<T>(&spec)),
};
testnet_config.write_to_file(output_dir)?;
Ok(())
}
/// Modfies the specification to better suit present-capacity testnets.
pub fn lighthouse_testnet_spec(mut spec: ChainSpec) -> ChainSpec {
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.eth1_follow_distance = 16;
// This value must be at least 2x the `ETH1_FOLLOW_DISTANCE` otherwise `all_eth1_data` can
// become a subset of `new_eth1_data` which may result in an Exception in the spec
// implementation.
//
// This value determines the delay between the eth1 block that triggers genesis and the first
// slot of that new chain.
//
// With a follow distance of 16, this is 40mins.
spec.min_genesis_delay = SECONDS_PER_ETH1_BLOCK * spec.eth1_follow_distance * 2 * 5;
spec.genesis_fork_version = [1, 3, 3, 7];
spec
}
pub fn parse_password(matches: &ArgMatches) -> Result<Option<String>, String> {
if let Some(password_path) = matches.value_of("password") {
Ok(Some(

85
lcli/src/helpers.rs Normal file
View File

@ -0,0 +1,85 @@
use clap::ArgMatches;
use hex;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use types::Address;
pub fn time_now() -> Result<u64, String> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.map_err(|e| format!("Unable to get time: {:?}", e))
}
pub fn parse_path_with_default_in_home_dir(
matches: &ArgMatches,
name: &'static str,
default: PathBuf,
) -> Result<PathBuf, String> {
matches
.value_of(name)
.map(|dir| {
dir.parse::<PathBuf>()
.map_err(|e| format!("Unable to parse {}: {}", name, e))
})
.unwrap_or_else(|| {
dirs::home_dir()
.map(|home| home.join(default))
.ok_or_else(|| format!("Unable to locate home directory. Try specifying {}", name))
})
}
pub fn parse_u64(matches: &ArgMatches, name: &'static str) -> Result<u64, String> {
matches
.value_of(name)
.ok_or_else(|| format!("{} not specified", name))?
.parse::<u64>()
.map_err(|e| format!("Unable to parse {}: {}", name, e))
}
pub fn parse_u64_opt(matches: &ArgMatches, name: &'static str) -> Result<Option<u64>, String> {
matches
.value_of(name)
.map(|val| {
val.parse::<u64>()
.map_err(|e| format!("Unable to parse {}: {}", name, e))
})
.transpose()
}
pub fn parse_address(matches: &ArgMatches, name: &'static str) -> Result<Address, String> {
matches
.value_of(name)
.ok_or_else(|| format!("{} not specified", name))
.and_then(|val| {
if val.starts_with("0x") {
val[2..]
.parse()
.map_err(|e| format!("Unable to parse {}: {:?}", name, e))
} else {
Err(format!("Unable to parse {}, must have 0x prefix", name))
}
})
}
pub fn parse_fork_opt(matches: &ArgMatches, name: &'static str) -> Result<Option<[u8; 4]>, String> {
matches
.value_of(name)
.map(|val| {
if val.starts_with("0x") {
let vec = hex::decode(&val[2..])
.map_err(|e| format!("Unable to parse {} as hex: {:?}", name, e))?;
if vec.len() != 4 {
Err(format!("{} must be exactly 4 bytes", name))
} else {
let mut arr = [0; 4];
arr.copy_from_slice(&vec);
Ok(arr)
}
} else {
Err(format!("Unable to parse {}, must have 0x prefix", name))
}
})
.transpose()
}

View File

@ -4,7 +4,9 @@ extern crate log;
mod change_genesis_time;
mod deploy_deposit_contract;
mod eth1_genesis;
mod helpers;
mod interop_genesis;
mod new_testnet;
mod parse_hex;
mod refund_deposit_contract;
mod transition_blocks;
@ -112,34 +114,7 @@ fn main() {
.subcommand(
SubCommand::with_name("deploy-deposit-contract")
.about(
"Deploy an eth1 deposit contract and create a ~/.lighthouse/testnet directory \
(unless another directory is specified).",
)
.arg(
Arg::with_name("output")
.short("o")
.long("output")
.value_name("PATH")
.takes_value(true)
.help("The output directory. Defaults to ~/.lighthouse/testnet"),
)
.arg(
Arg::with_name("min-genesis-time")
.short("t")
.long("min-genesis-time")
.value_name("UNIX_EPOCH_SECONDS")
.takes_value(true)
.default_value("0")
.help("The MIN_GENESIS_TIME constant."),
)
.arg(
Arg::with_name("min-genesis-active-validator-count")
.short("v")
.long("min-genesis-active-validator-count")
.value_name("INTEGER")
.takes_value(true)
.default_value("64")
.help("The MIN_GENESIS_ACTIVE_VALIDATOR_COUNT constant."),
"Deploy a testing eth1 deposit contract.",
)
.arg(
Arg::with_name("eth1-endpoint")
@ -281,6 +256,109 @@ fn main() {
.help("The value for state.genesis_time."),
)
)
.subcommand(
SubCommand::with_name("new-testnet")
.about(
"Produce a new testnet directory.",
)
.arg(
Arg::with_name("testnet-dir")
.long("testnet-dir")
.value_name("DIRECTORY")
.takes_value(true)
.help("The output path for the new testnet directory. Defaults to ~/.lighthouse/testnet"),
)
.arg(
Arg::with_name("min-genesis-time")
.long("min-genesis-time")
.value_name("UNIX_SECONDS")
.takes_value(true)
.help("The minimum permitted genesis time. For non-eth1 testnets will be
the genesis time. Defaults to now."),
)
.arg(
Arg::with_name("min-genesis-active-validator-count")
.long("min-genesis-active-validator-count")
.value_name("INTEGER")
.takes_value(true)
.default_value("16384")
.help("The number of validators required to trigger eth2 genesis."),
)
.arg(
Arg::with_name("min-genesis-delay")
.long("min-genesis-delay")
.value_name("SECONDS")
.takes_value(true)
.default_value("3600") // 10 minutes
.help("The delay between sufficient eth1 deposits and eth2 genesis."),
)
.arg(
Arg::with_name("min-deposit-amount")
.long("min-deposit-amount")
.value_name("GWEI")
.takes_value(true)
.default_value("100000000") // 0.1 Eth
.help("The minimum permitted deposit amount."),
)
.arg(
Arg::with_name("max-effective-balance")
.long("max-effective-balance")
.value_name("GWEI")
.takes_value(true)
.default_value("3200000000") // 3.2 Eth
.help("The amount required to become a validator."),
)
.arg(
Arg::with_name("effective-balance-increment")
.long("effective-balance-increment")
.value_name("GWEI")
.takes_value(true)
.default_value("100000000") // 0.1 Eth
.help("The steps in effective balance calculation."),
)
.arg(
Arg::with_name("ejection-balance")
.long("ejection-balance")
.value_name("GWEI")
.takes_value(true)
.default_value("1600000000") // 1.6 Eth
.help("The balance at which a validator gets ejected."),
)
.arg(
Arg::with_name("eth1-follow-distance")
.long("eth1-follow-distance")
.value_name("ETH1_BLOCKS")
.takes_value(true)
.default_value("16")
.help("The distance to follow behind the eth1 chain head."),
)
.arg(
Arg::with_name("genesis-fork-version")
.long("genesis-fork-version")
.value_name("HEX")
.takes_value(true)
.default_value("0x01030307") // [1, 3, 3, 7]
.help("Used to avoid reply attacks between testnets. Recommended to set to
non-default."),
)
.arg(
Arg::with_name("deposit-contract-address")
.long("deposit-contract-address")
.value_name("ETH1_ADDRESS")
.takes_value(true)
.default_value("0x0000000000000000000000000000000000000000")
.help("The address of the deposit contract."),
)
.arg(
Arg::with_name("deposit-contract-deploy-block")
.long("deposit-contract-deploy-block")
.value_name("ETH1_BLOCK_NUMBER")
.takes_value(true)
.default_value("0")
.help("The block the deposit contract was deployed. Setting this is a huge
optimization for nodes, please do it."),
)
)
.get_matches();
macro_rules! run_with_spec {
@ -365,6 +443,8 @@ fn run<T: EthSpec>(env_builder: EnvironmentBuilder<T>, matches: &ArgMatches) {
.unwrap_or_else(|e| error!("Failed to run interop-genesis command: {}", e)),
("change-genesis-time", Some(matches)) => change_genesis_time::run::<T>(matches)
.unwrap_or_else(|e| error!("Failed to run change-genesis-time command: {}", e)),
("new-testnet", Some(matches)) => new_testnet::run::<T>(matches)
.unwrap_or_else(|e| error!("Failed to run new_testnet command: {}", e)),
(other, _) => error!("Unknown subcommand {}. See --help.", other),
}
}

59
lcli/src/new_testnet.rs Normal file
View File

@ -0,0 +1,59 @@
use crate::helpers::*;
use clap::ArgMatches;
use eth2_testnet_config::Eth2TestnetConfig;
use std::path::PathBuf;
use types::{EthSpec, YamlConfig};
pub fn run<T: EthSpec>(matches: &ArgMatches) -> Result<(), String> {
let testnet_dir_path = parse_path_with_default_in_home_dir(
matches,
"testnet-dir",
PathBuf::from(".lighthouse/testnet"),
)?;
let min_genesis_time = parse_u64_opt(matches, "min-genesis-time")?;
let min_genesis_delay = parse_u64(matches, "min-genesis-delay")?;
let min_genesis_active_validator_count =
parse_u64(matches, "min-genesis-active-validator-count")?;
let min_deposit_amount = parse_u64(matches, "min-deposit-amount")?;
let max_effective_balance = parse_u64(matches, "max-effective-balance")?;
let effective_balance_increment = parse_u64(matches, "effective-balance-increment")?;
let ejection_balance = parse_u64(matches, "ejection-balance")?;
let eth1_follow_distance = parse_u64(matches, "eth1-follow-distance")?;
let deposit_contract_deploy_block = parse_u64(matches, "deposit-contract-deploy-block")?;
let genesis_fork_version = parse_fork_opt(matches, "genesis-fork-version")?;
let deposit_contract_address = parse_address(matches, "deposit-contract-address")?;
if testnet_dir_path.exists() {
return Err(format!(
"{:?} already exists, will not overwrite",
testnet_dir_path
));
}
let mut spec = T::default_spec();
if let Some(time) = min_genesis_time {
spec.min_genesis_time = time;
} else {
spec.min_genesis_time = time_now()?;
}
spec.min_deposit_amount = min_deposit_amount;
spec.min_genesis_active_validator_count = min_genesis_active_validator_count;
spec.max_effective_balance = max_effective_balance;
spec.effective_balance_increment = effective_balance_increment;
spec.ejection_balance = ejection_balance;
spec.eth1_follow_distance = eth1_follow_distance;
spec.min_genesis_delay = min_genesis_delay;
if let Some(v) = genesis_fork_version {
spec.genesis_fork_version = v;
}
let testnet: Eth2TestnetConfig<T> = Eth2TestnetConfig {
deposit_contract_address: format!("{:?}", deposit_contract_address),
deposit_contract_deploy_block,
boot_enr: Some(vec![]),
genesis_state: None,
yaml_config: Some(YamlConfig::from_spec::<T>(&spec)),
};
testnet.write_to_file(testnet_dir_path)
}