From ca0314ee555c03d3019cc68a8e4165829cba57bf Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Wed, 4 Mar 2020 14:28:02 +1100 Subject: [PATCH] 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 --- Dockerfile | 4 +- lcli/src/deploy_deposit_contract.rs | 75 +-------------- lcli/src/helpers.rs | 85 +++++++++++++++++ lcli/src/main.rs | 136 ++++++++++++++++++++++------ lcli/src/new_testnet.rs | 59 ++++++++++++ 5 files changed, 257 insertions(+), 102 deletions(-) create mode 100644 lcli/src/helpers.rs create mode 100644 lcli/src/new_testnet.rs diff --git a/Dockerfile b/Dockerfile index 0eadb2dc5..0a3145eb8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/lcli/src/deploy_deposit_contract.rs b/lcli/src/deploy_deposit_contract.rs index 1dada641f..095da6b66 100644 --- a/lcli/src/deploy_deposit_contract.rs +++ b/lcli/src/deploy_deposit_contract.rs @@ -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(mut env: Environment, matches: &ArgMatches) -> Result<(), String> { - let min_genesis_time = matches - .value_of("min-genesis-time") - .ok_or_else(|| "min_genesis_time not specified")? - .parse::() - .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::() - .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::() .map_err(|e| format!("Failed to parse confirmations: {}", e))?; - let output_dir = matches - .value_of("output") - .ok_or_else(|| ()) - .and_then(|output| output.parse::().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(mut env: Environment, 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(mut env: Environment, 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 = 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::(&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, String> { if let Some(password_path) = matches.value_of("password") { Ok(Some( diff --git a/lcli/src/helpers.rs b/lcli/src/helpers.rs new file mode 100644 index 000000000..7916c26e2 --- /dev/null +++ b/lcli/src/helpers.rs @@ -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 { + 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 { + matches + .value_of(name) + .map(|dir| { + dir.parse::() + .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 { + matches + .value_of(name) + .ok_or_else(|| format!("{} not specified", name))? + .parse::() + .map_err(|e| format!("Unable to parse {}: {}", name, e)) +} + +pub fn parse_u64_opt(matches: &ArgMatches, name: &'static str) -> Result, String> { + matches + .value_of(name) + .map(|val| { + val.parse::() + .map_err(|e| format!("Unable to parse {}: {}", name, e)) + }) + .transpose() +} + +pub fn parse_address(matches: &ArgMatches, name: &'static str) -> Result { + 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, 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() +} diff --git a/lcli/src/main.rs b/lcli/src/main.rs index 1119a5426..432f73215 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -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(env_builder: EnvironmentBuilder, matches: &ArgMatches) { .unwrap_or_else(|e| error!("Failed to run interop-genesis command: {}", e)), ("change-genesis-time", Some(matches)) => change_genesis_time::run::(matches) .unwrap_or_else(|e| error!("Failed to run change-genesis-time command: {}", e)), + ("new-testnet", Some(matches)) => new_testnet::run::(matches) + .unwrap_or_else(|e| error!("Failed to run new_testnet command: {}", e)), (other, _) => error!("Unknown subcommand {}. See --help.", other), } } diff --git a/lcli/src/new_testnet.rs b/lcli/src/new_testnet.rs new file mode 100644 index 000000000..447bffdfc --- /dev/null +++ b/lcli/src/new_testnet.rs @@ -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(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 = 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::(&spec)), + }; + + testnet.write_to_file(testnet_dir_path) +}