Merge branch 'deposit-flow' into kill-grpc

This commit is contained in:
Paul Hauner
2019-11-21 14:24:02 +11:00
21 changed files with 771 additions and 307 deletions

View File

@@ -7,6 +7,7 @@ members = [
"eth2/utils/bls",
"eth2/utils/compare_fields",
"eth2/utils/compare_fields_derive",
"eth2/utils/deposit_contract",
"eth2/utils/eth2_config",
"eth2/utils/eth2_interop_keypairs",
"eth2/utils/logging",

View File

@@ -4,6 +4,9 @@ version = "0.0.1"
authors = ["Luke Anderson <luke@sigmaprime.io>"]
edition = "2018"
[dev-dependencies]
tempdir = "0.3"
[dependencies]
bls = { path = "../eth2/utils/bls" }
clap = "2.33.0"
@@ -13,3 +16,9 @@ slog-async = "2.3.0"
validator_client = { path = "../validator_client" }
types = { path = "../eth2/types" }
dirs = "2.0.2"
environment = { path = "../lighthouse/environment" }
deposit_contract = { path = "../eth2/utils/deposit_contract" }
libc = "0.2.65"
eth2_ssz = { path = "../eth2/utils/ssz" }
eth2_ssz_derive = { path = "../eth2/utils/ssz_derive" }
hex = "0.4"

View File

@@ -0,0 +1,64 @@
use clap::{App, Arg, SubCommand};
pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
App::new("account_manager")
.visible_aliases(&["am", "account", "account_manager"])
.about("Eth 2.0 Accounts Manager")
.arg(
Arg::with_name("logfile")
.long("logfile")
.value_name("logfile")
.help("File path where output will be written.")
.takes_value(true),
)
.arg(
Arg::with_name("datadir")
.long("datadir")
.short("d")
.value_name("DIR")
.help("Data directory for keys and databases.")
.takes_value(true),
)
.subcommand(
SubCommand::with_name("validator")
.about("Eth2 validator managment commands.")
.version("0.0.1")
.author("Sigma Prime <contact@sigmaprime.io>")
.subcommand(
SubCommand::with_name("new")
.about("Create a new validator.")
.subcommand(
SubCommand::with_name("insecure")
.about("Uses the insecure deterministic keypairs. Do not store value in these.")
.arg(
Arg::with_name("first")
.index(1)
.value_name("INDEX")
.help("Index of the first validator")
.takes_value(true)
.required(true),
)
.arg(
Arg::with_name("last")
.index(2)
.value_name("INDEX")
.help("Index of the first validator")
.takes_value(true)
.required(true),
),
)
.subcommand(
SubCommand::with_name("random")
.about("Uses the Rust rand crate ThreadRandom to generate keys.")
.arg(
Arg::with_name("validator_count")
.index(1)
.value_name("INTEGER")
.help("The number of new validators to generate.")
.takes_value(true)
.default_value("1"),
),
)
)
)
}

150
account_manager/src/lib.rs Normal file
View File

