mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-18 20:32:45 +00:00
Wallet-based, encrypted key management (#1138)
* Update hashmap hashset to stable futures * Adds panic test to hashset delay * Port remote_beacon_node to stable futures * Fix lcli merge conflicts * Non rpc stuff compiles * Remove padding * Add error enum, zeroize more things * Fix comment * protocol.rs compiles * Port websockets, timer and notifier to stable futures (#1035) * Fix lcli * Port timer to stable futures * Fix timer * Port websocket_server to stable futures * Port notifier to stable futures * Add TODOS * Port remote_beacon_node to stable futures * Partial eth2-libp2p stable future upgrade * Finished first round of fighting RPC types * Further progress towards porting eth2-libp2p adds caching to discovery * Update behaviour * Add keystore builder * Remove keystore stuff from val client * Add more tests, comments * RPC handler to stable futures * Update RPC to master libp2p * Add more comments, test vectors * Network service additions * Progress on improving JSON validation * More JSON verification * Start moving JSON into own mod * Remove old code * Add more tests, reader/writers * Tidy * Move keystore into own file * Move more logic into keystore file * Tidy * Tidy * Fix the fallback transport construction (#1102) * Allow for odd-character hex * Correct warning * Remove hashmap delay * Compiling version of eth2-libp2p * Update all crates versions * Fix conversion function and add tests (#1113) * Add more json missing field checks * Use scrypt by default * Tidy, address comments * Test path and uuid in vectors * Fix comment * Add checks for kdf params * Enforce empty kdf message * Port validator_client to stable futures (#1114) * Add PH & MS slot clock changes * Account for genesis time * Add progress on duties refactor * Add simple is_aggregator bool to val subscription * Start work on attestation_verification.rs * Add progress on ObservedAttestations * Progress with ObservedAttestations * Fix tests * Add observed attestations to the beacon chain * Add attestation observation to processing code * Add progress on attestation verification * Add first draft of ObservedAttesters * Add more tests * Add observed attesters to beacon chain * Add observers to attestation processing * Add more attestation verification * Create ObservedAggregators map * Remove commented-out code * Add observed aggregators into chain * Add progress * Finish adding features to attestation verification * Ensure beacon chain compiles * Link attn verification into chain * Integrate new attn verification in chain * Remove old attestation processing code * Start trying to fix beacon_chain tests * Split adding into pools into two functions * Add aggregation to harness * Get test harness working again * Adjust the number of aggregators for test harness * Fix edge-case in harness * Integrate new attn processing in network * Fix compile bug in validator_client * Update validator API endpoints * Fix aggreagation in test harness * Fix enum thing * Fix attestation observation bug: * Patch failing API tests * Start adding comments to attestation verification * Remove unused attestation field * Unify "is block known" logic * Update comments * Supress fork choice errors for network processing * Add todos * Tidy * Add gossip attn tests * Disallow test harness to produce old attns * Comment out in-progress tests * Partially address pruning tests * Fix failing store test * Add aggregate tests * Add comments about which spec conditions we check * Dont re-aggregate * Split apart test harness attn production * Fix compile error in network * Make progress on commented-out test * Fix skipping attestation test * Add fork choice verification tests * Tidy attn tests, remove dead code * Remove some accidentally added code * Fix clippy lint * Rename test file * Add block tests, add cheap block proposer check * Rename block testing file * Add observed_block_producers * Tidy * Switch around block signature verification * Finish block testing * Remove gossip from signature tests * First pass of self review * Fix deviation in spec * Update test spec tags * Start moving over to hashset * Finish moving observed attesters to hashmap * Move aggregation pool over to hashmap * Make fc attn borrow again * Fix rest_api compile error * Fix missing comments * Fix monster test * Uncomment increasing slots test * Address remaining comments * Remove unsafe, use cfg test * Remove cfg test flag * Fix dodgy comment * Revert "Update hashmap hashset to stable futures" This reverts commitd432378a3c. * Revert "Adds panic test to hashset delay" This reverts commit281502396f. * Ported attestation_service * Ported duties_service * Ported fork_service * More ports * Port block_service * Minor fixes * VC compiles * Update TODOS * Borrow self where possible * Ignore aggregates that are already known. * Unify aggregator modulo logic * Fix typo in logs * Refactor validator subscription logic * Avoid reproducing selection proof * Skip HTTP call if no subscriptions * Rename DutyAndState -> DutyAndProof * Tidy logs * Print root as dbg * Fix compile errors in tests * Fix compile error in test * Re-Fix attestation and duties service * Minor fixes Co-authored-by: Paul Hauner <paul@paulhauner.com> * Expose json_keystore mod * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * First commits on path derivation * Progress with implementation * Move key derivation into own crate * Start defining JSON wallet * Add progress * Split out encrypt/decrypt * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * Add progress * Replace some password usage with slice * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * Add progress * Expose PlainText struct * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * Add builder * Expose consts, remove Password * Minor progress * Expose SALT_SIZE * First compiling version * Add test vectors * Network crate update to stable futures * Move dbg assert statement * Port account_manager to stable futures (#1121) * Port account_manager to stable futures * Run async fns in tokio environment * Port rest_api crate to stable futures (#1118) * Port rest_api lib to stable futures * Reduce tokio features * Update notifier to stable futures * Builder update * Further updates * Add mnemonic, tidy * Convert self referential async functions * Tidy * Add testing * Add first attempt at validator_dir * Present pubkey field * stable futures fixes (#1124) * Fix eth1 update functions * Fix genesis and client * Fix beacon node lib * Return appropriate runtimes from environment * Fix test rig * Refactor eth1 service update * Upgrade simulator to stable futures * Lighthouse compiles on stable futures * Add first pass of wallet manager * Progress with CLI * Remove println debugging statement * Tidy output * Tidy 600 perms * Update libp2p service, start rpc test upgrade * Add validator creation flow * Update network crate for new libp2p * Start tidying, adding comments * Update tokio::codec to futures_codec (#1128) * Further work towards RPC corrections * Correct http timeout and network service select * Add wallet mgr testing * Shift LockedWallet into own file * Add comments to fs * Start integration into VC * Use tokio runtime for libp2p * Revert "Update tokio::codec to futures_codec (#1128)" This reverts commite57aea924a. * Upgrade RPC libp2p tests * Upgrade secio fallback test * Add lcli keypair upgrade command * Upgrade gossipsub examples * Clean up RPC protocol * Test fixes (#1133) * Correct websocket timeout and run on os thread * Fix network test * Add --secrets-dir to VC * Remove --legacy-keys from VC * Clean up PR * Correct tokio tcp move attestation service tests * Upgrade attestation service tests * Fix sim * Correct network test * Correct genesis test * Start docs * Add progress for validator generation * Tidy error messages * Test corrections * Log info when block is received * Modify logs and update attester service events * Stable futures: fixes to vc, eth1 and account manager (#1142) * Add local testnet scripts * Remove whiteblock script * Rename local testnet script * Move spawns onto handle * Fix VC panic * Initial fix to block production issue * Tidy block producer fix * Tidy further * Add local testnet clean script * Run cargo fmt * Tidy duties service * Tidy fork service * Tidy ForkService * Tidy AttestationService * Tidy notifier * Ensure await is not suppressed in eth1 * Ensure await is not suppressed in account_manager * Use .ok() instead of .unwrap_or(()) * RPC decoding test for proto * Update discv5 and eth2-libp2p deps * Run cargo fmt * Pre-build keystores for sim * Fix lcli double runtime issue (#1144) * Handle stream termination and dialing peer errors * Correct peer_info variant types * Add progress on new deposit flow * Remove unnecessary warnings * Handle subnet unsubscription removal and improve logigng * Add logs around ping * Upgrade discv5 and improve logging * Handle peer connection status for multiple connections * Improve network service logging * Add more incomplete progress * Improve logging around peer manager * Upgrade swarm poll centralise peer management * Identify clients on error * Fix `remove_peer` in sync (#1150) * remove_peer removes from all chains * Remove logs * Fix early return from loop * Improved logging, fix panic * Partially correct tests * Add deposit command * Remove old validator directory * Start adding AM tests * Stable futures: Vc sync (#1149) * Improve syncing heuristic * Add comments * Use safer method for tolerance * Fix tests * Binary testing progress * Progress with CLI tests * Use constants for flags * More account manager testing * Improve CLI tests * Move upgrade-legacy-keypairs into account man * Use rayon for VC key generation * Add comments to `validator_dir` * Add testing to validator_dir * Add fix to eth1-sim * Check errors in eth1-sim * Fix mutability issue * Ensure password file ends in .pass * Add more tests to wallet manager * Tidy deposit * Tidy account manager * Tidy account manager * Remove panic * Generate keypairs earlier in sim * Tidy eth1-sime * Try to fix eth1 sim * Address review comments * Fix typo in CLI command * Update docs * Disable eth1 sim * Remove eth1 sim completely Co-authored-by: Age Manning <Age@AgeManning.com> Co-authored-by: pawanjay176 <pawandhananjay@gmail.com>
This commit is contained in:
26
eth2/utils/validator_dir/Cargo.toml
Normal file
26
eth2/utils/validator_dir/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "validator_dir"
|
||||
version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
unencrypted_keys = []
|
||||
insecure_keys = []
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
eth2_wallet = { path = "../eth2_wallet" }
|
||||
bls = { path = "../bls" }
|
||||
eth2_keystore = { path = "../eth2_keystore" }
|
||||
types = { path = "../../types" }
|
||||
rand = "0.7.2"
|
||||
deposit_contract = { path = "../deposit_contract" }
|
||||
eth2_ssz = { path = "../ssz" }
|
||||
eth2_ssz_derive = { path = "../ssz_derive" }
|
||||
rayon = "1.3.0"
|
||||
tree_hash = { path = "../tree_hash" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1.0"
|
||||
286
eth2/utils/validator_dir/src/builder.rs
Normal file
286
eth2/utils/validator_dir/src/builder.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use crate::{Error as DirError, ValidatorDir};
|
||||
use bls::get_withdrawal_credentials;
|
||||
use deposit_contract::{encode_eth1_tx_data, Error as DepositError};
|
||||
use eth2_keystore::{Error as KeystoreError, Keystore, KeystoreBuilder, PlainText};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use std::fs::{create_dir_all, File, OpenOptions};
|
||||
use std::io::{self, Write};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use types::{ChainSpec, DepositData, Hash256, Keypair, Signature};
|
||||
|
||||
/// The `Alphanumeric` crate only generates a-z, A-Z, 0-9, therefore it has a range of 62
|
||||
/// characters.
|
||||
///
|
||||
/// 62**48 is greater than 255**32, therefore this password has more bits of entropy than a byte
|
||||
/// array of length 32.
|
||||
const DEFAULT_PASSWORD_LEN: usize = 48;
|
||||
|
||||
pub const VOTING_KEYSTORE_FILE: &str = "voting-keystore.json";
|
||||
pub const WITHDRAWAL_KEYSTORE_FILE: &str = "withdrawal-keystore.json";
|
||||
pub const ETH1_DEPOSIT_DATA_FILE: &str = "eth1-deposit-data.rlp";
|
||||
pub const ETH1_DEPOSIT_AMOUNT_FILE: &str = "eth1-deposit-gwei.txt";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DirectoryAlreadyExists(PathBuf),
|
||||
UnableToCreateDir(io::Error),
|
||||
UnableToEncodeDeposit(DepositError),
|
||||
DepositDataAlreadyExists(PathBuf),
|
||||
UnableToSaveDepositData(io::Error),
|
||||
DepositAmountAlreadyExists(PathBuf),
|
||||
UnableToSaveDepositAmount(io::Error),
|
||||
KeystoreAlreadyExists(PathBuf),
|
||||
UnableToSaveKeystore(io::Error),
|
||||
PasswordAlreadyExists(PathBuf),
|
||||
UnableToSavePassword(io::Error),
|
||||
KeystoreError(KeystoreError),
|
||||
UnableToOpenDir(DirError),
|
||||
#[cfg(feature = "insecure_keys")]
|
||||
InsecureKeysError(String),
|
||||
}
|
||||
|
||||
impl From<KeystoreError> for Error {
|
||||
fn from(e: KeystoreError) -> Error {
|
||||
Error::KeystoreError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for creating a `ValidatorDir`.
|
||||
pub struct Builder<'a> {
|
||||
base_validators_dir: PathBuf,
|
||||
password_dir: PathBuf,
|
||||
pub(crate) voting_keystore: Option<(Keystore, PlainText)>,
|
||||
pub(crate) withdrawal_keystore: Option<(Keystore, PlainText)>,
|
||||
store_withdrawal_keystore: bool,
|
||||
deposit_info: Option<(u64, &'a ChainSpec)>,
|
||||
}
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
/// Instantiate a new builder.
|
||||
pub fn new(base_validators_dir: PathBuf, password_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
base_validators_dir,
|
||||
password_dir,
|
||||
voting_keystore: None,
|
||||
withdrawal_keystore: None,
|
||||
store_withdrawal_keystore: true,
|
||||
deposit_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the `ValidatorDir` use the given `keystore` which can be unlocked with `password`.
|
||||
///
|
||||
/// If this argument (or equivalent key specification argument) is not supplied a keystore will
|
||||
/// be randomly generated.
|
||||
pub fn voting_keystore(mut self, keystore: Keystore, password: &[u8]) -> Self {
|
||||
self.voting_keystore = Some((keystore, password.to_vec().into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the `ValidatorDir` use the given `keystore` which can be unlocked with `password`.
|
||||
///
|
||||
/// If this argument (or equivalent key specification argument) is not supplied a keystore will
|
||||
/// be randomly generated.
|
||||
pub fn withdrawal_keystore(mut self, keystore: Keystore, password: &[u8]) -> Self {
|
||||
self.withdrawal_keystore = Some((keystore, password.to_vec().into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Upon build, create files in the `ValidatorDir` which will permit the submission of a
|
||||
/// deposit to the eth1 deposit contract with the given `deposit_amount`.
|
||||
pub fn create_eth1_tx_data(mut self, deposit_amount: u64, spec: &'a ChainSpec) -> Self {
|
||||
self.deposit_info = Some((deposit_amount, spec));
|
||||
self
|
||||
}
|
||||
|
||||
/// If `should_store == true`, the validator keystore will be saved in the `ValidatorDir` (and
|
||||
/// the password to it stored in the `password_dir`). If `should_store == false`, the
|
||||
/// withdrawal keystore will be dropped after `Self::build`.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// If `should_store == false`, it is important to ensure that the withdrawal keystore is
|
||||
/// backed up. Backup can be via saving the files elsewhere, or in the case of HD key
|
||||
/// derivation, ensuring the seed and path are known.
|
||||
///
|
||||
/// If the builder is not specifically given a withdrawal keystore then one will be generated
|
||||
/// randomly. When this random keystore is generated, calls to this function are ignored and
|
||||
/// the withdrawal keystore is *always* stored to disk. This is to prevent data loss.
|
||||
pub fn store_withdrawal_keystore(mut self, should_store: bool) -> Self {
|
||||
self.store_withdrawal_keystore = should_store;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consumes `self`, returning a `ValidatorDir` if no error is encountered.
|
||||
pub fn build(mut self) -> Result<ValidatorDir, Error> {
|
||||
// If the withdrawal keystore will be generated randomly, always store it.
|
||||
if self.withdrawal_keystore.is_none() {
|
||||
self.store_withdrawal_keystore = true;
|
||||
}
|
||||
|
||||
// Attempts to get `self.$keystore`, unwrapping it into a random keystore if it is `None`.
|
||||
// Then, decrypts the keypair from the keystore.
|
||||
macro_rules! expand_keystore {
|
||||
($keystore: ident) => {
|
||||
self.$keystore
|
||||
.map(Result::Ok)
|
||||
.unwrap_or_else(random_keystore)
|
||||
.and_then(|(keystore, password)| {
|
||||
keystore
|
||||
.decrypt_keypair(password.as_bytes())
|
||||
.map(|keypair| (keystore, password, keypair))
|
||||
.map_err(Into::into)
|
||||
})?;
|
||||
};
|
||||
}
|
||||
|
||||
let (voting_keystore, voting_password, voting_keypair) = expand_keystore!(voting_keystore);
|
||||
let (withdrawal_keystore, withdrawal_password, withdrawal_keypair) =
|
||||
expand_keystore!(withdrawal_keystore);
|
||||
|
||||
let dir = self
|
||||
.base_validators_dir
|
||||
.join(format!("0x{}", voting_keystore.pubkey()));
|
||||
|
||||
if dir.exists() {
|
||||
return Err(Error::DirectoryAlreadyExists(dir));
|
||||
} else {
|
||||
create_dir_all(&dir).map_err(Error::UnableToCreateDir)?;
|
||||
}
|
||||
|
||||
if let Some((amount, spec)) = self.deposit_info {
|
||||
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);
|
||||
|
||||
let deposit_data =
|
||||
encode_eth1_tx_data(&deposit_data).map_err(Error::UnableToEncodeDeposit)?;
|
||||
|
||||
// Save `ETH1_DEPOSIT_DATA_FILE` to file.
|
||||
//
|
||||
// This allows us to know the RLP data for the eth1 transaction without needed to know
|
||||
// the withdrawal/voting keypairs again at a later date.
|
||||
let path = dir.clone().join(ETH1_DEPOSIT_DATA_FILE);
|
||||
if path.exists() {
|
||||
return Err(Error::DepositDataAlreadyExists(path));
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(path.clone())
|
||||
.map_err(Error::UnableToSaveDepositData)?
|
||||
.write_all(&deposit_data)
|
||||
.map_err(Error::UnableToSaveDepositData)?
|
||||
}
|
||||
|
||||
// Save `ETH1_DEPOSIT_AMOUNT_FILE` to file.
|
||||
//
|
||||
// This allows us to know the intended deposit amount at a later date.
|
||||
let path = dir.clone().join(ETH1_DEPOSIT_AMOUNT_FILE);
|
||||
if path.exists() {
|
||||
return Err(Error::DepositAmountAlreadyExists(path));
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(path.clone())
|
||||
.map_err(Error::UnableToSaveDepositAmount)?
|
||||
.write_all(format!("{}", amount).as_bytes())
|
||||
.map_err(Error::UnableToSaveDepositAmount)?
|
||||
}
|
||||
}
|
||||
|
||||
write_password_to_file(
|
||||
self.password_dir
|
||||
.clone()
|
||||
.join(voting_keypair.pk.as_hex_string()),
|
||||
voting_password.as_bytes(),
|
||||
)?;
|
||||
|
||||
write_keystore_to_file(dir.clone().join(VOTING_KEYSTORE_FILE), &voting_keystore)?;
|
||||
|
||||
if self.store_withdrawal_keystore {
|
||||
write_password_to_file(
|
||||
self.password_dir
|
||||
.clone()
|
||||
.join(withdrawal_keypair.pk.as_hex_string()),
|
||||
withdrawal_password.as_bytes(),
|
||||
)?;
|
||||
write_keystore_to_file(
|
||||
dir.clone().join(WITHDRAWAL_KEYSTORE_FILE),
|
||||
&withdrawal_keystore,
|
||||
)?;
|
||||
}
|
||||
|
||||
ValidatorDir::open(dir).map_err(Error::UnableToOpenDir)
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a JSON keystore to file.
|
||||
fn write_keystore_to_file(path: PathBuf, keystore: &Keystore) -> Result<(), Error> {
|
||||
if path.exists() {
|
||||
Err(Error::KeystoreAlreadyExists(path))
|
||||
} else {
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create_new(true)
|
||||
.open(path.clone())
|
||||
.map_err(Error::UnableToSaveKeystore)?;
|
||||
|
||||
keystore.to_json_writer(file).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a file with `600 (-rw-------)` permissions.
|
||||
pub fn write_password_to_file<P: AsRef<Path>>(path: P, bytes: &[u8]) -> Result<(), Error> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if path.exists() {
|
||||
return Err(Error::PasswordAlreadyExists(path.into()));
|
||||
}
|
||||
|
||||
let mut file = File::create(&path).map_err(Error::UnableToSavePassword)?;
|
||||
|
||||
let mut perm = file
|
||||
.metadata()
|
||||
.map_err(Error::UnableToSavePassword)?
|
||||
.permissions();
|
||||
|
||||
perm.set_mode(0o600);
|
||||
|
||||
file.set_permissions(perm)
|
||||
.map_err(Error::UnableToSavePassword)?;
|
||||
|
||||
file.write_all(bytes).map_err(Error::UnableToSavePassword)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a random keystore with a random password.
|
||||
fn random_keystore() -> Result<(Keystore, PlainText), Error> {
|
||||
let keypair = Keypair::random();
|
||||
let password: PlainText = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(DEFAULT_PASSWORD_LEN)
|
||||
.collect::<String>()
|
||||
.into_bytes()
|
||||
.into();
|
||||
|
||||
let keystore = KeystoreBuilder::new(&keypair, password.as_bytes(), "".into())?.build()?;
|
||||
|
||||
Ok((keystore, password))
|
||||
}
|
||||
67
eth2/utils/validator_dir/src/insecure_keys.rs
Normal file
67
eth2/utils/validator_dir/src/insecure_keys.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! These features exist to allow for generating deterministic, well-known, unsafe keys for use in
|
||||
//! testing.
|
||||
//!
|
||||
//! **NEVER** use these keys in production!
|
||||
#![cfg(feature = "insecure_keys")]
|
||||
|
||||
use crate::{Builder, BuilderError};
|
||||
use eth2_keystore::{Keystore, KeystoreBuilder, PlainText};
|
||||
use std::path::PathBuf;
|
||||
use types::test_utils::generate_deterministic_keypair;
|
||||
|
||||
/// A very weak password with which to encrypt the keystores.
|
||||
pub const INSECURE_PASSWORD: &[u8] = &[30; 32];
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
/// Generate the voting and withdrawal keystores using deterministic, well-known, **unsafe**
|
||||
/// keypairs.
|
||||
///
|
||||
/// **NEVER** use these keys in production!
|
||||
pub fn insecure_keys(mut self, deterministic_key_index: usize) -> Result<Self, BuilderError> {
|
||||
self.voting_keystore = Some(
|
||||
generate_deterministic_keystore(deterministic_key_index)
|
||||
.map_err(BuilderError::InsecureKeysError)?,
|
||||
);
|
||||
self.withdrawal_keystore = Some(
|
||||
generate_deterministic_keystore(deterministic_key_index)
|
||||
.map_err(BuilderError::InsecureKeysError)?,
|
||||
);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a keystore, encrypted with `INSECURE_PASSWORD` using a deterministic, well-known,
|
||||
/// **unsafe** secret key.
|
||||
///
|
||||
/// **NEVER** use these keys in production!
|
||||
pub fn generate_deterministic_keystore(i: usize) -> Result<(Keystore, PlainText), String> {
|
||||
let keypair = generate_deterministic_keypair(i);
|
||||
|
||||
let keystore = KeystoreBuilder::new(&keypair, INSECURE_PASSWORD, "".into())
|
||||
.map_err(|e| format!("Unable to create keystore builder: {:?}", e))?
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to build keystore: {:?}", e))?;
|
||||
|
||||
Ok((keystore, INSECURE_PASSWORD.to_vec().into()))
|
||||
}
|
||||
|
||||
/// A helper function to use the `Builder` to generate deterministic, well-known, **unsafe**
|
||||
/// validator directories for the given validator `indices`.
|
||||
///
|
||||
/// **NEVER** use these keys in production!
|
||||
pub fn build_deterministic_validator_dirs(
|
||||
validators_dir: PathBuf,
|
||||
password_dir: PathBuf,
|
||||
indices: &[usize],
|
||||
) -> Result<(), String> {
|
||||
for &i in indices {
|
||||
Builder::new(validators_dir.clone(), password_dir.clone())
|
||||
.insecure_keys(i)
|
||||
.map_err(|e| format!("Unable to generate insecure keypair: {:?}", e))?
|
||||
.store_withdrawal_keystore(false)
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to build keystore: {:?}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
21
eth2/utils/validator_dir/src/lib.rs
Normal file
21
eth2/utils/validator_dir/src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! Provides:
|
||||
//!
|
||||
//! - `ValidatorDir`: manages a directory containing validator keypairs, deposit info and other
|
||||
//! things.
|
||||
//! - `Manager`: manages a directory that contains multiple `ValidatorDir`.
|
||||
//!
|
||||
//! This crate is intended to be used by the account manager to create validators and the validator
|
||||
//! client to load those validators.
|
||||
|
||||
mod builder;
|
||||
pub mod insecure_keys;
|
||||
mod manager;
|
||||
pub mod unencrypted_keys;
|
||||
mod validator_dir;
|
||||
|
||||
pub use crate::validator_dir::{Error, Eth1DepositData, ValidatorDir, ETH1_DEPOSIT_TX_HASH_FILE};
|
||||
pub use builder::{
|
||||
Builder, Error as BuilderError, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE,
|
||||
WITHDRAWAL_KEYSTORE_FILE,
|
||||
};
|
||||
pub use manager::{Error as ManagerError, Manager};
|
||||
115
eth2/utils/validator_dir/src/manager.rs
Normal file
115
eth2/utils/validator_dir/src/manager.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use crate::{Error as ValidatorDirError, ValidatorDir};
|
||||
use bls::Keypair;
|
||||
use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_dir;
|
||||
use std::io;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DirectoryDoesNotExist(PathBuf),
|
||||
UnableToReadBaseDir(io::Error),
|
||||
UnableToReadFile(io::Error),
|
||||
ValidatorDirError(ValidatorDirError),
|
||||
}
|
||||
|
||||
/// Manages a directory containing multiple `ValidatorDir` directories.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// validators
|
||||
/// └── 0x91494d3ac4c078049f37aa46934ba8cdf5a9cca6e1b9a9e12403d69d8a2c43a25a7f576df2a5a3d7cb3f45e6aa5e2812
|
||||
/// ├── eth1_deposit_data.rlp
|
||||
/// ├── deposit-tx-hash.txt
|
||||
/// ├── voting-keystore.json
|
||||
/// └── withdrawal-keystore.json
|
||||
/// ```
|
||||
pub struct Manager {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
/// Open a directory containing multiple validators.
|
||||
///
|
||||
/// Pass the `validators` director as `dir` (see struct-level example).
|
||||
pub fn open<P: AsRef<Path>>(dir: P) -> Result<Self, Error> {
|
||||
let dir: PathBuf = dir.as_ref().into();
|
||||
|
||||
if dir.exists() {
|
||||
Ok(Self { dir })
|
||||
} else {
|
||||
Err(Error::DirectoryDoesNotExist(dir))
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate the nodes in `self.dir`, filtering out things that are unlikely to be a validator
|
||||
/// directory.
|
||||
fn iter_dir(&self) -> Result<Vec<PathBuf>, Error> {
|
||||
read_dir(&self.dir)
|
||||
.map_err(Error::UnableToReadBaseDir)?
|
||||
.map(|file_res| file_res.map(|f| f.path()))
|
||||
// We use `map_or` with `true` here to ensure that we always fail if there is any
|
||||
// error.
|
||||
.filter(|path_res| path_res.as_ref().map_or(true, |p| p.is_dir()))
|
||||
.map(|res| res.map_err(Error::UnableToReadFile))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Open a `ValidatorDir` at the given `path`.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// It is not enforced that `path` is contained in `self.dir`.
|
||||
pub fn open_validator<P: AsRef<Path>>(&self, path: P) -> Result<ValidatorDir, Error> {
|
||||
ValidatorDir::open(path).map_err(Error::ValidatorDirError)
|
||||
}
|
||||
|
||||
/// Opens all the validator directories in `self`.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error if any of the directories is unable to be opened, perhaps due to a
|
||||
/// file-system error or directory with an active lockfile.
|
||||
pub fn open_all_validators(&self) -> Result<Vec<ValidatorDir>, Error> {
|
||||
self.iter_dir()?
|
||||
.into_iter()
|
||||
.map(|path| ValidatorDir::open(path).map_err(Error::ValidatorDirError))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Opens all the validator directories in `self` and decrypts the validator keypairs.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error if any of the directories is unable to be opened.
|
||||
pub fn decrypt_all_validators(
|
||||
&self,
|
||||
secrets_dir: PathBuf,
|
||||
) -> Result<Vec<(Keypair, ValidatorDir)>, Error> {
|
||||
self.iter_dir()?
|
||||
.into_par_iter()
|
||||
.map(|path| {
|
||||
ValidatorDir::open(path)
|
||||
.and_then(|v| v.voting_keypair(&secrets_dir).map(|kp| (kp, v)))
|
||||
.map_err(Error::ValidatorDirError)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a map of directory name to full directory path. E.g., `myval -> /home/vals/myval`.
|
||||
/// Filters out nodes in `self.dir` that are unlikely to be a validator directory.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error if a directory is unable to be read.
|
||||
pub fn directory_names(&self) -> Result<HashMap<String, PathBuf>, Error> {
|
||||
Ok(HashMap::from_iter(
|
||||
self.iter_dir()?
|
||||
.into_iter()
|
||||
.map(|path| (format!("{:?}", path), path)),
|
||||
))
|
||||
}
|
||||
}
|
||||
66
eth2/utils/validator_dir/src/unencrypted_keys.rs
Normal file
66
eth2/utils/validator_dir/src/unencrypted_keys.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! The functionality in this module is only required for backward compatibility with the old
|
||||
//! method of key generation (unencrypted, SSZ-encoded keypairs). It should be removed as soon as
|
||||
//! we're confident that no-one is using these keypairs anymore (hopefully mid-June 2020).
|
||||
#![cfg(feature = "unencrypted_keys")]
|
||||
|
||||
use eth2_keystore::PlainText;
|
||||
use ssz::Decode;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use types::{Keypair, PublicKey, SecretKey};
|
||||
|
||||
/// Read a keypair from disk, using the old format where keys were stored as unencrypted
|
||||
/// SSZ-encoded keypairs.
|
||||
///
|
||||
/// This only exists as compatibility with the old scheme and should not be implemented on any new
|
||||
/// features.
|
||||
pub fn load_unencrypted_keypair<P: AsRef<Path>>(path: P) -> Result<Keypair, String> {
|
||||
let path = path.as_ref();
|
||||
|
||||
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))?;
|
||||
|
||||
let bytes: PlainText = bytes.into();
|
||||
|
||||
SszEncodableKeypair::from_ssz_bytes(bytes.as_bytes())
|
||||
.map(Into::into)
|
||||
.map_err(|e| format!("Unable to decode keypair: {:?}", e))
|
||||
}
|
||||
|
||||
/// A helper struct to allow SSZ enc/dec for a `Keypair`.
|
||||
///
|
||||
/// This only exists as compatibility with the old scheme and should not be implemented on any new
|
||||
/// features.
|
||||
#[derive(Encode, Decode)]
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
230
eth2/utils/validator_dir/src/validator_dir.rs
Normal file
230
eth2/utils/validator_dir/src/validator_dir.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use crate::builder::{
|
||||
ETH1_DEPOSIT_AMOUNT_FILE, ETH1_DEPOSIT_DATA_FILE, VOTING_KEYSTORE_FILE,
|
||||
WITHDRAWAL_KEYSTORE_FILE,
|
||||
};
|
||||
use deposit_contract::decode_eth1_tx_data;
|
||||
use eth2_keystore::{Error as KeystoreError, Keystore, PlainText};
|
||||
use std::fs::{read, remove_file, write, OpenOptions};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tree_hash::TreeHash;
|
||||
use types::{DepositData, Hash256, Keypair};
|
||||
|
||||
/// The file used for indicating if a directory is in-use by another process.
|
||||
const LOCK_FILE: &str = ".lock";
|
||||
|
||||
/// The file used to save the Eth1 transaction hash from a deposit.
|
||||
pub const ETH1_DEPOSIT_TX_HASH_FILE: &str = "eth1-deposit-tx-hash.txt";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DirectoryDoesNotExist(PathBuf),
|
||||
DirectoryLocked(PathBuf),
|
||||
UnableToCreateLockfile(io::Error),
|
||||
UnableToOpenKeystore(io::Error),
|
||||
UnableToReadKeystore(KeystoreError),
|
||||
UnableToOpenPassword(io::Error),
|
||||
UnableToReadPassword(PathBuf),
|
||||
UnableToDecryptKeypair(KeystoreError),
|
||||
UnableToReadDepositData(io::Error),
|
||||
DepositAmountDoesNotExist(PathBuf),
|
||||
UnableToReadDepositAmount(io::Error),
|
||||
UnableToParseDepositAmount(std::num::ParseIntError),
|
||||
DepositAmountIsNotUtf8(std::string::FromUtf8Error),
|
||||
UnableToParseDepositData(deposit_contract::DecodeError),
|
||||
Eth1TxHashExists(PathBuf),
|
||||
UnableToWriteEth1TxHash(io::Error),
|
||||
/// The deposit root in the deposit data file does not match the one generated locally. This is
|
||||
/// generally caused by supplying an `amount` at deposit-time that is different to the one used
|
||||
/// at generation-time.
|
||||
Eth1DepositRootMismatch,
|
||||
#[cfg(feature = "unencrypted_keys")]
|
||||
SszKeypairError(String),
|
||||
}
|
||||
|
||||
/// Information required to submit a deposit to the Eth1 deposit contract.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Eth1DepositData {
|
||||
/// An RLP encoded Eth1 transaction.
|
||||
pub rlp: Vec<u8>,
|
||||
/// The deposit data used to generate `self.rlp`.
|
||||
pub deposit_data: DepositData,
|
||||
/// The root of `self.deposit_data`.
|
||||
pub root: Hash256,
|
||||
}
|
||||
|
||||
/// Provides a wrapper around a directory containing validator information.
|
||||
///
|
||||
/// Creates/deletes a lockfile in `self.dir` to attempt to prevent concurrent access from multiple
|
||||
/// processes.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ValidatorDir {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ValidatorDir {
|
||||
/// Open `dir`, creating a lockfile to prevent concurrent access.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If there is a filesystem error or if a lockfile already exists.
|
||||
pub fn open<P: AsRef<Path>>(dir: P) -> Result<Self, Error> {
|
||||
let dir: &Path = dir.as_ref();
|
||||
let dir: PathBuf = dir.into();
|
||||
|
||||
if !dir.exists() {
|
||||
return Err(Error::DirectoryDoesNotExist(dir));
|
||||
}
|
||||
|
||||
let lockfile = dir.join(LOCK_FILE);
|
||||
if lockfile.exists() {
|
||||
return Err(Error::DirectoryLocked(dir));
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(lockfile)
|
||||
.map_err(Error::UnableToCreateLockfile)?;
|
||||
}
|
||||
|
||||
Ok(Self { dir })
|
||||
}
|
||||
|
||||
/// Returns the `dir` provided to `Self::open`.
|
||||
pub fn dir(&self) -> &PathBuf {
|
||||
&self.dir
|
||||
}
|
||||
|
||||
/// Attempts to read the keystore in `self.dir` and decrypt the keypair using a password file
|
||||
/// in `password_dir`.
|
||||
///
|
||||
/// The password file that is used will be based upon the pubkey value in the keystore.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If there is a filesystem error, a password is missing or the password is incorrect.
|
||||
pub fn voting_keypair<P: AsRef<Path>>(&self, password_dir: P) -> Result<Keypair, Error> {
|
||||
unlock_keypair(&self.dir.clone(), VOTING_KEYSTORE_FILE, password_dir)
|
||||
}
|
||||
|
||||
/// Attempts to read the keystore in `self.dir` and decrypt the keypair using a password file
|
||||
/// in `password_dir`.
|
||||
///
|
||||
/// The password file that is used will be based upon the pubkey value in the keystore.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If there is a file-system error, a password is missing or the password is incorrect.
|
||||
pub fn withdrawal_keypair<P: AsRef<Path>>(&self, password_dir: P) -> Result<Keypair, Error> {
|
||||
unlock_keypair(&self.dir.clone(), WITHDRAWAL_KEYSTORE_FILE, password_dir)
|
||||
}
|
||||
|
||||
/// Indicates if there is a file containing an eth1 deposit transaction. This can be used to
|
||||
/// check if a deposit transaction has been created.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// It's possible to submit an Eth1 deposit without creating this file, so use caution when
|
||||
/// relying upon this value.
|
||||
pub fn eth1_deposit_tx_hash_exists(&self) -> bool {
|
||||
self.dir.join(ETH1_DEPOSIT_TX_HASH_FILE).exists()
|
||||
}
|
||||
|
||||
/// Saves the `tx_hash` to a file in `self.dir`. Artificially requires `mut self` to prevent concurrent
|
||||
/// calls.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If there is a file-system error, or if there is already a transaction hash stored in
|
||||
/// `self.dir`.
|
||||
pub fn save_eth1_deposit_tx_hash(&mut self, tx_hash: &str) -> Result<(), Error> {
|
||||
let path = self.dir.join(ETH1_DEPOSIT_TX_HASH_FILE);
|
||||
|
||||
if path.exists() {
|
||||
return Err(Error::Eth1TxHashExists(path));
|
||||
}
|
||||
|
||||
write(path, tx_hash.as_bytes()).map_err(Error::UnableToWriteEth1TxHash)
|
||||
}
|
||||
|
||||
/// Attempts to read files in `self.dir` and return an `Eth1DepositData` that can be used for
|
||||
/// submitting an Eth1 deposit.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If there is a file-system error, not all required files exist or the files are
|
||||
/// inconsistent.
|
||||
pub fn eth1_deposit_data(&self) -> Result<Option<Eth1DepositData>, Error> {
|
||||
// Read and parse `ETH1_DEPOSIT_DATA_FILE`.
|
||||
let path = self.dir.join(ETH1_DEPOSIT_DATA_FILE);
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let deposit_data_rlp = read(path).map_err(Error::UnableToReadDepositData)?;
|
||||
|
||||
// Read and parse `ETH1_DEPOSIT_AMOUNT_FILE`.
|
||||
let path = self.dir.join(ETH1_DEPOSIT_AMOUNT_FILE);
|
||||
if !path.exists() {
|
||||
return Err(Error::DepositAmountDoesNotExist(path));
|
||||
}
|
||||
let deposit_amount: u64 =
|
||||
String::from_utf8(read(path).map_err(Error::UnableToReadDepositAmount)?)
|
||||
.map_err(Error::DepositAmountIsNotUtf8)?
|
||||
.parse()
|
||||
.map_err(Error::UnableToParseDepositAmount)?;
|
||||
|
||||
let (deposit_data, root) = decode_eth1_tx_data(&deposit_data_rlp, deposit_amount)
|
||||
.map_err(Error::UnableToParseDepositData)?;
|
||||
|
||||
// This acts as a sanity check to ensure that the amount from `ETH1_DEPOSIT_AMOUNT_FILE`
|
||||
// matches the value that `ETH1_DEPOSIT_DATA_FILE` was created with.
|
||||
if deposit_data.tree_hash_root() != root {
|
||||
return Err(Error::Eth1DepositRootMismatch);
|
||||
}
|
||||
|
||||
Ok(Some(Eth1DepositData {
|
||||
rlp: deposit_data_rlp,
|
||||
deposit_data,
|
||||
root,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ValidatorDir {
|
||||
fn drop(&mut self) {
|
||||
let lockfile = self.dir.clone().join(LOCK_FILE);
|
||||
if let Err(e) = remove_file(&lockfile) {
|
||||
eprintln!(
|
||||
"Unable to remove validator lockfile {:?}: {:?}",
|
||||
lockfile, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to load and decrypt a keystore.
|
||||
fn unlock_keypair<P: AsRef<Path>>(
|
||||
keystore_dir: &PathBuf,
|
||||
filename: &str,
|
||||
password_dir: P,
|
||||
) -> Result<Keypair, Error> {
|
||||
let keystore = Keystore::from_json_reader(
|
||||
&mut OpenOptions::new()
|
||||
.read(true)
|
||||
.create(false)
|
||||
.open(keystore_dir.clone().join(filename))
|
||||
.map_err(Error::UnableToOpenKeystore)?,
|
||||
)
|
||||
.map_err(Error::UnableToReadKeystore)?;
|
||||
|
||||
let password_path = password_dir
|
||||
.as_ref()
|
||||
.join(format!("0x{}", keystore.pubkey()));
|
||||
let password: PlainText = read(&password_path)
|
||||
.map_err(|_| Error::UnableToReadPassword(password_path.into()))?
|
||||
.into();
|
||||
|
||||
keystore
|
||||
.decrypt_keypair(password.as_bytes())
|
||||
.map_err(Error::UnableToDecryptKeypair)
|
||||
}
|
||||
273
eth2/utils/validator_dir/tests/tests.rs
Normal file
273
eth2/utils/validator_dir/tests/tests.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
#![cfg(not(debug_assertions))]
|
||||
|
||||
use eth2_keystore::{Keystore, KeystoreBuilder, PlainText};
|
||||
use std::fs::{self, File};
|
||||
use std::path::Path;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
use types::{test_utils::generate_deterministic_keypair, EthSpec, Keypair, MainnetEthSpec};
|
||||
use validator_dir::{
|
||||
Builder, ValidatorDir, ETH1_DEPOSIT_TX_HASH_FILE, VOTING_KEYSTORE_FILE,
|
||||
WITHDRAWAL_KEYSTORE_FILE,
|
||||
};
|
||||
|
||||
/// A very weak password with which to encrypt the keystores.
|
||||
pub const INSECURE_PASSWORD: &[u8] = &[30; 32];
|
||||
|
||||
/// Helper struct for configuring tests.
|
||||
struct BuildConfig {
|
||||
random_voting_keystore: bool,
|
||||
random_withdrawal_keystore: bool,
|
||||
deposit_amount: Option<u64>,
|
||||
store_withdrawal_keystore: bool,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
random_voting_keystore: true,
|
||||
random_withdrawal_keystore: true,
|
||||
deposit_amount: None,
|
||||
store_withdrawal_keystore: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that a keystore exists and can be decrypted with a password in password_dir
|
||||
fn check_keystore<P: AsRef<Path>>(path: P, password_dir: P) -> Keypair {
|
||||
let mut file = File::open(path).unwrap();
|
||||
let keystore = Keystore::from_json_reader(&mut file).unwrap();
|
||||
let pubkey = keystore.pubkey();
|
||||
let password_path = password_dir.as_ref().join(format!("0x{}", pubkey));
|
||||
let password = fs::read(password_path).unwrap();
|
||||
keystore.decrypt_keypair(&password).unwrap()
|
||||
}
|
||||
|
||||
/// Creates a keystore using `generate_deterministic_keypair`.
|
||||
pub fn generate_deterministic_keystore(i: usize) -> Result<(Keystore, PlainText), String> {
|
||||
let keypair = generate_deterministic_keypair(i);
|
||||
|
||||
let keystore = KeystoreBuilder::new(&keypair, INSECURE_PASSWORD, "".into())
|
||||
.map_err(|e| format!("Unable to create keystore builder: {:?}", e))?
|
||||
.build()
|
||||
.map_err(|e| format!("Unable to build keystore: {:?}", e))?;
|
||||
|
||||
Ok((keystore, INSECURE_PASSWORD.to_vec().into()))
|
||||
}
|
||||
|
||||
/// A testing harness for generating validator directories.
|
||||
struct Harness {
|
||||
validators_dir: TempDir,
|
||||
password_dir: TempDir,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
/// Create a new harness using temporary directories.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
validators_dir: tempdir().unwrap(),
|
||||
password_dir: tempdir().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `ValidatorDir` from the `config`, then assert that the `ValidatorDir` was generated
|
||||
/// correctly with respect to the `config`.
|
||||
pub fn create_and_test(&self, config: &BuildConfig) -> ValidatorDir {
|
||||
let spec = MainnetEthSpec::default_spec();
|
||||
|
||||
/*
|
||||
* Build the `ValidatorDir`.
|
||||
*/
|
||||
|
||||
let builder = Builder::new(
|
||||
self.validators_dir.path().into(),
|
||||
self.password_dir.path().into(),
|
||||
)
|
||||
.store_withdrawal_keystore(config.store_withdrawal_keystore);
|
||||
|
||||
let builder = if config.random_voting_keystore {
|
||||
builder
|
||||
} else {
|
||||
let (keystore, password) = generate_deterministic_keystore(0).unwrap();
|
||||
builder.voting_keystore(keystore, password.as_bytes())
|
||||
};
|
||||
|
||||
let builder = if config.random_withdrawal_keystore {
|
||||
builder
|
||||
} else {
|
||||
let (keystore, password) = generate_deterministic_keystore(1).unwrap();
|
||||
builder.withdrawal_keystore(keystore, password.as_bytes())
|
||||
};
|
||||
|
||||
let builder = if let Some(amount) = config.deposit_amount {
|
||||
builder.create_eth1_tx_data(amount, &spec)
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
|
||||
let mut validator = builder.build().unwrap();
|
||||
|
||||
/*
|
||||
* Assert that the dir is consistent with the config.
|
||||
*/
|
||||
|
||||
let withdrawal_keystore_path = validator.dir().join(WITHDRAWAL_KEYSTORE_FILE);
|
||||
let password_dir = self.password_dir.path().into();
|
||||
|
||||
// Ensure the voting keypair exists and can be decrypted.
|
||||
let voting_keypair =
|
||||
check_keystore(&validator.dir().join(VOTING_KEYSTORE_FILE), &password_dir);
|
||||
|
||||
if !config.random_voting_keystore {
|
||||
assert_eq!(voting_keypair, generate_deterministic_keypair(0))
|
||||
}
|
||||
|
||||
// Use OR here instead of AND so we *always* check for the withdrawal keystores if random
|
||||
// keystores were generated.
|
||||
if config.random_withdrawal_keystore || config.store_withdrawal_keystore {
|
||||
// Ensure the withdrawal keypair exists and can be decrypted.
|
||||
let withdrawal_keypair = check_keystore(&withdrawal_keystore_path, &password_dir);
|
||||
|
||||
if !config.random_withdrawal_keystore {
|
||||
assert_eq!(withdrawal_keypair, generate_deterministic_keypair(1))
|
||||
}
|
||||
|
||||
// The withdrawal keys should be distinct from the voting keypairs.
|
||||
assert_ne!(withdrawal_keypair, voting_keypair);
|
||||
}
|
||||
|
||||
if !config.store_withdrawal_keystore && !config.random_withdrawal_keystore {
|
||||
assert!(!withdrawal_keystore_path.exists())
|
||||
}
|
||||
|
||||
if let Some(amount) = config.deposit_amount {
|
||||
// Check that the deposit data can be decoded.
|
||||
let data = validator.eth1_deposit_data().unwrap().unwrap();
|
||||
|
||||
// Ensure the amount is consistent.
|
||||
assert_eq!(data.deposit_data.amount, amount);
|
||||
} else {
|
||||
// If there was no deposit then we should return `Ok(None)`.
|
||||
assert!(validator.eth1_deposit_data().unwrap().is_none());
|
||||
}
|
||||
|
||||
let tx_hash_path = validator.dir().join(ETH1_DEPOSIT_TX_HASH_FILE);
|
||||
|
||||
// The eth1 deposit file should not exist, yet.
|
||||
assert!(!tx_hash_path.exists());
|
||||
|
||||
let tx = "junk data";
|
||||
|
||||
// Save a tx hash.
|
||||
validator.save_eth1_deposit_tx_hash(tx).unwrap();
|
||||
|
||||
// Ensure the saved tx hash is correct.
|
||||
assert_eq!(fs::read(tx_hash_path).unwrap(), tx.as_bytes().to_vec());
|
||||
|
||||
// Saving a second tx hash should fail.
|
||||
validator.save_eth1_deposit_tx_hash(tx).unwrap_err();
|
||||
|
||||
validator
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrency() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let val_dir = harness.create_and_test(&BuildConfig::default());
|
||||
let path = val_dir.dir().clone();
|
||||
|
||||
// Should not re-open whilst opened after build.
|
||||
ValidatorDir::open(&path).unwrap_err();
|
||||
|
||||
drop(val_dir);
|
||||
|
||||
// Should re-open after drop.
|
||||
let val_dir = ValidatorDir::open(&path).unwrap();
|
||||
|
||||
// Should not re-open when opened via ValidatorDir.
|
||||
ValidatorDir::open(&path).unwrap_err();
|
||||
|
||||
drop(val_dir);
|
||||
|
||||
// Should re-open again.
|
||||
ValidatorDir::open(&path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_voting_keystore() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let config = BuildConfig {
|
||||
random_voting_keystore: false,
|
||||
..BuildConfig::default()
|
||||
};
|
||||
|
||||
harness.create_and_test(&config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_withdrawal_keystore_without_saving() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let config = BuildConfig {
|
||||
random_withdrawal_keystore: false,
|
||||
store_withdrawal_keystore: false,
|
||||
..BuildConfig::default()
|
||||
};
|
||||
|
||||
harness.create_and_test(&config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_withdrawal_keystore_with_saving() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let config = BuildConfig {
|
||||
random_withdrawal_keystore: false,
|
||||
store_withdrawal_keystore: true,
|
||||
..BuildConfig::default()
|
||||
};
|
||||
|
||||
harness.create_and_test(&config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_keystores_deterministic_without_saving() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let config = BuildConfig {
|
||||
random_voting_keystore: false,
|
||||
random_withdrawal_keystore: false,
|
||||
store_withdrawal_keystore: false,
|
||||
..BuildConfig::default()
|
||||
};
|
||||
|
||||
harness.create_and_test(&config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_keystores_deterministic_with_saving() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let config = BuildConfig {
|
||||
random_voting_keystore: false,
|
||||
random_withdrawal_keystore: false,
|
||||
store_withdrawal_keystore: true,
|
||||
..BuildConfig::default()
|
||||
};
|
||||
|
||||
harness.create_and_test(&config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eth1_data() {
|
||||
let harness = Harness::new();
|
||||
|
||||
let config = BuildConfig {
|
||||
deposit_amount: Some(123456),
|
||||
..BuildConfig::default()
|
||||
};
|
||||
|
||||
harness.create_and_test(&config);
|
||||
}
|
||||
Reference in New Issue
Block a user