mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-09 03:17:55 +00:00
Add progress on validator onboarding
This commit is contained in:
@@ -4,6 +4,9 @@ version = "0.0.1"
|
|||||||
authors = ["Luke Anderson <luke@sigmaprime.io>"]
|
authors = ["Luke Anderson <luke@sigmaprime.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempdir = "0.3"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bls = { path = "../eth2/utils/bls" }
|
bls = { path = "../eth2/utils/bls" }
|
||||||
clap = "2.33.0"
|
clap = "2.33.0"
|
||||||
@@ -14,3 +17,7 @@ validator_client = { path = "../validator_client" }
|
|||||||
types = { path = "../eth2/types" }
|
types = { path = "../eth2/types" }
|
||||||
dirs = "2.0.2"
|
dirs = "2.0.2"
|
||||||
environment = { path = "../lighthouse/environment" }
|
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" }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
|
pub mod validator;
|
||||||
|
|
||||||
use bls::Keypair;
|
use bls::Keypair;
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
@@ -15,6 +16,49 @@ pub const DEFAULT_DATA_DIR: &str = ".lighthouse-validator";
|
|||||||
pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml";
|
pub const CLIENT_CONFIG_FILENAME: &str = "account-manager.toml";
|
||||||
|
|
||||||
pub fn run<T: EthSpec>(matches: &ArgMatches, context: RuntimeContext<T>) {
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_account_manager<T: EthSpec>(
|
||||||
|
matches: &ArgMatches,
|
||||||
|
context: RuntimeContext<T>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let log = context.log.clone();
|
||||||
|
|
||||||
|
let data_dir = 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(DEFAULT_DATA_DIR);
|
||||||
|
default_dir
|
||||||
|
});
|
||||||
|
|
||||||
|
fs::create_dir_all(&data_dir).map_err(|e| format!("Failed to initialize data dir: {}", e))?;
|
||||||
|
|
||||||
|
let mut client_config = ValidatorClientConfig::default();
|
||||||
|
client_config.data_dir = data_dir.clone();
|
||||||
|
client_config
|
||||||
|
.apply_cli_args(&matches, &log)
|
||||||
|
.map_err(|e| format!("Failed to parse ClientConfig CLI arguments: {:?}", e))?;
|
||||||
|
|
||||||
|
info!(log, "Located data directory";
|
||||||
|
"path" => &client_config.data_dir.to_str());
|
||||||
|
|
||||||
|
panic!()
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_old<T: EthSpec>(matches: &ArgMatches, context: RuntimeContext<T>) {
|
||||||
let mut log = context.log;
|
let mut log = context.log;
|
||||||
|
|
||||||
let data_dir = match matches
|
let data_dir = match matches
|
||||||
|
|||||||
321
account_manager/src/validator.rs
Normal file
321
account_manager/src/validator.rs
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
use bls::get_withdrawal_credentials;
|
||||||
|
use deposit_contract::eth1_tx_data;
|
||||||
|
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";
|
||||||
|
|
||||||
|
fn keypair_file(prefix: &str) -> String {
|
||||||
|
format!("{}_keypair", prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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))?;
|
||||||
|
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 random_keypairs(mut self) -> Self {
|
||||||
|
self.voting_keypair = Some(Keypair::random());
|
||||||
|
self.withdrawal_keypair = Some(Keypair::random());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deterministic_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(&deposit_data)
|
||||||
|
.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 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")
|
||||||
|
.deterministic_keypairs(42)
|
||||||
|
.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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use ethabi::{Contract, Token};
|
use ethabi::{Contract, Token};
|
||||||
use ssz::Encode;
|
use ssz::Encode;
|
||||||
use types::{ChainSpec, DepositData, SecretKey};
|
use types::{DepositData, SecretKey};
|
||||||
|
|
||||||
pub use ethabi::Error;
|
pub use ethabi::Error;
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ pub fn eth1_tx_data(deposit_data: &DepositData) -> Result<Vec<u8>, Error> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use types::{
|
use types::{
|
||||||
test_utils::generate_deterministic_keypair, EthSpec, Hash256, Keypair, MinimalEthSpec,
|
test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Hash256, Keypair,
|
||||||
Signature,
|
MinimalEthSpec, Signature,
|
||||||
};
|
};
|
||||||
|
|
||||||
type E = MinimalEthSpec;
|
type E = MinimalEthSpec;
|
||||||
|
|||||||
Reference in New Issue
Block a user