@@ -0,0 +1,150 @@
mod cli;
pub mod validator;
use clap::ArgMatches;
use environment::RuntimeContext;
use slog::{crit, info};
use std::fs;
use std::path::PathBuf;
use types::{ChainSpec, EthSpec};
use validator::{ValidatorDirectory, ValidatorDirectoryBuilder};
pub use cli::cli_app;
/// Run the account manager, logging an error if the operation did not succeed.
pub fn run<T: EthSpec>(matches: &ArgMatches, context: RuntimeContext<T>) {
let log = context.log.clone();
match run_account_manager(matches, context) {
Ok(()) => (),
Err(e) => crit!(log, "Account manager failed"; "error" => e),
}
}
/// Run the account manager, returning an error if the operation did not succeed.
fn run_account_manager<T: EthSpec>(
matches: &ArgMatches,
context: RuntimeContext<T>,
) -> Result<(), String> {
let log = context.log.clone();
let datadir = matches
.value_of("datadir")
.map(PathBuf::from)
.unwrap_or_else(|| {
let mut default_dir = match dirs::home_dir() {
Some(v) => v,
None => {
panic!("Failed to find a home directory");
}
};
default_dir.push(".lighthouse");
default_dir.push("validator");
default_dir
});
fs::create_dir_all(&datadir).map_err(|e| format!("Failed to initialize datadir: {}", e))?;
info!(
log,
"Located data directory";
"path" => format!("{:?}", datadir)
);
match matches.subcommand() {
("validator", Some(matches)) => match matches.subcommand() {
("new", Some(matches)) => new_validator_subcommand(matches, datadir, context)?,
_ => {
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 new_validator_subcommand<T: EthSpec>(
matches: &ArgMatches,
datadir: PathBuf,
context: RuntimeContext<T>,
) -> Result<(), String> {
let log = context.log.clone();
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 validators = make_validators(datadir.clone(), &methods, context.eth2_config.spec)?;
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],
spec: ChainSpec,
) -> Result<Vec<ValidatorDirectory>, String> {
methods
.iter()
.map(|method| {
let mut builder = ValidatorDirectoryBuilder::default()
.spec(spec.clone())
.full_deposit_amount()?;
builder = match method {
KeygenMethod::Insecure(index) => builder.insecure_keypairs(*index),
KeygenMethod::ThreadRandom => builder.thread_random_keypairs(),
};
builder
.create_directory(datadir.clone())?
.write_keypair_files()?
.write_eth1_data_file()?
.build()
})
.collect()
}

View File

@@ -1,173 +0,0 @@
use bls::Keypair;
use clap::{App, Arg, SubCommand};
use slog::{crit, debug, info, o, Drain};
use std::fs;
use std::path::PathBuf;
use types::test_utils::generate_deterministic_keypair;
use validator_client::Config as ValidatorClientConfig;
pub const DEFAULT_DATA_DIR: &str = ".lighthouse-validator";
pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml";
fn main() {
// Logging
let decorator = slog_term::TermDecorator::new().build();
let drain = slog_term::CompactFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build().fuse();
let mut log = slog::Logger::root(drain, o!());
// CLI
let matches = App::new("Lighthouse Accounts Manager")
.version("0.0.1")
.author("Sigma Prime <contact@sigmaprime.io>")
.about("Eth 2.0 Accounts Manager")
.arg(
Arg::with_name("logfile")
.long("logfile")
.value_name("logfile")
.help("File path where output will be written.")
.takes_value(true),
)
.arg(
Arg::with_name("datadir")
.long("datadir")
.short("d")
.value_name("DIR")
.help("Data directory for keys and databases.")
.takes_value(true),
)
.subcommand(
SubCommand::with_name("generate")
.about("Generates a new validator private key")
.version("0.0.1")
.author("Sigma Prime <contact@sigmaprime.io>"),
)
.subcommand(
SubCommand::with_name("generate_deterministic")
.about("Generates a deterministic validator private key FOR TESTING")
.version("0.0.1")
.author("Sigma Prime <contact@sigmaprime.io>")
.arg(
Arg::with_name("validator index")
.long("index")
.short("i")
.value_name("index")
.help("The index of the validator, for which the test key is generated")
.takes_value(true)
.required(true),
)
.arg(
Arg::with_name("validator count")
.long("validator_count")
.short("n")
.value_name("validator_count")
.help("If supplied along with `index`, generates keys `i..i + n`.")
.takes_value(true)
.default_value("1"),
),
)
.get_matches();
let data_dir = match matches
.value_of("datadir")
.and_then(|v| Some(PathBuf::from(v)))
{
Some(v) => v,
None => {
// use the default
let mut default_dir = match dirs::home_dir() {
Some(v) => v,
None => {
crit!(log, "Failed to find a home directory");
return;
}
};
default_dir.push(DEFAULT_DATA_DIR);
default_dir
}
};
// create the directory if needed
match fs::create_dir_all(&data_dir) {
Ok(_) => {}
Err(e) => {
crit!(log, "Failed to initialize data dir"; "error" => format!("{}", e));
return;
}
}
let mut client_config = ValidatorClientConfig::default();
// Ensure the `data_dir` in the config matches that supplied to the CLI.
client_config.data_dir = data_dir.clone();
if let Err(e) = client_config.apply_cli_args(&matches, &mut log) {
crit!(log, "Failed to parse ClientConfig CLI arguments"; "error" => format!("{:?}", e));
return;
};
// Log configuration
info!(log, "";
"data_dir" => &client_config.data_dir.to_str());
match matches.subcommand() {
("generate", Some(_)) => generate_random(&client_config, &log),
("generate_deterministic", Some(m)) => {
if let Some(string) = m.value_of("validator index") {
let i: usize = string.parse().expect("Invalid validator index");
if let Some(string) = m.value_of("validator count") {
let n: usize = string.parse().expect("Invalid end validator count");
let indices: Vec<usize> = (i..i + n).collect();
generate_deterministic_multiple(&indices, &client_config, &log)
} else {
generate_deterministic(i, &client_config, &log)
}
}
}
_ => {
crit!(
log,
"The account manager must be run with a subcommand. See help for more information."
);
}
}
}
fn generate_random(config: &ValidatorClientConfig, log: &slog::Logger) {
save_key(&Keypair::random(), config, log)
}
fn generate_deterministic_multiple(
validator_indices: &[usize],
config: &ValidatorClientConfig,
log: &slog::Logger,
) {
for validator_index in validator_indices {
generate_deterministic(*validator_index, config, log)
}
}
fn generate_deterministic(
validator_index: usize,
config: &ValidatorClientConfig,
log: &slog::Logger,
) {
save_key(
&generate_deterministic_keypair(validator_index),
config,
log,
)
}
fn save_key(keypair: &Keypair, config: &ValidatorClientConfig, log: &slog::Logger) {
let key_path: PathBuf = config
.save_key(&keypair)
.expect("Unable to save newly generated private key.");
debug!(
log,
"Keypair generated {:?}, saved to: {:?}",
keypair.identifier(),
key_path.to_string_lossy()
);
}

View File

@@ -0,0 +1,399 @@
use bls::get_withdrawal_credentials;
use deposit_contract::eth1_tx_data;
use hex;
use ssz::{Decode, Encode};
use ssz_derive::{Decode, Encode};
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use types::{
test_utils::generate_deterministic_keypair, ChainSpec, DepositData, Hash256, Keypair,
PublicKey, SecretKey, Signature,
};
const VOTING_KEY_PREFIX: &str = "voting";
const WITHDRAWAL_KEY_PREFIX: &str = "withdrawal";
const ETH1_DEPOSIT_DATA_FILE: &str = "eth1_deposit_data.rlp";
/// Returns the filename of a keypair file.
fn keypair_file(prefix: &str) -> String {
format!("{}_keypair", prefix)
}
/// Represents the files/objects for each dedicated lighthouse validator directory.
///
/// Generally lives in `~/.lighthouse/validators/`.
#[derive(Debug, Clone, PartialEq)]
pub struct ValidatorDirectory {
pub directory: PathBuf,
pub voting_keypair: Option<Keypair>,
pub withdrawal_keypair: Option<Keypair>,
pub deposit_data: Option<Vec<u8>>,
}
impl ValidatorDirectory {
/// Attempts to load a validator from the given directory, requiring only components necessary
/// for signing messages.
pub fn load_for_signing(directory: PathBuf) -> Result<Self, String> {
if !directory.exists() {
return Err(format!(
"Validator directory does not exist: {:?}",
directory
));
}
Ok(Self {
voting_keypair: Some(
load_keypair(directory.clone(), VOTING_KEY_PREFIX)
.map_err(|e| format!("Unable to get voting keypair: {}", e))?,
),
withdrawal_keypair: load_keypair(directory.clone(), WITHDRAWAL_KEY_PREFIX).ok(),
deposit_data: load_eth1_deposit_data(directory.clone()).ok(),
directory,
})
}
}
/// Load a `Keypair` from a file.
fn load_keypair(base_path: PathBuf, file_prefix: &str) -> Result<Keypair, String> {
let path = base_path.join(keypair_file(file_prefix));
if !path.exists() {
return Err(format!("Keypair file does not exist: {:?}", path));
}
let mut bytes = vec![];
File::open(&path)
.map_err(|e| format!("Unable to open keypair file: {}", e))?
.read_to_end(&mut bytes)
.map_err(|e| format!("Unable to read keypair file: {}", e))?;
SszEncodableKeypair::from_ssz_bytes(&bytes)
.map(Into::into)
.map_err(|e| format!("Unable to decode keypair: {:?}", e))
}
/// Load eth1_deposit_data from file.
fn load_eth1_deposit_data(base_path: PathBuf) -> Result<Vec<u8>, String> {
let path = base_path.join(ETH1_DEPOSIT_DATA_FILE);
if !path.exists() {
return Err(format!("Eth1 deposit data file does not exist: {:?}", path));
}
let mut bytes = vec![];
File::open(&path)
.map_err(|e| format!("Unable to open eth1 deposit data file: {}", e))?
.read_to_end(&mut bytes)
.map_err(|e| format!("Unable to read eth1 deposit data file: {}", e))?;
let string = String::from_utf8_lossy(&bytes);
if string.starts_with("0x") {
hex::decode(&string[2..])
.map_err(|e| format!("Unable to decode eth1 data file as hex: {}", e))
} else {
Err(format!("String did not start with 0x: {}", string))
}
}
/// A helper struct to allow SSZ enc/dec for a `Keypair`.
#[derive(Encode, Decode)]
struct SszEncodableKeypair {
pk: PublicKey,
sk: SecretKey,
}
impl Into<Keypair> for SszEncodableKeypair {
fn into(self) -> Keypair {
Keypair {
sk: self.sk,
pk: self.pk,
}
}
}
impl From<Keypair> for SszEncodableKeypair {
fn from(kp: Keypair) -> Self {
Self {
sk: kp.sk,
pk: kp.pk,
}
}
}
/// Builds a `ValidatorDirectory`, both in-memory and on-disk.
#[derive(Default)]
pub struct ValidatorDirectoryBuilder {
directory: Option<PathBuf>,
voting_keypair: Option<Keypair>,
withdrawal_keypair: Option<Keypair>,
amount: Option<u64>,
deposit_data: Option<Vec<u8>>,
spec: Option<ChainSpec>,
}
impl ValidatorDirectoryBuilder {
pub fn spec(mut self, spec: ChainSpec) -> Self {
self.spec = Some(spec);
self
}
pub fn full_deposit_amount(mut self) -> Result<Self, String> {
let spec = self
.spec
.as_ref()
.ok_or_else(|| "full_deposit_amount requires a spec")?;
self.amount = Some(spec.max_effective_balance);
Ok(self)
}
pub fn custom_deposit_amount(mut self, gwei: u64) -> Self {
self.amount = Some(gwei);
self
}
pub fn thread_random_keypairs(mut self) -> Self {
self.voting_keypair = Some(Keypair::random());
self.withdrawal_keypair = Some(Keypair::random());
self
}
pub fn insecure_keypairs(mut self, index: usize) -> Self {
let keypair = generate_deterministic_keypair(index);
self.voting_keypair = Some(keypair.clone());
self.withdrawal_keypair = Some(keypair);
self
}
/// Creates a validator directory in the given `base_path` (e.g., `~/.lighthouse/validators/`).
pub fn create_directory(mut self, base_path: PathBuf) -> Result<Self, String> {
let voting_keypair = self
.voting_keypair
.as_ref()
.ok_or_else(|| "directory requires a voting_keypair")?;
let directory = base_path.join(voting_keypair.identifier());
if directory.exists() {
return Err(format!(
"Validator directory already exists: {:?}",
directory
));
}
fs::create_dir_all(&directory)
.map_err(|e| format!("Unable to create validator directory: {}", e))?;
self.directory = Some(directory);
Ok(self)
}
pub fn write_keypair_files(self) -> Result<Self, String> {
let voting_keypair = self
.voting_keypair
.clone()
.ok_or_else(|| "build requires a voting_keypair")?;
let withdrawal_keypair = self
.withdrawal_keypair
.clone()
.ok_or_else(|| "build requires a withdrawal_keypair")?;
self.save_keypair(voting_keypair, VOTING_KEY_PREFIX)?;
self.save_keypair(withdrawal_keypair, WITHDRAWAL_KEY_PREFIX)?;
Ok(self)
}
fn save_keypair(&self, keypair: Keypair, file_prefix: &str) -> Result<(), String> {
let path = self
.directory
.as_ref()
.map(|directory| directory.join(keypair_file(file_prefix)))
.ok_or_else(|| "save_keypair requires a directory")?;
if path.exists() {
return Err(format!("Keypair file already exists at: {:?}", path));
}
let mut file = File::create(&path).map_err(|e| format!("Unable to create file: {}", e))?;
// Ensure file has correct permissions.
let mut perm = file
.metadata()
.map_err(|e| format!("Unable to get file metadata: {}", e))?
.permissions();
perm.set_mode((libc::S_IWUSR | libc::S_IRUSR) as u32);
file.set_permissions(perm)
.map_err(|e| format!("Unable to set file permissions: {}", e))?;
file.write_all(&SszEncodableKeypair::from(keypair).as_ssz_bytes())
.map_err(|e| format!("Unable to write keypair to file: {}", e))?;
Ok(())
}
pub fn write_eth1_data_file(mut self) -> Result<Self, String> {
let voting_keypair = self
.voting_keypair
.as_ref()
.ok_or_else(|| "write_eth1_data_file requires a voting_keypair")?;
let withdrawal_keypair = self
.withdrawal_keypair
.as_ref()
.ok_or_else(|| "write_eth1_data_file requires a withdrawal_keypair")?;
let amount = self
.amount
.ok_or_else(|| "write_eth1_data_file requires an amount")?;
let spec = self.spec.as_ref().ok_or_else(|| "build requires a spec")?;
let path = self
.directory
.as_ref()
.map(|directory| directory.join(ETH1_DEPOSIT_DATA_FILE))
.ok_or_else(|| "write_eth1_data_filer requires a directory")?;
let deposit_data = {
let withdrawal_credentials = Hash256::from_slice(&get_withdrawal_credentials(
&withdrawal_keypair.pk,
spec.bls_withdrawal_prefix_byte,
));
let mut deposit_data = DepositData {
pubkey: voting_keypair.pk.clone().into(),
withdrawal_credentials,
amount,
signature: Signature::empty_signature().into(),
};
deposit_data.signature = deposit_data.create_signature(&voting_keypair.sk, &spec);
eth1_tx_data(&deposit_data)
.map_err(|e| format!("Unable to encode eth1 deposit tx data: {:?}", e))?
};
if path.exists() {
return Err(format!("Eth1 data file already exists at: {:?}", path));
}
File::create(&path)
.map_err(|e| format!("Unable to create file: {}", e))?
.write_all(&format!("0x{}", hex::encode(&deposit_data)).as_bytes())
.map_err(|e| format!("Unable to write eth1 data file: {}", e))?;
self.deposit_data = Some(deposit_data);
Ok(self)
}
pub fn build(self) -> Result<ValidatorDirectory, String> {
Ok(ValidatorDirectory {
directory: self.directory.ok_or_else(|| "build requires a directory")?,
voting_keypair: self.voting_keypair,
withdrawal_keypair: self.withdrawal_keypair,
deposit_data: self.deposit_data,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use types::{EthSpec, MinimalEthSpec};
type E = MinimalEthSpec;
#[test]
fn random_keypairs_round_trip() {
let spec = E::default_spec();
let temp_dir = TempDir::new("acc_manager").expect("should create test dir");
let created_dir = ValidatorDirectoryBuilder::default()
.spec(spec)
.full_deposit_amount()
.expect("should set full deposit amount")
.thread_random_keypairs()
.create_directory(temp_dir.path().into())
.expect("should create directory")
.write_keypair_files()
.expect("should write keypair files")
.write_eth1_data_file()
.expect("should write eth1 data file")
.build()
.expect("should build dir");
let loaded_dir = ValidatorDirectory::load_for_signing(created_dir.directory.clone())
.expect("should load directory");
assert_eq!(
created_dir, loaded_dir,
"the directory created should match the one loaded"
);
}
#[test]
fn deterministic_keypairs_round_trip() {
let spec = E::default_spec();
let temp_dir = TempDir::new("acc_manager").expect("should create test dir");
let index = 42;
let created_dir = ValidatorDirectoryBuilder::default()
.spec(spec)
.full_deposit_amount()
.expect("should set full deposit amount")
.insecure_keypairs(index)
.create_directory(temp_dir.path().into())
.expect("should create directory")
.write_keypair_files()
.expect("should write keypair files")
.write_eth1_data_file()
.expect("should write eth1 data file")
.build()
.expect("should build dir");
assert!(
created_dir.directory.exists(),
"should have created directory"
);
let mut parent = created_dir.directory.clone();
parent.pop();
assert_eq!(
parent,
PathBuf::from(temp_dir.path()),
"should have created directory ontop of base dir"
);
let expected_keypair = generate_deterministic_keypair(index);
assert_eq!(
created_dir.voting_keypair,
Some(expected_keypair.clone()),
"voting keypair should be as expected"
);
assert_eq!(
created_dir.withdrawal_keypair,
Some(expected_keypair),
"withdrawal keypair should be as expected"
);
assert!(
created_dir
.deposit_data
.clone()
.expect("should have data")
.len()
> 0,
"should have some deposit data"
);
let loaded_dir = ValidatorDirectory::load_for_signing(created_dir.directory.clone())
.expect("should load directory");
assert_eq!(
created_dir, loaded_dir,
"the directory created should match the one loaded"
);
}
}

View File

@@ -26,7 +26,6 @@ error-chain = "0.12.1"
serde_yaml = "0.8.11"
slog = { version = "2.5.2", features = ["max_level_trace"] }
slog-async = "2.3.0"
slog-json = "2.3.0"
tokio = "0.1.22"
clap = "2.33.0"
dirs = "2.0.2"

View File

@@ -1,10 +1,8 @@
use clap::ArgMatches;
use network::NetworkConfig;
use serde_derive::{Deserialize, Serialize};
use slog::{info, o, Drain};
use std::fs::{self, OpenOptions};
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
/// The number initial validators when starting the `Minimal`.
const TESTNET_SPEC_CONSTANTS: &str = "minimal";
@@ -93,47 +91,11 @@ impl Config {
Some(path)
}
// Update the logger to output in JSON to specified file
fn update_logger(&mut self, log: &mut slog::Logger) -> Result<(), &'static str> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&self.log_file);
if file.is_err() {
return Err("Cannot open log file");
}
let file = file.unwrap();
if let Some(file) = self.log_file.to_str() {
info!(
*log,
"Log file specified, output will now be written to {} in json.", file
);
} else {
info!(
*log,
"Log file specified output will now be written in json"
);
}
let drain = Mutex::new(slog_json::Json::default(file)).fuse();
let drain = slog_async::Async::new(drain).build().fuse();
*log = slog::Logger::root(drain, o!());
Ok(())
}
/// Apply the following arguments to `self`, replacing values if they are specified in `args`.
///
/// Returns an error if arguments are obviously invalid. May succeed even if some values are
/// invalid.
pub fn apply_cli_args(
&mut self,
args: &ArgMatches,
log: &mut slog::Logger,
) -> Result<(), String> {
pub fn apply_cli_args(&mut self, args: &ArgMatches, _log: &slog::Logger) -> Result<(), String> {
if let Some(dir) = args.value_of("datadir") {
self.data_dir = PathBuf::from(dir);
};
@@ -146,11 +108,6 @@ impl Config {
self.rest_api.apply_cli_args(args)?;
self.websocket_server.apply_cli_args(args)?;
if let Some(log_file) = args.value_of("logfile") {
self.log_file = PathBuf::from(log_file);
self.update_logger(log)?;
};
Ok(())
}
}

View File

@@ -0,0 +1 @@
contract/

View File

@@ -0,0 +1,17 @@
[package]
name = "deposit_contract"
version = "0.1.0"
authors = ["Paul Hauner <paul@paulhauner.com>"]
edition = "2018"
build = "build.rs"
[build-dependencies]
reqwest = "0.9.20"
serde_json = "1.0"
[dependencies]
types = { path = "../../types"}
eth2_ssz = { path = "../ssz"}
tree_hash = { path = "../tree_hash"}
ethabi = "9.0"

View File

@@ -0,0 +1,56 @@
use ethabi::{Contract, Token};
use ssz::Encode;
use types::{DepositData, SecretKey};
pub use ethabi::Error;
pub const CONTRACT_DEPLOY_GAS: usize = 4_000_000;
pub const DEPOSIT_GAS: usize = 4_000_000;
pub const ABI: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.json");
pub const BYTECODE: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.bytecode");
pub fn eth1_tx_data(deposit_data: &DepositData) -> Result<Vec<u8>, Error> {
let params = vec![
Token::Bytes(deposit_data.pubkey.as_ssz_bytes()),
Token::Bytes(deposit_data.withdrawal_credentials.as_ssz_bytes()),
Token::Bytes(deposit_data.signature.as_ssz_bytes()),
];
let abi = Contract::load(ABI)?;
let function = abi.function("deposit")?;
function.encode_input(&params)
}
#[cfg(test)]
mod tests {
use super::*;
use types::{
test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Hash256, Keypair,
MinimalEthSpec, Signature,
};
type E = MinimalEthSpec;
fn get_deposit(keypair: Keypair, spec: &ChainSpec) -> DepositData {
let mut deposit_data = DepositData {
pubkey: keypair.pk.into(),
withdrawal_credentials: Hash256::from_slice(&[42; 32]),
amount: u64::max_value(),
signature: Signature::empty_signature().into(),
};
deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec);
deposit_data
}
#[test]
fn basic() {
let spec = &E::default_spec();
let keypair = generate_deterministic_keypair(42);
let deposit = get_deposit(keypair.clone(), spec);
let data = eth1_tx_data(&deposit).expect("should produce tx data");
assert_eq!(data.len(), 388, "bytes should be correct length");
}
}

View File

@@ -18,3 +18,4 @@ slog-async = "^2.3.0"
environment = { path = "./environment" }
futures = "0.1.25"
validator_client = { "path" = "../validator_client" }
account_manager = { "path" = "../account_manager" }

View File

@@ -17,3 +17,4 @@ slog-async = "^2.3.0"
ctrlc = { version = "3.1.1", features = ["termination"] }
futures = "0.1.25"
parking_lot = "0.7"
slog-json = "2.3.0"

View File

@@ -9,9 +9,12 @@
use eth2_config::Eth2Config;
use futures::{sync::oneshot, Future};
use slog::{o, Drain, Level, Logger};
use slog::{info, o, Drain, Level, Logger};
use sloggers::{null::NullLoggerBuilder, Build};
use std::cell::RefCell;
use std::fs::OpenOptions;
use std::path::PathBuf;
use std::sync::Mutex;
use tokio::runtime::{Builder as RuntimeBuilder, Runtime, TaskExecutor};
use types::{EthSpec, InteropEthSpec, MainnetEthSpec, MinimalEthSpec};
@@ -224,6 +227,28 @@ impl<E: EthSpec> Environment<E> {
.map_err(|e| format!("Tokio runtime shutdown returned an error: {:?}", e))
}
/// Sets the logger (and all child loggers) to log to a file.
pub fn log_to_json_file(&mut self, path: PathBuf) -> Result<(), String> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.map_err(|e| format!("Unable to open logfile: {:?}", e))?;
let drain = Mutex::new(slog_json::Json::default(file)).fuse();
let drain = slog_async::Async::new(drain).build().fuse();
self.log = slog::Logger::root(drain, o!());
info!(
self.log,
"Logging to JSON file";
"path" => format!("{:?}", path)
);
Ok(())
}
pub fn eth_spec_instance(&self) -> &E {
&self.eth_spec_instance
}

View File

@@ -6,6 +6,7 @@ use clap::{App, Arg, ArgMatches};
use env_logger::{Builder, Env};
use environment::EnvironmentBuilder;
use slog::{crit, info, warn};
use std::path::PathBuf;
use std::process::exit;
use types::EthSpec;
use validator_client::ProductionValidatorClient;
@@ -30,7 +31,6 @@ fn main() {
.value_name("TITLE")
.help("Specifies the default eth2 spec type. Only effective when creating a new datadir.")
.takes_value(true)
.required(true)
.possible_values(&["mainnet", "minimal", "interop"])
.global(true)
.default_value("minimal")
@@ -53,6 +53,7 @@ fn main() {
)
.subcommand(beacon_node::cli_app())
.subcommand(validator_client::cli_app())
.subcommand(account_manager::cli_app())
.get_matches();
macro_rules! run_with_spec {
@@ -93,6 +94,13 @@ fn run<E: EthSpec>(
let log = environment.core_context().log;
if let Some(log_path) = matches.value_of("logfile") {
let path = log_path
.parse::<PathBuf>()
.map_err(|e| format!("Failed to parse log path: {:?}", e))?;
environment.log_to_json_file(path)?;
}
if std::mem::size_of::<usize>() != 8 {
crit!(
log,
@@ -115,6 +123,16 @@ fn run<E: EthSpec>(
//
// Creating a command which can run both might be useful future works.
if let Some(sub_matches) = matches.subcommand_matches("account_manager") {
let runtime_context = environment.core_context();
account_manager::run(sub_matches, runtime_context);
// Exit early if the account manager was run. It does not used the tokio executor, so no
// need to wait for it to shutdown.
return Ok(());
}
let beacon_node = if let Some(sub_matches) = matches.subcommand_matches("Beacon Node") {
let runtime_context = environment.core_context();

View File

@@ -4,12 +4,6 @@ version = "0.1.0"
authors = ["Paul Hauner <paul@paulhauner.com>"]
edition = "2018"
build = "build.rs"
[build-dependencies]
reqwest = "0.9.20"
serde_json = "1.0"
[dependencies]
web3 = "0.8.0"
tokio = "0.1.17"
@@ -17,3 +11,4 @@ futures = "0.1.25"
types = { path = "../../eth2/types"}
eth2_ssz = { path = "../../eth2/utils/ssz"}
serde_json = "1.0"
deposit_contract = { path = "../../eth2/utils/deposit_contract"}

View File

@@ -7,6 +7,7 @@
//! some initial issues.
mod ganache;
use deposit_contract::{eth1_tx_data, ABI, BYTECODE, CONTRACT_DEPLOY_GAS, DEPOSIT_GAS};
use futures::{stream, Future, IntoFuture, Stream};
use ganache::GanacheInstance;
use ssz::Encode;
@@ -16,19 +17,12 @@ use types::DepositData;
use types::{EthSpec, Hash256, Keypair, Signature};
use web3::contract::{Contract, Options};
use web3::transports::Http;
use web3::types::{Address, U256};
use web3::types::{Address, TransactionRequest, U256};
use web3::{Transport, Web3};
pub const DEPLOYER_ACCOUNTS_INDEX: usize = 0;
pub const DEPOSIT_ACCOUNTS_INDEX: usize = 0;
const CONTRACT_DEPLOY_GAS: usize = 4_000_000;
const DEPOSIT_GAS: usize = 4_000_000;
// Deposit contract
pub const ABI: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.json");
pub const BYTECODE: &[u8] = include_bytes!("../contract/v0.8.3_validator_registration.bytecode");
/// Provides a dedicated ganache-cli instance with the deposit contract already deployed.
pub struct GanacheEth1Instance {
pub ganache: GanacheInstance,
@@ -138,6 +132,7 @@ impl DepositContract {
deposit_data: DepositData,
) -> impl Future<Item = (), Error = String> {
let contract = self.contract.clone();
let web3_1 = self.web3.clone();
self.web3
.eth()
@@ -149,19 +144,27 @@ impl DepositContract {
.cloned()
.ok_or_else(|| "Insufficient accounts for deposit".to_string())
})
.and_then(move |from_address| {
let params = (
deposit_data.pubkey.as_ssz_bytes(),
deposit_data.withdrawal_credentials.as_ssz_bytes(),
deposit_data.signature.as_ssz_bytes(),
);
let options = Options {
.and_then(move |from| {
let tx_request = TransactionRequest {
from,
to: Some(contract.address()),
gas: Some(U256::from(DEPOSIT_GAS)),
gas_price: None,
value: Some(from_gwei(deposit_data.amount)),
..Options::default()
// Note: the reason we use this `TransactionRequest` instead of just using the
// function in `self.contract` is so that the `eth1_tx_data` function gets used
// during testing.
//
// It's important that `eth1_tx_data` stays correct and does not suffer from
// code-rot.
data: eth1_tx_data(&deposit_data).map(Into::into).ok(),
nonce: None,
condition: None,
};
contract
.call("deposit", params, from_address, options)
web3_1
.eth()
.send_transaction(tx_request)
.map_err(|e| format!("Failed to call deposit fn: {:?}", e))
})
.map(|_| ())

View File

@@ -23,7 +23,6 @@ serde_derive = "1.0.102"
serde_json = "1.0.41"
slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] }
slog-async = "2.3.0"
slog-json = "2.3.0"
slog-term = "2.4.2"
tokio = "0.1.22"
tokio-timer = "0.2.11"

View File

@@ -22,16 +22,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.help("File path where output will be written.")
.takes_value(true),
)
.arg(
Arg::with_name("spec")
.long("spec")
.value_name("TITLE")
.help("Specifies the default eth2 spec type.")
.takes_value(true)
.possible_values(&["mainnet", "minimal", "interop"])
.conflicts_with("eth2-config")
.global(true)
)
.arg(
Arg::with_name("eth2-config")
.long("eth2-config")
@@ -66,16 +56,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.default_value(DEFAULT_SERVER_HTTP_PORT)
.takes_value(true),
)
.arg(
Arg::with_name("debug-level")
.long("debug-level")
.value_name("LEVEL")
.short("s")
.help("The title of the spec constants for chain config.")
.takes_value(true)
.possible_values(&["info", "debug", "trace", "warn", "error", "crit"])
.default_value("trace"),
)
/*
* The "testnet" sub-command.
*

View File

@@ -2,12 +2,11 @@ use bincode;
use bls::Keypair;
use clap::ArgMatches;
use serde_derive::{Deserialize, Serialize};
use slog::{error, info, o, warn, Drain};
use std::fs::{self, File, OpenOptions};
use slog::{error, warn};
use std::fs::{self, File};
use std::io::{Error, ErrorKind};
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Mutex;
use types::{
test_utils::{generate_deterministic_keypair, load_keypairs_from_yaml},
EthSpec, MainnetEthSpec,
@@ -94,17 +93,12 @@ impl Config {
pub fn apply_cli_args(
&mut self,
args: &ArgMatches,
log: &mut slog::Logger,
_log: &slog::Logger,
) -> Result<(), &'static str> {
if let Some(datadir) = args.value_of("datadir") {
self.data_dir = PathBuf::from(datadir);
};
if let Some(log_file) = args.value_of("logfile") {
self.log_file = PathBuf::from(log_file);
self.update_logger(log)?;
};
if let Some(srv) = args.value_of("server") {
self.server = srv.to_string();
};
@@ -112,38 +106,6 @@ impl Config {
Ok(())
}
// Update the logger to output in JSON to specified file
fn update_logger(&mut self, log: &mut slog::Logger) -> Result<(), &'static str> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&self.log_file);
if file.is_err() {
return Err("Cannot open log file");
}
let file = file.unwrap();
if let Some(file) = self.log_file.to_str() {
info!(
*log,
"Log file specified, output will now be written to {} in json.", file
);
} else {
info!(
*log,
"Log file specified output will now be written in json"
);
}
let drain = Mutex::new(slog_json::Json::default(file)).fuse();
let drain = slog_async::Async::new(drain).build().fuse();
*log = slog::Logger::root(drain, o!());
Ok(())
}
/// Reads a single keypair from the given `path`.
///
/// `path` should be the path to a directory containing a private key. The file name of `path`