* Add progress on new deposits * Add deposited command to account manager * Remove old lcli::helpers mod * Clean clap_utils * Refactor lcli deposit contract commands to use IPC * Make testnet optional for environment * Use dbg formatting for deploy address * Add command to generate bootnode enr * Ensure lcli returns with 1 on error * Ensure account manager returns 1 on error * Disallow deposits to the zero address * Update web3 in eth1 crate * Ensure correct lighthouse dir is created * Reduce deposit gas requirement * Update cargo.lock * Add progress on new deposits * Add deposited command to account manager * Remove old lcli::helpers mod * Clean clap_utils * Refactor lcli deposit contract commands to use IPC * Add command to generate bootnode enr * Ensure lcli returns with 1 on error * Ensure account manager returns 1 on error * Update web3 in eth1 crate * Update Cargo.lock * Move lcli out of main install script * Change --limit to --at-least * Change --datadir to --validator-dir * Remove duplication in docs
459 lines
16 KiB
Rust
459 lines
16 KiB
Rust
mod cli;
|
|
mod deposits;
|
|
|
|
use clap::ArgMatches;
|
|
use deposit_contract::DEPOSIT_GAS;
|
|
use environment::{Environment, RuntimeContext};
|
|
use eth2_testnet_config::Eth2TestnetConfig;
|
|
use futures::{future, Future, IntoFuture, Stream};
|
|
use rayon::prelude::*;
|
|
use slog::{error, info, Logger};
|
|
use std::fs;
|
|
use std::fs::File;
|
|
use std::io::Read;
|
|
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;
|
|
|
|
/// Run the account manager, returning an error if the operation did not succeed.
|
|
pub fn run<T: EthSpec>(matches: &ArgMatches, mut env: Environment<T>) -> Result<(), String> {
|
|
let context = env.core_context();
|
|
let log = context.log.clone();
|
|
|
|
// If the `datadir` was not provided, default to the home directory. If the home directory is
|
|
// not known, use the current directory.
|
|
let datadir = matches
|
|
.value_of("datadir")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(|| {
|
|
dirs::home_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join(".lighthouse")
|
|
.join("validators")
|
|
});
|
|
|
|
fs::create_dir_all(&datadir).map_err(|e| format!("Failed to create datadir: {}", e))?;
|
|
|
|
info!(
|
|
log,
|
|
"Located data directory";
|
|
"path" => format!("{:?}", datadir)
|
|
);
|
|
|
|
match matches.subcommand() {
|
|
("validator", Some(matches)) => match matches.subcommand() {
|
|
("deposited", Some(matches)) => deposits::cli_run(matches, env)?,
|
|
("new", Some(matches)) => run_new_validator_subcommand(matches, datadir, env)?,
|
|
_ => {
|
|
return Err("Invalid 'validator new' command. See --help.".to_string());
|
|
}
|
|
},
|
|
_ => {
|
|
return Err("Invalid 'validator' command. See --help.".to_string());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Describes the crypto key generation methods for a validator.
|
|
enum KeygenMethod {
|
|
/// Produce an insecure "deterministic" keypair. Used only for interop and testing.
|
|
Insecure(usize),
|
|
/// Generate a new key from the `rand` thread random RNG.
|
|
ThreadRandom,
|
|
}
|
|
|
|
/// Process the subcommand for creating new validators.
|
|
fn run_new_validator_subcommand<T: EthSpec>(
|
|
matches: &ArgMatches,
|
|
datadir: PathBuf,
|
|
mut env: Environment<T>,
|
|
) -> Result<(), String> {
|
|
let mut context = env.core_context();
|
|
let log = context.log.clone();
|
|
|
|
// Load the testnet configuration from disk, or use the default testnet.
|
|
let eth2_testnet_config: Eth2TestnetConfig<T> =
|
|
if let Some(testnet_dir_str) = matches.value_of("testnet-dir") {
|
|
let testnet_dir = testnet_dir_str
|
|
.parse::<PathBuf>()
|
|
.map_err(|e| format!("Unable to parse testnet-dir: {}", e))?;
|
|
|
|
if !testnet_dir.exists() {
|
|
return Err(format!(
|
|
"Testnet directory at {:?} does not exist",
|
|
testnet_dir
|
|
));
|
|
}
|
|
|
|
info!(
|
|
log,
|
|
"Loading deposit contract address";
|
|
"testnet_dir" => format!("{:?}", &testnet_dir)
|
|
);
|
|
|
|
Eth2TestnetConfig::load(testnet_dir.clone())
|
|
.map_err(|e| format!("Failed to load testnet dir at {:?}: {}", testnet_dir, e))?
|
|
} else {
|
|
info!(
|
|
log,
|
|
"Using Lighthouse testnet deposit contract";
|
|
);
|
|
|
|
Eth2TestnetConfig::hard_coded()
|
|
.map_err(|e| format!("Failed to load hard_coded testnet dir: {}", e))?
|
|
};
|
|
|
|
context.eth2_config.spec = eth2_testnet_config
|
|
.yaml_config
|
|
.as_ref()
|
|
.ok_or_else(|| "The testnet directory must contain a spec config".to_string())?
|
|
.apply_to_chain_spec::<T>(&context.eth2_config.spec)
|
|
.ok_or_else(|| {
|
|
format!(
|
|
"The loaded config is not compatible with the {} spec",
|
|
&context.eth2_config.spec_constants
|
|
)
|
|
})?;
|
|
|
|
let methods: Vec<KeygenMethod> = match matches.subcommand() {
|
|
("insecure", Some(matches)) => {
|
|
let first = matches
|
|
.value_of("first")
|
|
.ok_or_else(|| "No first index".to_string())?
|
|
.parse::<usize>()
|
|
.map_err(|e| format!("Unable to parse first index: {}", e))?;
|
|
let last = matches
|
|
.value_of("last")
|
|
.ok_or_else(|| "No last index".to_string())?
|
|
.parse::<usize>()
|
|
.map_err(|e| format!("Unable to parse first index: {}", e))?;
|
|
|
|
(first..last).map(KeygenMethod::Insecure).collect()
|
|
}
|
|
("random", Some(matches)) => {
|
|
let count = matches
|
|
.value_of("validator_count")
|
|
.ok_or_else(|| "No validator count".to_string())?
|
|
.parse::<usize>()
|
|
.map_err(|e| format!("Unable to parse validator count: {}", e))?;
|
|
|
|
(0..count).map(|_| KeygenMethod::ThreadRandom).collect()
|
|
}
|
|
_ => {
|
|
return Err("Invalid 'validator' command. See --help.".to_string());
|
|
}
|
|
};
|
|
|
|
let deposit_value = matches
|
|
.value_of("deposit-value")
|
|
.ok_or_else(|| "No deposit-value".to_string())?
|
|
.parse::<u64>()
|
|
.map_err(|e| format!("Unable to parse deposit-value: {}", e))?;
|
|
|
|
let validators = make_validators(
|
|
datadir.clone(),
|
|
&methods,
|
|
deposit_value,
|
|
&context.eth2_config.spec,
|
|
&log,
|
|
)?;
|
|
|
|
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::<usize>()
|
|
.map_err(|e| format!("Unable to parse account-index: {}", e))?;
|
|
|
|
// If supplied, load the eth1 account password from file.
|
|
let password = if let Some(password_path) = matches.value_of("password") {
|
|
Some(
|
|
File::open(password_path)
|
|
.map_err(|e| format!("Unable to open password file: {:?}", e))
|
|
.and_then(|mut file| {
|
|
let mut password = String::new();
|
|
file.read_to_string(&mut password)
|
|
.map_err(|e| format!("Unable to read password file to string: {:?}", e))
|
|
.map(|_| password)
|
|
})
|
|
.map(|password| {
|
|
// Trim the line feed from the end of the password file, if present.
|
|
if password.ends_with('\n') {
|
|
password[0..password.len() - 1].to_string()
|
|
} else {
|
|
password
|
|
}
|
|
})?,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
info!(
|
|
log,
|
|
"Submitting validator deposits";
|
|
"eth1_node_http_endpoint" => eth1_endpoint
|
|
);
|
|
|
|
// Convert from `types::Address` to `web3::types::Address`.
|
|
let deposit_contract = Address::from_slice(
|
|
eth2_testnet_config
|
|
.deposit_contract_address()?
|
|
.as_fixed_bytes(),
|
|
);
|
|
|
|
if let Err(()) = env.runtime().block_on(deposit_validators(
|
|
context.clone(),
|
|
eth1_endpoint.to_string(),
|
|
deposit_contract,
|
|
validators.clone(),
|
|
account_index,
|
|
deposit_value,
|
|
password,
|
|
)) {
|
|
error!(
|
|
log,
|
|
"Created validators but could not submit deposits";
|
|
)
|
|
} else {
|
|
info!(
|
|
log,
|
|
"Validator deposits complete";
|
|
);
|
|
}
|
|
}
|
|
|
|
info!(
|
|
log,
|
|
"Generated validator directories";
|
|
"base_path" => format!("{:?}", datadir),
|
|
"count" => validators.len(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Produces a validator directory for each of the key generation methods provided in `methods`.
|
|
fn make_validators(
|
|
datadir: PathBuf,
|
|
methods: &[KeygenMethod],
|
|
deposit_value: u64,
|
|
spec: &ChainSpec,
|
|
log: &Logger,
|
|
) -> Result<Vec<ValidatorDirectory>, String> {
|
|
methods
|
|
.par_iter()
|
|
.map(|method| {
|
|
let mut builder = ValidatorDirectoryBuilder::default()
|
|
.spec(spec.clone())
|
|
.custom_deposit_amount(deposit_value);
|
|
|
|
builder = match method {
|
|
KeygenMethod::Insecure(index) => builder.insecure_keypairs(*index),
|
|
KeygenMethod::ThreadRandom => builder.thread_random_keypairs(),
|
|
};
|
|
|
|
let validator = builder
|
|
.create_directory(datadir.clone())?
|
|
.write_keypair_files()?
|
|
.write_eth1_data_file()?
|
|
.build()?;
|
|
|
|
let pubkey = &validator
|
|
.voting_keypair
|
|
.as_ref()
|
|
.ok_or_else(|| "Generated validator must have voting keypair".to_string())?
|
|
.pk;
|
|
|
|
info!(
|
|
log,
|
|
"Saved new validator to disk";
|
|
"voting_pubkey" => format!("{:?}", pubkey)
|
|
);
|
|
|
|
Ok(validator)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// For each `ValidatorDirectory`, submit a deposit transaction to the `eth1_endpoint`.
|
|
///
|
|
/// Returns success as soon as the eth1 endpoint accepts the transaction (i.e., does not wait for
|
|
/// transaction success/revert).
|
|
fn deposit_validators<E: EthSpec>(
|
|
context: RuntimeContext<E>,
|
|
eth1_endpoint: String,
|
|
deposit_contract: Address,
|
|
validators: Vec<ValidatorDirectory>,
|
|
account_index: usize,
|
|
deposit_value: u64,
|
|
password: Option<String>,
|
|
) -> impl Future<Item = (), Error = ()> {
|
|
let log_1 = context.log.clone();
|
|
let log_2 = context.log.clone();
|
|
|
|
Http::new(ð1_endpoint)
|
|
.map_err(move |e| {
|
|
error!(
|
|
log_1,
|
|
"Failed to start web3 HTTP transport";
|
|
"error" => format!("{:?}", e)
|
|
)
|
|
})
|
|
.into_future()
|
|
/*
|
|
* Loop through the validator directories and submit the deposits.
|
|
*/
|
|
.and_then(move |(event_loop, transport)| {
|
|
let web3 = Web3::new(transport);
|
|
|
|
futures::stream::iter_ok(validators)
|
|
.for_each(move |validator| {
|
|
let web3 = web3.clone();
|
|
let log = log_2.clone();
|
|
let password = password.clone();
|
|
|
|
deposit_validator(
|
|
web3,
|
|
deposit_contract,
|
|
&validator,
|
|
deposit_value,
|
|
account_index,
|
|
password,
|
|
log,
|
|
)
|
|
})
|
|
.map(|_| event_loop)
|
|
})
|
|
// Web3 gives errors if the event loop is dropped whilst performing requests.
|
|
.map(drop)
|
|
}
|
|
|
|
/// For the given `ValidatorDirectory`, submit a deposit transaction to the `web3` node.
|
|
///
|
|
/// Returns success as soon as the eth1 endpoint accepts the transaction (i.e., does not wait for
|
|
/// transaction success/revert).
|
|
fn deposit_validator(
|
|
web3: Web3<Http>,
|
|
deposit_contract: Address,
|
|
validator: &ValidatorDirectory,
|
|
deposit_amount: u64,
|
|
account_index: usize,
|
|
password_opt: Option<String>,
|
|
log: Logger,
|
|
) -> impl Future<Item = (), Error = ()> {
|
|
validator
|
|
.voting_keypair
|
|
.clone()
|
|
.ok_or_else(|| error!(log, "Validator does not have voting keypair"))
|
|
.and_then(|voting_keypair| {
|
|
validator
|
|
.deposit_data
|
|
.clone()
|
|
.ok_or_else(|| error!(log, "Validator does not have deposit data"))
|
|
.map(|deposit_data| (voting_keypair, deposit_data))
|
|
})
|
|
.into_future()
|
|
.and_then(move |(voting_keypair, deposit_data)| {
|
|
let pubkey_1 = voting_keypair.pk.clone();
|
|
let pubkey_2 = voting_keypair.pk;
|
|
|
|
let web3_1 = web3.clone();
|
|
let web3_2 = web3.clone();
|
|
|
|
let log_1 = log.clone();
|
|
let log_2 = log.clone();
|
|
|
|
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())
|
|
})
|
|
/*
|
|
* If a password was supplied, unlock the account.
|
|
*/
|
|
.and_then(move |from_address| {
|
|
let future: Box<dyn Future<Item = Address, Error = String> + Send> =
|
|
if let Some(password) = password_opt {
|
|
// Unlock for only a single transaction.
|
|
let duration = None;
|
|
|
|
let future = web3_1
|
|
.personal()
|
|
.unlock_account(from_address, &password, duration)
|
|
.then(move |result| match result {
|
|
Ok(true) => Ok(from_address),
|
|
Ok(false) => {
|
|
Err("Eth1 node refused to unlock account. Check password."
|
|
.to_string())
|
|
}
|
|
Err(e) => Err(format!("Eth1 unlock request failed: {:?}", e)),
|
|
});
|
|
|
|
Box::new(future)
|
|
} else {
|
|
Box::new(future::ok(from_address))
|
|
};
|
|
|
|
future
|
|
})
|
|
/*
|
|
* Submit the deposit transaction.
|
|
*/
|
|
.and_then(move |from| {
|
|
let tx_request = TransactionRequest {
|
|
from,
|
|
to: Some(deposit_contract),
|
|
gas: Some(U256::from(DEPOSIT_GAS)),
|
|
gas_price: None,
|
|
value: Some(from_gwei(deposit_amount)),
|
|
data: Some(deposit_data.into()),
|
|
nonce: None,
|
|
condition: None,
|
|
};
|
|
|
|
web3_2
|
|
.eth()
|
|
.send_transaction(tx_request)
|
|
.map_err(|e| format!("Failed to call deposit fn: {:?}", e))
|
|
})
|
|
.map(move |tx| {
|
|
info!(
|
|
log_1,
|
|
"Validator deposit successful";
|
|
"eth1_tx_hash" => format!("{:?}", tx),
|
|
"validator_voting_pubkey" => format!("{:?}", pubkey_1)
|
|
)
|
|
})
|
|
.map_err(move |e| {
|
|
error!(
|
|
log_2,
|
|
"Validator deposit_failed";
|
|
"error" => e,
|
|
"validator_voting_pubkey" => format!("{:?}", pubkey_2)
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
/// Converts gwei to wei.
|
|
fn from_gwei(gwei: u64) -> U256 {
|
|
U256::from(gwei) * U256::exp10(9)
|
|
}
|