diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 9a4fe112c6..3fcdb2597d 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -24,3 +24,5 @@ hex = "0.4" validator_client = { path = "../validator_client" } rayon = "1.2.0" eth2_testnet = { path = "../eth2/utils/eth2_testnet" } +web3 = "0.8.0" +futures = "0.1.25" diff --git a/account_manager/src/cli.rs b/account_manager/src/cli.rs index ef69dd1227..58a9d41eda 100644 --- a/account_manager/src/cli.rs +++ b/account_manager/src/cli.rs @@ -10,6 +10,47 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand( SubCommand::with_name("new") .about("Create a new Ethereum 2.0 validator.") + .arg( + Arg::with_name("send-deposits") + .long("send-deposits") + .help("If present, submit validator deposits to an eth1 endpoint") + ) + .arg( + Arg::with_name("eth1-endpoint") + .short("e") + .value_name("HTTP_SERVER") + .takes_value(true) + .requires("send-deposits") + .default_value("http://localhost:8545") + .help("The URL to the eth1 JSON-RPC http API."), + ) + .arg( + Arg::with_name("deposit-contract") + .short("c") + .value_name("ADDRESS") + .takes_value(true) + .requires("send-deposits") + .conflicts_with("testnet-dir") + .help("The deposit contract for submitting deposits."), + ) + .arg( + Arg::with_name("account-index") + .short("i") + .value_name("INDEX") + .takes_value(true) + .requires("send-deposits") + .default_value("0") + .help("The eth1 accounts[] index which will send the transaction"), + ) + .arg( + Arg::with_name("testnet-dir") + .long("testnet-dir") + .value_name("DIRECTORY") + .takes_value(true) + .requires("send-deposits") + .default_value("0") + .help("The directory from which to read the deposit contract address."), + ) .subcommand( SubCommand::with_name("insecure") .about("Produce insecure, ephemeral validators. DO NOT USE TO STORE VALUE.") diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 37d2d7692a..30687e8956 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -1,13 +1,21 @@ mod cli; use clap::ArgMatches; +use deposit_contract::DEPOSIT_GAS; use environment::RuntimeContext; +use eth2_testnet::Eth2TestnetDir; +use futures::{stream::unfold, Future, IntoFuture, Stream}; use rayon::prelude::*; -use slog::{crit, info}; +use slog::{crit, error, info}; use std::fs; use std::path::PathBuf; use types::{ChainSpec, EthSpec}; use validator_client::validator_directory::{ValidatorDirectory, ValidatorDirectoryBuilder}; +use web3::{ + transports::Http, + types::{Address, TransactionRequest, U256}, + Web3, +}; pub use cli::cli_app; @@ -110,7 +118,48 @@ fn run_new_validator_subcommand( } }; - let validators = make_validators(datadir.clone(), &methods, context.eth2_config.spec)?; + let validators = make_validators(datadir.clone(), &methods, &context.eth2_config.spec)?; + + if matches.is_present("send-deposits") { + let eth1_endpoint = matches + .value_of("eth1-endpoint") + .ok_or_else(|| "No eth1-endpoint".to_string())?; + let account_index = matches + .value_of("account-index") + .ok_or_else(|| "No account-index".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse account-index: {}", e))?; + + let deposit_contract = if let Some(testnet_dir_str) = matches.value_of("testnet-dir") { + let testnet_dir = testnet_dir_str + .parse::() + .map_err(|e| format!("Unable to parse testnet-dir: {}", e))?; + + let eth2_testnet_dir = Eth2TestnetDir::load(testnet_dir) + .map_err(|e| format!("Failed to load testnet dir: {}", e))?; + + // Convert from `types::Address` to `web3::types::Address`. + Address::from_slice( + eth2_testnet_dir + .deposit_contract_address()? + .as_fixed_bytes(), + ) + } else { + matches + .value_of("deposit-contract") + .ok_or_else(|| "No deposit-contract".to_string())? + .parse::
() + .map_err(|e| format!("Unable to parse deposit-contract: {}", e))? + }; + + context.executor.spawn(deposit_validators( + context.clone(), + eth1_endpoint.to_string(), + deposit_contract, + validators.clone(), + account_index, + )); + } info!( log, @@ -122,11 +171,90 @@ fn run_new_validator_subcommand( Ok(()) } +fn deposit_validators( + context: RuntimeContext, + eth1_endpoint: String, + deposit_contract: Address, + validators: Vec, + account_index: usize, +) -> impl Future { + let deposit_amount = context.eth2_config.spec.max_effective_balance; + let log = context.log.clone(); + + Http::new(ð1_endpoint) + .map_err(|e| format!("Failed to start web3 HTTP transport: {:?}", e)) + .into_future() + .and_then(move |(_event_loop, transport)| { + let web3 = Web3::new(transport); + + unfold(validators.into_iter(), move |mut validators| { + let web3 = web3.clone(); + + validators.next().map(move |validator| { + deposit_validator( + web3, + deposit_contract, + &validator, + deposit_amount, + account_index, + ) + .map(|()| ((), validators)) + }) + }) + .collect() + }) + .map_err(move |e| error!(log, "Error whilst depositing validator"; "error" => e)) + .map(|_| ()) +} + +fn deposit_validator( + web3: Web3, + deposit_contract: Address, + validator: &ValidatorDirectory, + deposit_amount: u64, + account_index: usize, +) -> impl Future { + let web3_1 = web3.clone(); + + let deposit_data = validator + .deposit_data + .clone() + .expect("Validators must have a deposit data"); + + web3.eth() + .accounts() + .map_err(|e| format!("Failed to get accounts: {:?}", e)) + .and_then(move |accounts| { + accounts + .get(account_index) + .cloned() + .ok_or_else(|| "Insufficient accounts for deposit".to_string()) + }) + .and_then(move |from| { + let tx_request = TransactionRequest { + from, + to: Some(deposit_contract), + gas: Some(U256::from(DEPOSIT_GAS)), + gas_price: None, + value: Some(U256::from(deposit_amount)), + data: Some(deposit_data.into()), + nonce: None, + condition: None, + }; + + web3_1 + .eth() + .send_transaction(tx_request) + .map_err(|e| format!("Failed to call deposit fn: {:?}", e)) + }) + .map(|_| ()) +} + /// Produces a validator directory for each of the key generation methods provided in `methods`. fn make_validators( datadir: PathBuf, methods: &[KeygenMethod], - spec: ChainSpec, + spec: &ChainSpec, ) -> Result, String> { methods .par_iter() diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index f617d9310e..df56084765 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -275,7 +275,8 @@ fn process_testnet_subcommand( epoch: Epoch::new(0), }; - client_config.eth1.deposit_contract_address = eth2_testnet_dir.deposit_contract_address; + 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; client_config.eth1.follow_distance = 16; diff --git a/eth2/utils/eth2_testnet/Cargo.toml b/eth2/utils/eth2_testnet/Cargo.toml index dc97e1327c..5fcdd3d0c3 100644 --- a/eth2/utils/eth2_testnet/Cargo.toml +++ b/eth2/utils/eth2_testnet/Cargo.toml @@ -12,3 +12,4 @@ tempdir = "0.3" [dependencies] serde = "1.0" serde_json = "^1.0" +types = { path = "../../types"} diff --git a/eth2/utils/eth2_testnet/src/lib.rs b/eth2/utils/eth2_testnet/src/lib.rs index 9883d45ade..43f3f832a8 100644 --- a/eth2/utils/eth2_testnet/src/lib.rs +++ b/eth2/utils/eth2_testnet/src/lib.rs @@ -9,6 +9,7 @@ use std::fs::{create_dir_all, File}; use std::path::PathBuf; +use types::Address; pub const ADDRESS_FILE: &str = "deposit_contract.txt"; pub const DEPLOY_BLOCK_FILE: &str = "deploy_block.txt"; @@ -16,7 +17,7 @@ pub const MIN_GENESIS_TIME_FILE: &str = "min_genesis_time.txt"; #[derive(Clone, PartialEq, Debug)] pub struct Eth2TestnetDir { - pub deposit_contract_address: String, + deposit_contract_address: String, pub deposit_contract_deploy_block: u64, pub min_genesis_time: u64, } @@ -91,6 +92,16 @@ impl Eth2TestnetDir { min_genesis_time, }) } + + pub fn deposit_contract_address(&self) -> Result { + if self.deposit_contract_address.starts_with("0x") { + self.deposit_contract_address[2..] + .parse() + .map_err(|e| format!("Corrupted address, unable to parse: {:?}", e)) + } else { + Err("Corrupted address, must start with 0x".to_string()) + } + } } #[cfg(test)] diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index 728948ad78..df56b67fa5 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -136,10 +136,6 @@ fn run( let runtime_context = environment.core_context(); account_manager::run(sub_matches, runtime_context); - - // Exit early if the account manager was run. It does not use the tokio executor, no need - // to wait for it to shutdown. - return Ok(()); } let beacon_node = if let Some(sub_matches) = matches.subcommand_matches("beacon_node") {