Introduce validator definition file for VC (#1357)
## Issue Addressed
NA
## Proposed Changes
- Introduces the `valdiator_definitions.yml` file which serves as an explicit list of validators that should be run by the validator client.
- Removes `--strict` flag, split into `--strict-lockfiles` and `--disable-auto-discover`
- Adds a "Validator Management" page to the book.
- Adds the `common/account_utils` crate which contains some logic that was starting to duplicate across the codebase.
The new docs for this feature are the best description of it (apart from the code, I guess): 9cb87e93ce/book/src/validator-management.md
## API Changes
This change should be transparent for *most* existing users. If the `valdiator_definitions.yml` doesn't exist then it will be automatically generated using a method that will detect all the validators in their `validators_dir`.
Users will have issues if they are:
1. Using `--strict`.
1. Have keystores in their `~/.lighthouse/validators` directory that weren't being detected by the current keystore discovery method.
For users with (1), the VC will refuse to start because the `--strict` flag has been removed. They will be forced to review `--help` and choose an equivalent flag.
For users with (2), this seems fairly unlikely and since we're only in testnets there's no *real* value on the line here. I'm happy to take the risk, it would be a different case for mainnet.
## Additional Info
This PR adds functionality we will need for #1347.
## TODO
- [x] Reconsider flags
- [x] Move doc into a more reasonable chapter.
- [x] Check for compile warnings.
This commit is contained in:
parent
393782f632
commit
e26da35cbf
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -4,6 +4,7 @@
|
||||
name = "account_manager"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"account_utils",
|
||||
"bls",
|
||||
"clap",
|
||||
"clap_utils",
|
||||
@ -30,6 +31,18 @@ dependencies = [
|
||||
"web3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "account_utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eth2_keystore",
|
||||
"eth2_wallet",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.13.0"
|
||||
@ -5980,6 +5993,7 @@ dependencies = [
|
||||
name = "validator_client"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"account_utils",
|
||||
"bls",
|
||||
"clap",
|
||||
"clap_utils",
|
||||
@ -5988,6 +6002,7 @@ dependencies = [
|
||||
"environment",
|
||||
"eth2_config",
|
||||
"eth2_interop_keypairs",
|
||||
"eth2_keystore",
|
||||
"eth2_ssz",
|
||||
"eth2_ssz_derive",
|
||||
"exit-future",
|
||||
@ -6002,6 +6017,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"slashing_protection",
|
||||
"slog",
|
||||
"slog-async",
|
||||
|
@ -16,6 +16,7 @@ members = [
|
||||
|
||||
"boot_node",
|
||||
|
||||
"common/account_utils",
|
||||
"common/clap_utils",
|
||||
"common/compare_fields",
|
||||
"common/compare_fields_derive",
|
||||
|
@ -29,3 +29,4 @@ rand = "0.7.2"
|
||||
validator_dir = { path = "../common/validator_dir", features = ["unencrypted_keys"] }
|
||||
tokio = { version = "0.2.21", features = ["full"] }
|
||||
eth2_keystore = { path = "../crypto/eth2_keystore" }
|
||||
account_utils = { path = "../common/account_utils" }
|
||||
|
@ -1,25 +1,7 @@
|
||||
use clap::ArgMatches;
|
||||
use eth2_wallet::PlainText;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// 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 fn random_password() -> PlainText {
|
||||
rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(DEFAULT_PASSWORD_LEN)
|
||||
.collect::<String>()
|
||||
.into_bytes()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> Result<(), String> {
|
||||
let path = path.as_ref();
|
||||
|
||||
@ -37,56 +19,3 @@ pub fn base_wallet_dir(matches: &ArgMatches, arg: &'static str) -> Result<PathBu
|
||||
PathBuf::new().join(".lighthouse").join("wallets"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove any number of newline or carriage returns from the end of a vector of bytes.
|
||||
pub fn strip_off_newlines(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||
let mut strip_off = 0;
|
||||
for (i, byte) in bytes.iter().rev().enumerate() {
|
||||
if *byte == b'\n' || *byte == b'\r' {
|
||||
strip_off = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes.truncate(bytes.len() - strip_off);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::strip_off_newlines;
|
||||
|
||||
#[test]
|
||||
fn test_strip_off() {
|
||||
let expected = "hello world".as_bytes().to_vec();
|
||||
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\n\n\n\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r\r\r\r\r".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r\n\r\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
use crate::{
|
||||
common::{ensure_dir_exists, random_password, strip_off_newlines},
|
||||
SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG,
|
||||
};
|
||||
use crate::{common::ensure_dir_exists, SECRETS_DIR_FLAG, VALIDATOR_DIR_FLAG};
|
||||
use account_utils::{random_password, strip_off_newlines};
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use environment::Environment;
|
||||
use eth2_wallet::PlainText;
|
||||
|
@ -1,7 +1,5 @@
|
||||
use crate::{
|
||||
common::{random_password, strip_off_newlines},
|
||||
BASE_DIR_FLAG,
|
||||
};
|
||||
use crate::BASE_DIR_FLAG;
|
||||
use account_utils::{random_password, strip_off_newlines};
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use eth2_wallet::{
|
||||
bip39::{Language, Mnemonic, MnemonicType},
|
||||
|
@ -10,6 +10,7 @@
|
||||
* [Key Management](./key-managment.md)
|
||||
* [Create a wallet](./wallet-create.md)
|
||||
* [Create a validator](./validator-create.md)
|
||||
* [Validator Management](./validator-management.md)
|
||||
* [Local Testnets](./local-testnets.md)
|
||||
* [API](./api.md)
|
||||
* [HTTP (RESTful JSON)](./http.md)
|
||||
|
187
book/src/validator-management.md
Normal file
187
book/src/validator-management.md
Normal file
@ -0,0 +1,187 @@
|
||||
# Validator Management
|
||||
|
||||
The `lighthouse vc` command starts a *validator client* instance which connects
|
||||
to a beacon node performs the duties of a staked validator.
|
||||
|
||||
This document provides information on how the validator client discovers the
|
||||
validators it will act for and how it should obtain their cryptographic
|
||||
signatures.
|
||||
|
||||
Users that create validators using the `lighthouse account` tool in the
|
||||
standard directories and do not start their `lighthouse vc` with the
|
||||
`--disable-auto-discover` flag should not need to understand the contents of
|
||||
this document. However, users with more complex needs may find this document
|
||||
useful.
|
||||
|
||||
## Introducing the `validator_definitions.yml` file
|
||||
|
||||
The `validator_definitions.yml` file is located in the `validator-dir`, which
|
||||
defaults to `~/.lighthouse/validators`. It is a
|
||||
[YAML](https://en.wikipedia.org/wiki/YAML) encoded file defining exactly which
|
||||
validators the validator client will (and won't) act for.
|
||||
|
||||
### Example
|
||||
|
||||
Here's an example file with two validators:
|
||||
|
||||
```yaml
|
||||
---
|
||||
- enabled: true
|
||||
voting_public_key: "0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007"
|
||||
type: local_keystore
|
||||
voting_keystore_path: /home/paul/.lighthouse/validators/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007/voting-keystore.json
|
||||
voting_keystore_password_path: /home/paul/.lighthouse/secrets/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007
|
||||
- enabled: false
|
||||
voting_public_key: "0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477"
|
||||
type: local_keystore
|
||||
voting_keystore_path: /home/paul/.lighthouse/validators/0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477/voting-keystore.json
|
||||
voting_keystore_password: myStrongpa55word123&$
|
||||
```
|
||||
In this example we can see two validators:
|
||||
|
||||
- A validator identified by the `0x87a5...` public key which is enabled.
|
||||
- Another validator identified by the `0x0xa556...` public key which is **not** enabled.
|
||||
|
||||
### Fields
|
||||
|
||||
Each permitted field of the file is listed below for reference:
|
||||
|
||||
- `enabled`: A `true`/`false` indicating if the validator client should consider this
|
||||
validator "enabled".
|
||||
- `voting_public_key`: A validator public key.
|
||||
- `type`: How the validator signs messages (currently restricted to `local_keystore`).
|
||||
- `voting_keystore_path`: The path to a EIP-2335 keystore.
|
||||
- `voting_keystore_password_path`: The path to the password for the EIP-2335 keystore.
|
||||
- `voting_keystore_password`: The password to the EIP-2335 keystore.
|
||||
|
||||
> **Note**: Either `voting_keystore_password_path` or `voting_keystore_password` *must* be
|
||||
> supplied. If both are supplied, `voting_keystore_password_path` is ignored.
|
||||
|
||||
## Populating the `validator_definitions.yml` file
|
||||
|
||||
When validator client starts and the `validator_definitions.yml` file doesn't
|
||||
exist, a new file will be created. If the `--disable-auto-discover` flag is
|
||||
provided, the new file will be empty and the validator client will not start
|
||||
any validators. If the `--disable-auto-discover` flag is **not** provided, an
|
||||
*automatic validator discovery* routine will start (more on that later). To
|
||||
recap:
|
||||
|
||||
- `lighthouse vc`: validators are automatically discovered.
|
||||
- `lighthouse vc --disable-auto-discover`: validators are **not** automatically discovered.
|
||||
|
||||
### Automatic validator discovery
|
||||
|
||||
When the `--disable-auto-discover` flag is **not** provided, the validator will search the
|
||||
`validator-dir` for validators and add any *new* validators to the
|
||||
`validator_definitions.yml` with `enabled: true`.
|
||||
|
||||
The routine for this search begins in the `validator-dir`, where it obtains a
|
||||
list of all files in that directory and all sub-directories (i.e., recursive
|
||||
directory-tree search). For each file named `voting-keystore.json` it creates a
|
||||
new validator definition by the following process:
|
||||
|
||||
1. Set `enabled` to `true`.
|
||||
1. Set `voting_public_key` to the `pubkey` value from the `voting-keystore.json`.
|
||||
1. Set `type` to `local_keystore`.
|
||||
1. Set `voting_keystore_path` to the full path of the discovered keystore.
|
||||
1. Set `voting_keystore_password_path` to be a file in the `secrets-dir` with a
|
||||
name identical to the `voting_public_key` value.
|
||||
|
||||
#### Discovery Example
|
||||
|
||||
Lets assume the following directory structure:
|
||||
|
||||
```
|
||||
~/.lighthouse/validators
|
||||
├── john
|
||||
│ └── voting-keystore.json
|
||||
├── sally
|
||||
│ ├── one
|
||||
│ │ └── voting-keystore.json
|
||||
│ ├── three
|
||||
│ │ └── my-voting-keystore.json
|
||||
│ └── two
|
||||
│ └── voting-keystore.json
|
||||
└── slashing_protection.sqlite
|
||||
```
|
||||
|
||||
There is no `validator_definitions.yml` file present, so we can run `lighthouse
|
||||
vc` (**without** `--disable-auto-discover`) and it will create the following `validator_definitions.yml`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
- enabled: true
|
||||
voting_public_key: "0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477"
|
||||
type: local_keystore
|
||||
voting_keystore_path: /home/paul/.lighthouse/validators/sally/one/voting-keystore.json
|
||||
voting_keystore_password_path: /home/paul/.lighthouse/secrets/0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477
|
||||
- enabled: true
|
||||
voting_public_key: "0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337"
|
||||
type: local_keystore
|
||||
voting_keystore_path: /home/paul/.lighthouse/validators/sally/two/voting-keystore.json
|
||||
voting_keystore_password_path: /home/paul/.lighthouse/secrets/0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337
|
||||
- enabled: true
|
||||
voting_public_key: "0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007"
|
||||
type: local_keystore
|
||||
voting_keystore_path: /home/paul/.lighthouse/validators/john/voting-keystore.json
|
||||
voting_keystore_password_path: /home/paul/.lighthouse/secrets/0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007
|
||||
```
|
||||
|
||||
All `voting-keystore.json` files have been detected and added to the file.
|
||||
Notably, the `sally/three/my-voting-keystore.json` file was *not* added to the
|
||||
file, since the file name is not exactly `voting-keystore.json`.
|
||||
|
||||
In order for the validator client to decrypt the validators, they will need to
|
||||
ensure their `secrets-dir` is organised as below:
|
||||
|
||||
```
|
||||
~/.lighthouse/secrets
|
||||
├── 0xa5566f9ec3c6e1fdf362634ebec9ef7aceb0e460e5079714808388e5d48f4ae1e12897fed1bea951c17fa389d511e477
|
||||
├── 0xaa440c566fcf34dedf233baf56cf5fb05bb420d9663b4208272545608c27c13d5b08174518c758ecd814f158f2b4a337
|
||||
└── 0x87a580d31d7bc69069b55f5a01995a610dd391a26dc9e36e81057a17211983a79266800ab8531f21f1083d7d84085007
|
||||
```
|
||||
|
||||
|
||||
### Manual configuration
|
||||
|
||||
The automatic validator discovery process works out-of-the-box with validators
|
||||
that are created using the `lighthouse account validator new` command. The
|
||||
details of this process are only interesting to those who are using keystores
|
||||
generated with another tool or have a non-standard requirements.
|
||||
|
||||
If you are one of these users, manually edit the `validator_definitions.yml`
|
||||
file to suit your requirements. If the file is poorly formatted or any one of
|
||||
the validators is unable to be initialized, the validator client will refuse to
|
||||
start.
|
||||
|
||||
## How the `validator_definitions.yml` file is processed
|
||||
|
||||
If a validator client were to start using the [first example
|
||||
`validator_definitions.yml` file](#example) it would print the following log,
|
||||
acknowledging there there are two validators and one is disabled:
|
||||
|
||||
```
|
||||
INFO Initialized validators enabled: 1, disabled: 1
|
||||
```
|
||||
|
||||
The validator client will simply ignore the disabled validator. However, for
|
||||
the active validator, the validator client will:
|
||||
|
||||
1. Load an EIP-2335 keystore from the `voting_keystore_path`.
|
||||
1. If the `voting_keystore_password` field is present, use it as the keystore
|
||||
password. Otherwise, attempt to read the file at
|
||||
`voting_keystore_password_path` and use the contents as the keystore
|
||||
password.
|
||||
1. Use the keystore password to decrypt the keystore and obtain a BLS keypair.
|
||||
1. Verify that the decrypted BLS keypair matches the `voting_public_key`.
|
||||
1. Create a `voting-keystore.json.lock` file adjacent to the
|
||||
`voting_keystore_path`, indicating that the voting keystore is in-use and
|
||||
should not be opened by another process.
|
||||
1. Proceed to act for that validator, creating blocks and attestations if/when required.
|
||||
|
||||
If there is an error during any of these steps (e.g., a file is missing or
|
||||
corrupt) the validator client will log an error and continue to attempt to
|
||||
process other validators.
|
||||
|
||||
When the validator client exits (or the validator is deactivated) it will
|
||||
remove the `voting-keystore.json.lock` to indicate that the keystore is free for use again.
|
15
common/account_utils/Cargo.toml
Normal file
15
common/account_utils/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "account_utils"
|
||||
version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rand = "0.7.2"
|
||||
eth2_wallet = { path = "../../crypto/eth2_wallet" }
|
||||
eth2_keystore = { path = "../../crypto/eth2_keystore" }
|
||||
zeroize = { version = "1.0.0", features = ["zeroize_derive"] }
|
||||
serde = "1.0.110"
|
||||
serde_derive = "1.0.110"
|
151
common/account_utils/src/lib.rs
Normal file
151
common/account_utils/src/lib.rs
Normal file
@ -0,0 +1,151 @@
|
||||
//! Provides functions that are used for key/account management across multiple crates in the
|
||||
//! Lighthouse project.
|
||||
|
||||
use eth2_keystore::Keystore;
|
||||
use eth2_wallet::Wallet;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::{self, File};
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub use eth2_wallet::PlainText;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Returns the "default" path where a wallet should store its password file.
|
||||
pub fn default_wallet_password_path<P: AsRef<Path>>(wallet_name: &str, secrets_dir: P) -> PathBuf {
|
||||
secrets_dir.as_ref().join(format!("{}.pass", wallet_name))
|
||||
}
|
||||
|
||||
/// Returns a password for a wallet, where that password is loaded from the "default" path.
|
||||
pub fn default_wallet_password<P: AsRef<Path>>(
|
||||
wallet: &Wallet,
|
||||
secrets_dir: P,
|
||||
) -> Result<PlainText, io::Error> {
|
||||
let path = default_wallet_password_path(wallet.name(), secrets_dir);
|
||||
fs::read(path).map(|bytes| PlainText::from(strip_off_newlines(bytes)))
|
||||
}
|
||||
|
||||
/// Returns the "default" path where a keystore should store its password file.
|
||||
pub fn default_keystore_password_path<P: AsRef<Path>>(
|
||||
keystore: &Keystore,
|
||||
secrets_dir: P,
|
||||
) -> PathBuf {
|
||||
secrets_dir
|
||||
.as_ref()
|
||||
.join(format!("0x{}", keystore.pubkey()))
|
||||
}
|
||||
|
||||
/// Reads a password file into a Zeroize-ing `PlainText` struct, with new-lines removed.
|
||||
pub fn read_password<P: AsRef<Path>>(path: P) -> Result<PlainText, io::Error> {
|
||||
fs::read(path).map(strip_off_newlines).map(Into::into)
|
||||
}
|
||||
|
||||
/// Creates a file with `600 (-rw-------)` permissions.
|
||||
pub fn create_with_600_perms<P: AsRef<Path>>(path: P, bytes: &[u8]) -> Result<(), io::Error> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut file = File::create(&path)?;
|
||||
|
||||
let mut perm = file.metadata()?.permissions();
|
||||
|
||||
perm.set_mode(0o600);
|
||||
|
||||
file.set_permissions(perm)?;
|
||||
|
||||
file.write_all(bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN`.
|
||||
pub fn random_password() -> PlainText {
|
||||
rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(DEFAULT_PASSWORD_LEN)
|
||||
.collect::<String>()
|
||||
.into_bytes()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Remove any number of newline or carriage returns from the end of a vector of bytes.
|
||||
pub fn strip_off_newlines(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||
let mut strip_off = 0;
|
||||
for (i, byte) in bytes.iter().rev().enumerate() {
|
||||
if *byte == b'\n' || *byte == b'\r' {
|
||||
strip_off = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
bytes.truncate(bytes.len() - strip_off);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
|
||||
///
|
||||
/// Useful for ensuring that password memory is zeroed-out on drop.
|
||||
#[derive(Clone, Serialize, Deserialize, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
#[serde(transparent)]
|
||||
pub struct ZeroizeString(String);
|
||||
|
||||
impl From<String> for ZeroizeString {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ZeroizeString {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::strip_off_newlines;
|
||||
|
||||
#[test]
|
||||
fn test_strip_off() {
|
||||
let expected = "hello world".as_bytes().to_vec();
|
||||
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\n\n\n\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r\r\r\r\r".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world\r\n\r\n".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
strip_off_newlines("hello world".as_bytes().to_vec()),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
@ -130,7 +130,7 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
|
||||
network
|
||||
.add_validator_client(
|
||||
ValidatorConfig {
|
||||
auto_register: true,
|
||||
disable_auto_discover: false,
|
||||
..ValidatorConfig::default()
|
||||
},
|
||||
i,
|
||||
|
@ -104,7 +104,7 @@ pub fn run_no_eth1_sim(matches: &ArgMatches) -> Result<(), String> {
|
||||
network
|
||||
.add_validator_client(
|
||||
ValidatorConfig {
|
||||
auto_register: true,
|
||||
disable_auto_discover: false,
|
||||
..ValidatorConfig::default()
|
||||
},
|
||||
i,
|
||||
|
@ -24,6 +24,7 @@ types = { path = "../consensus/types" }
|
||||
serde = "1.0.110"
|
||||
serde_derive = "1.0.110"
|
||||
serde_json = "1.0.52"
|
||||
serde_yaml = "0.8.13"
|
||||
slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] }
|
||||
slog-async = "2.5.0"
|
||||
slog-term = "2.5.0"
|
||||
@ -44,3 +45,5 @@ tempdir = "0.3.7"
|
||||
rayon = "1.3.0"
|
||||
validator_dir = { path = "../common/validator_dir" }
|
||||
clap_utils = { path = "../common/clap_utils" }
|
||||
eth2_keystore = { path = "../crypto/eth2_keystore" }
|
||||
account_utils = { path = "../common/account_utils" }
|
||||
|
@ -37,11 +37,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
||||
nodes using the same key. Automatically enabled unless `--strict` is specified",
|
||||
))
|
||||
.arg(
|
||||
Arg::with_name("strict")
|
||||
.long("strict")
|
||||
Arg::with_name("strict-lockfiles")
|
||||
.long("strict-lockfiles")
|
||||
.help(
|
||||
"If present, require that validator keypairs are unlocked and that auto-register \
|
||||
is explicit before new validators are allowed to be used."
|
||||
"If present, do not load validators that have are guarded by a lockfile. Note: for \
|
||||
Eth2 mainnet, this flag will likely be removed and its behaviour will become default."
|
||||
)
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("disable-auto-discover")
|
||||
.long("disable-auto-discover")
|
||||
.help(
|
||||
"If present, do not attempt to discover new validators in the validators-dir. Validators \
|
||||
will need to be manually added to the validator_definitions.yml file."
|
||||
)
|
||||
)
|
||||
.arg(
|
||||
|
@ -23,10 +23,10 @@ pub struct Config {
|
||||
/// If true, the validator client will still poll for duties and produce blocks even if the
|
||||
/// beacon node is not synced at startup.
|
||||
pub allow_unsynced_beacon_node: bool,
|
||||
/// If true, we will be strict about concurrency and validator registration.
|
||||
pub strict: bool,
|
||||
/// If true, register new validator keys with the slashing protection database.
|
||||
pub auto_register: bool,
|
||||
/// If true, refuse to unlock a keypair that is guarded by a lockfile.
|
||||
pub strict_lockfiles: bool,
|
||||
/// If true, don't scan the validators dir for new keystores.
|
||||
pub disable_auto_discover: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -43,8 +43,8 @@ impl Default for Config {
|
||||
secrets_dir,
|
||||
http_server: DEFAULT_HTTP_SERVER.to_string(),
|
||||
allow_unsynced_beacon_node: false,
|
||||
auto_register: false,
|
||||
strict: false,
|
||||
strict_lockfiles: false,
|
||||
disable_auto_discover: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,13 +73,8 @@ impl Config {
|
||||
}
|
||||
|
||||
config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced");
|
||||
config.auto_register = cli_args.is_present("auto-register");
|
||||
config.strict = cli_args.is_present("strict");
|
||||
|
||||
if !config.strict {
|
||||
// Do not require an explicit `--auto-register` if `--strict` is disabled.
|
||||
config.auto_register = true
|
||||
}
|
||||
config.strict_lockfiles = cli_args.is_present("strict-lockfiles");
|
||||
config.disable_auto_discover = cli_args.is_present("disable-auto-discover");
|
||||
|
||||
if let Some(secrets_dir) = parse_optional(cli_args, "secrets-dir")? {
|
||||
config.secrets_dir = secrets_dir;
|
||||
|
425
validator_client/src/initialized_validators.rs
Normal file
425
validator_client/src/initialized_validators.rs
Normal file
@ -0,0 +1,425 @@
|
||||
//! Provides management of "initialized" validators.
|
||||
//!
|
||||
//! A validator is "initialized" if it is ready for signing blocks, attestations, etc in this
|
||||
//! validator client.
|
||||
//!
|
||||
//! The `InitializedValidators` struct in this file serves as the source-of-truth of which
|
||||
//! validators are managed by this validator client.
|
||||
|
||||
use crate::validator_definitions::{
|
||||
self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME,
|
||||
};
|
||||
use account_utils::{read_password, ZeroizeString};
|
||||
use eth2_keystore::Keystore;
|
||||
use slog::{error, info, warn, Logger};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{self, BufRead, Stdin};
|
||||
use std::path::PathBuf;
|
||||
use types::{Keypair, PublicKey};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Refused to open a validator with an existing lockfile since that validator may be in-use by
|
||||
/// another process.
|
||||
LockfileExists(PathBuf),
|
||||
/// There was a filesystem error when creating the lockfile.
|
||||
UnableToCreateLockfile(io::Error),
|
||||
/// The voting public key in the definition did not match the one in the keystore.
|
||||
VotingPublicKeyMismatch {
|
||||
definition: Box<PublicKey>,
|
||||
keystore: Box<PublicKey>,
|
||||
},
|
||||
/// There was a filesystem error when opening the keystore.
|
||||
UnableToOpenVotingKeystore(io::Error),
|
||||
/// The keystore path is not as expected. It should be a file, not `..` or something obscure
|
||||
/// like that.
|
||||
BadVotingKeystorePath(PathBuf),
|
||||
/// The keystore could not be parsed, it is likely bad JSON.
|
||||
UnableToParseVotingKeystore(eth2_keystore::Error),
|
||||
/// The keystore could not be decrypted. The password might be wrong.
|
||||
UnableToDecryptKeystore(eth2_keystore::Error),
|
||||
/// There was a filesystem error when reading the keystore password from disk.
|
||||
UnableToReadVotingKeystorePassword(io::Error),
|
||||
/// There was an error updating the on-disk validator definitions file.
|
||||
UnableToSaveDefinitions(validator_definitions::Error),
|
||||
/// It is not legal to try and initialize a disabled validator definition.
|
||||
UnableToInitializeDisabledValidator,
|
||||
/// It is not legal to try and initialize a disabled validator definition.
|
||||
PasswordUnknown(PathBuf),
|
||||
/// There was no line when reading from stdin.
|
||||
NoStdinLine,
|
||||
/// There was an error reading from stdin.
|
||||
UnableToReadFromStdin(io::Error),
|
||||
}
|
||||
|
||||
/// A method used by a validator to sign messages.
|
||||
///
|
||||
/// Presently there is only a single variant, however we expect more variants to arise (e.g.,
|
||||
/// remote signing).
|
||||
pub enum SigningMethod {
|
||||
/// A validator that is defined by an EIP-2335 keystore on the local filesystem.
|
||||
LocalKeystore {
|
||||
voting_keystore_path: PathBuf,
|
||||
voting_keystore_lockfile_path: PathBuf,
|
||||
voting_keystore: Keystore,
|
||||
voting_keypair: Keypair,
|
||||
},
|
||||
}
|
||||
|
||||
/// A validator that is ready to sign messages.
|
||||
pub struct InitializedValidator {
|
||||
signing_method: SigningMethod,
|
||||
}
|
||||
|
||||
impl InitializedValidator {
|
||||
/// Instantiate `self` from a `ValidatorDefinition`.
|
||||
///
|
||||
/// If `stdin.is_some()` any missing passwords will result in a prompt requesting input on
|
||||
/// stdin (prompts published to stderr).
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// If the validator is unable to be initialized for whatever reason.
|
||||
pub fn from_definition(
|
||||
def: ValidatorDefinition,
|
||||
strict_lockfiles: bool,
|
||||
stdin: Option<&Stdin>,
|
||||
log: &Logger,
|
||||
) -> Result<Self, Error> {
|
||||
if !def.enabled {
|
||||
return Err(Error::UnableToInitializeDisabledValidator);
|
||||
}
|
||||
|
||||
match def.signing_definition {
|
||||
// Load the keystore, password, decrypt the keypair and create a lockfile for a
|
||||
// EIP-2335 keystore on the local filesystem.
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_password_path,
|
||||
voting_keystore_password,
|
||||
} => {
|
||||
let keystore_file =
|
||||
File::open(&voting_keystore_path).map_err(Error::UnableToOpenVotingKeystore)?;
|
||||
let voting_keystore = Keystore::from_json_reader(keystore_file)
|
||||
.map_err(Error::UnableToParseVotingKeystore)?;
|
||||
|
||||
let voting_keypair = match (voting_keystore_password_path, voting_keystore_password)
|
||||
{
|
||||
// If the password is supplied, use it and ignore the path (if supplied).
|
||||
(_, Some(password)) => voting_keystore
|
||||
.decrypt_keypair(password.as_ref())
|
||||
.map_err(Error::UnableToDecryptKeystore)?,
|
||||
// If only the path is supplied, use the path.
|
||||
(Some(path), None) => {
|
||||
let password = read_password(path)
|
||||
.map_err(Error::UnableToReadVotingKeystorePassword)?;
|
||||
|
||||
voting_keystore
|
||||
.decrypt_keypair(password.as_bytes())
|
||||
.map_err(Error::UnableToDecryptKeystore)?
|
||||
}
|
||||
// If there is no password available, maybe prompt for a password.
|
||||
(None, None) => {
|
||||
if let Some(stdin) = stdin {
|
||||
unlock_keystore_via_stdin_password(
|
||||
stdin,
|
||||
&voting_keystore,
|
||||
&voting_keystore_path,
|
||||
)?
|
||||
} else {
|
||||
return Err(Error::PasswordUnknown(voting_keystore_path));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if voting_keypair.pk != def.voting_public_key {
|
||||
return Err(Error::VotingPublicKeyMismatch {
|
||||
definition: Box::new(def.voting_public_key),
|
||||
keystore: Box::new(voting_keypair.pk),
|
||||
});
|
||||
}
|
||||
|
||||
// Append a `.lock` suffix to the voting keystore.
|
||||
let voting_keystore_lockfile_path = voting_keystore_path
|
||||
.file_name()
|
||||
.ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone()))
|
||||
.and_then(|os_str| {
|
||||
os_str.to_str().ok_or_else(|| {
|
||||
Error::BadVotingKeystorePath(voting_keystore_path.clone())
|
||||
})
|
||||
})
|
||||
.map(|filename| {
|
||||
voting_keystore_path
|
||||
.clone()
|
||||
.with_file_name(format!("{}.lock", filename))
|
||||
})?;
|
||||
|
||||
if voting_keystore_lockfile_path.exists() {
|
||||
if strict_lockfiles {
|
||||
return Err(Error::LockfileExists(voting_keystore_lockfile_path));
|
||||
} else {
|
||||
// If **not** respecting lockfiles, just raise a warning if the voting
|
||||
// keypair cannot be unlocked.
|
||||
warn!(
|
||||
log,
|
||||
"Ignoring validator lockfile";
|
||||
"file" => format!("{:?}", voting_keystore_lockfile_path)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Create a new lockfile.
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&voting_keystore_lockfile_path)
|
||||
.map_err(Error::UnableToCreateLockfile)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
signing_method: SigningMethod::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_lockfile_path,
|
||||
voting_keystore,
|
||||
voting_keypair,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the voting public key for this validator.
|
||||
pub fn voting_public_key(&self) -> &PublicKey {
|
||||
match &self.signing_method {
|
||||
SigningMethod::LocalKeystore { voting_keypair, .. } => &voting_keypair.pk,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the voting keypair for this validator.
|
||||
pub fn voting_keypair(&self) -> &Keypair {
|
||||
match &self.signing_method {
|
||||
SigningMethod::LocalKeystore { voting_keypair, .. } => voting_keypair,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom drop implementation to allow for `LocalKeystore` to remove lockfiles.
|
||||
impl Drop for InitializedValidator {
|
||||
fn drop(&mut self) {
|
||||
match &self.signing_method {
|
||||
SigningMethod::LocalKeystore {
|
||||
voting_keystore_lockfile_path,
|
||||
..
|
||||
} => {
|
||||
if voting_keystore_lockfile_path.exists() {
|
||||
if let Err(e) = fs::remove_file(&voting_keystore_lockfile_path) {
|
||||
eprintln!(
|
||||
"Failed to remove {:?}: {:?}",
|
||||
voting_keystore_lockfile_path, e
|
||||
)
|
||||
}
|
||||
} else {
|
||||
eprintln!("Lockfile missing: {:?}", voting_keystore_lockfile_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to unlock `keystore` at `keystore_path` by prompting the user via `stdin`.
|
||||
fn unlock_keystore_via_stdin_password(
|
||||
stdin: &Stdin,
|
||||
keystore: &Keystore,
|
||||
keystore_path: &PathBuf,
|
||||
) -> Result<Keypair, Error> {
|
||||
eprintln!("");
|
||||
eprintln!(
|
||||
"The {} file does not contain either of the following fields for {:?}:",
|
||||
CONFIG_FILENAME, keystore_path
|
||||
);
|
||||
eprintln!("");
|
||||
eprintln!(" - voting_keystore_password");
|
||||
eprintln!(" - voting_keystore_password_path");
|
||||
eprintln!("");
|
||||
eprintln!(
|
||||
"You may exit and update {} or enter a password. \
|
||||
If you choose to enter a password now then this prompt \
|
||||
will be raised next time the validator is started.",
|
||||
CONFIG_FILENAME
|
||||
);
|
||||
eprintln!("");
|
||||
eprintln!("Enter password (or press Ctrl+c to exit):");
|
||||
|
||||
loop {
|
||||
let password = stdin
|
||||
.lock()
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or_else(|| Error::NoStdinLine)?
|
||||
.map_err(Error::UnableToReadFromStdin)
|
||||
.map(ZeroizeString::from)?;
|
||||
|
||||
eprintln!("");
|
||||
|
||||
match keystore.decrypt_keypair(password.as_ref()) {
|
||||
Ok(keystore) => break Ok(keystore),
|
||||
Err(eth2_keystore::Error::InvalidPassword) => {
|
||||
eprintln!("Invalid password, try again (or press Ctrl+c to exit):");
|
||||
}
|
||||
Err(e) => return Err(Error::UnableToDecryptKeystore(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of `InitializedValidator` objects which is initialized from a list of
|
||||
/// `ValidatorDefinition`. The `ValidatorDefinition` file is maintained as `self` is modified.
|
||||
///
|
||||
/// Forms the fundamental list of validators that are managed by this validator client instance.
|
||||
pub struct InitializedValidators {
|
||||
/// If `true`, no validator will be opened if a lockfile exists. If `false`, a warning will be
|
||||
/// raised for an existing lockfile, but it will ultimately be ignored.
|
||||
strict_lockfiles: bool,
|
||||
/// A list of validator definitions which can be stored on-disk.
|
||||
definitions: ValidatorDefinitions,
|
||||
/// The directory that the `self.definitions` will be saved into.
|
||||
validators_dir: PathBuf,
|
||||
/// The canonical set of validators.
|
||||
validators: HashMap<PublicKey, InitializedValidator>,
|
||||
/// For logging via `slog`.
|
||||
log: Logger,
|
||||
}
|
||||
|
||||
impl InitializedValidators {
|
||||
/// Instantiates `Self`, initializing all validators in `definitions`.
|
||||
pub fn from_definitions(
|
||||
definitions: ValidatorDefinitions,
|
||||
validators_dir: PathBuf,
|
||||
strict_lockfiles: bool,
|
||||
log: Logger,
|
||||
) -> Result<Self, Error> {
|
||||
let mut this = Self {
|
||||
strict_lockfiles,
|
||||
validators_dir,
|
||||
definitions,
|
||||
validators: HashMap::default(),
|
||||
log,
|
||||
};
|
||||
this.update_validators()?;
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// The count of enabled validators contained in `self`.
|
||||
pub fn num_enabled(&self) -> usize {
|
||||
self.validators.len()
|
||||
}
|
||||
|
||||
/// The total count of enabled and disabled validators contained in `self`.
|
||||
pub fn num_total(&self) -> usize {
|
||||
self.definitions.as_slice().len()
|
||||
}
|
||||
|
||||
/// Iterate through all **enabled** voting public keys in `self`.
|
||||
pub fn iter_voting_pubkeys(&self) -> impl Iterator<Item = &PublicKey> {
|
||||
self.validators.iter().map(|(pubkey, _)| pubkey)
|
||||
}
|
||||
|
||||
/// Returns the voting `Keypair` for a given voting `PublicKey`, if that validator is known to
|
||||
/// `self` **and** the validator is enabled.
|
||||
pub fn voting_keypair(&self, voting_public_key: &PublicKey) -> Option<&Keypair> {
|
||||
self.validators
|
||||
.get(voting_public_key)
|
||||
.map(|v| v.voting_keypair())
|
||||
}
|
||||
|
||||
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// Enabling or disabling a validator will cause `self.definitions` to be updated and saved to
|
||||
/// disk. A newly enabled validator will be added to `self.validators`, whilst a newly disabled
|
||||
/// validator will be removed from `self.validators`.
|
||||
///
|
||||
/// Saves the `ValidatorDefinitions` to file, even if no definitions were changed.
|
||||
pub fn set_validator_status(
|
||||
&mut self,
|
||||
voting_public_key: &PublicKey,
|
||||
enabled: bool,
|
||||
) -> Result<(), Error> {
|
||||
self.definitions
|
||||
.as_mut_slice()
|
||||
.iter_mut()
|
||||
.find(|def| def.voting_public_key == *voting_public_key)
|
||||
.map(|def| def.enabled = enabled);
|
||||
|
||||
self.update_validators()?;
|
||||
|
||||
self.definitions
|
||||
.save(&self.validators_dir)
|
||||
.map_err(Error::UnableToSaveDefinitions)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scans `self.definitions` and attempts to initialize and validators which are not already
|
||||
/// initialized.
|
||||
///
|
||||
/// The function exits early with an error if any enabled validator is unable to be
|
||||
/// initialized.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// A validator is considered "already known" and skipped if the public key is already known.
|
||||
/// I.e., if there are two different definitions with the same public key then the second will
|
||||
/// be ignored.
|
||||
fn update_validators(&mut self) -> Result<(), Error> {
|
||||
let stdin = io::stdin();
|
||||
|
||||
for def in self.definitions.as_slice() {
|
||||
if def.enabled {
|
||||
match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore { .. } => {
|
||||
if self.validators.contains_key(&def.voting_public_key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match InitializedValidator::from_definition(
|
||||
def.clone(),
|
||||
self.strict_lockfiles,
|
||||
Some(&stdin),
|
||||
&self.log,
|
||||
) {
|
||||
Ok(init) => {
|
||||
self.validators
|
||||
.insert(init.voting_public_key().clone(), init);
|
||||
info!(
|
||||
self.log,
|
||||
"Enabled validator";
|
||||
"voting_pubkey" => format!("{:?}", def.voting_public_key)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
self.log,
|
||||
"Failed to initialize validator";
|
||||
"error" => format!("{:?}", e),
|
||||
"validator" => format!("{:?}", def.voting_public_key)
|
||||
);
|
||||
|
||||
// Exit on an invalid validator.
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.validators.remove(&def.voting_public_key);
|
||||
info!(
|
||||
self.log,
|
||||
"Disabled validator";
|
||||
"voting_pubkey" => format!("{:?}", def.voting_public_key)
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -4,8 +4,10 @@ mod cli;
|
||||
mod config;
|
||||
mod duties_service;
|
||||
mod fork_service;
|
||||
mod initialized_validators;
|
||||
mod is_synced;
|
||||
mod notifier;
|
||||
mod validator_definitions;
|
||||
mod validator_store;
|
||||
|
||||
pub use cli::cli_app;
|
||||
@ -14,20 +16,20 @@ pub use config::Config;
|
||||
use attestation_service::{AttestationService, AttestationServiceBuilder};
|
||||
use block_service::{BlockService, BlockServiceBuilder};
|
||||
use clap::ArgMatches;
|
||||
use config::SLASHING_PROTECTION_FILENAME;
|
||||
use duties_service::{DutiesService, DutiesServiceBuilder};
|
||||
use environment::RuntimeContext;
|
||||
use fork_service::{ForkService, ForkServiceBuilder};
|
||||
use futures::channel::mpsc;
|
||||
use initialized_validators::InitializedValidators;
|
||||
use notifier::spawn_notifier;
|
||||
use remote_beacon_node::RemoteBeaconNode;
|
||||
use slog::{error, info, warn, Logger};
|
||||
use slog::{error, info, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use slot_clock::SystemTimeSlotClock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::time::{delay_for, Duration};
|
||||
use types::EthSpec;
|
||||
use validator_dir::Manager as ValidatorManager;
|
||||
use validator_definitions::ValidatorDefinitions;
|
||||
use validator_store::ValidatorStore;
|
||||
|
||||
/// The interval between attempts to contact the beacon node during startup.
|
||||
@ -69,30 +71,36 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
|
||||
"datadir" => format!("{:?}", config.data_dir),
|
||||
);
|
||||
|
||||
if !config.data_dir.join(SLASHING_PROTECTION_FILENAME).exists() && !config.auto_register {
|
||||
warn!(
|
||||
let mut validator_defs = ValidatorDefinitions::open_or_create(&config.data_dir)
|
||||
.map_err(|e| format!("Unable to open or create validator definitions: {:?}", e))?;
|
||||
|
||||
if !config.disable_auto_discover {
|
||||
let new_validators = validator_defs
|
||||
.discover_local_keystores(&config.data_dir, &config.secrets_dir, &log)
|
||||
.map_err(|e| format!("Unable to discover local validator keystores: {:?}", e))?;
|
||||
validator_defs
|
||||
.save(&config.data_dir)
|
||||
.map_err(|e| format!("Unable to update validator definitions: {:?}", e))?;
|
||||
info!(
|
||||
log,
|
||||
"Will not register any validators";
|
||||
"msg" => "strongly consider using --auto-register on the first use",
|
||||
"Completed validator discovery";
|
||||
"new_validators" => new_validators,
|
||||
);
|
||||
}
|
||||
|
||||
let validator_manager = ValidatorManager::open(&config.data_dir)
|
||||
.map_err(|e| format!("unable to read data_dir: {:?}", e))?;
|
||||
|
||||
let validators_result = if config.strict {
|
||||
validator_manager.decrypt_all_validators(config.secrets_dir.clone(), Some(&log))
|
||||
} else {
|
||||
validator_manager.force_decrypt_all_validators(config.secrets_dir.clone(), Some(&log))
|
||||
};
|
||||
|
||||
let validators = validators_result
|
||||
.map_err(|e| format!("unable to decrypt all validator directories: {:?}", e))?;
|
||||
let validators = InitializedValidators::from_definitions(
|
||||
validator_defs,
|
||||
config.data_dir.clone(),
|
||||
config.strict_lockfiles,
|
||||
log.clone(),
|
||||
)
|
||||
.map_err(|e| format!("Unable to initialize validators: {:?}", e))?;
|
||||
|
||||
info!(
|
||||
log,
|
||||
"Decrypted validator keystores";
|
||||
"count" => validators.len(),
|
||||
"Initialized validators";
|
||||
"disabled" => validators.num_total().saturating_sub(validators.num_enabled()),
|
||||
"enabled" => validators.num_enabled(),
|
||||
);
|
||||
|
||||
let beacon_node =
|
||||
@ -194,11 +202,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
|
||||
"voting_validators" => validator_store.num_voting_validators()
|
||||
);
|
||||
|
||||
if config.auto_register {
|
||||
info!(log, "Registering all validators for slashing protection");
|
||||
validator_store.register_all_validators_for_slashing_protection()?;
|
||||
info!(log, "Validator auto-registration complete");
|
||||
}
|
||||
|
||||
let duties_service = DutiesServiceBuilder::new()
|
||||
.slot_clock(slot_clock.clone())
|
||||
|
244
validator_client/src/validator_definitions.rs
Normal file
244
validator_client/src/validator_definitions.rs
Normal file
@ -0,0 +1,244 @@
|
||||
//! Provides a file format for defining validators that should be initialized by this validator.
|
||||
//!
|
||||
//! Serves as the source-of-truth of which validators this validator client should attempt (or not
|
||||
//! attempt) to load //! into the `crate::intialized_validators::InitializedValidators` struct.
|
||||
|
||||
use account_utils::{create_with_600_perms, default_keystore_password_path, ZeroizeString};
|
||||
use eth2_keystore::Keystore;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use serde_yaml;
|
||||
use slog::{error, Logger};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::{Path, PathBuf};
|
||||
use types::PublicKey;
|
||||
use validator_dir::VOTING_KEYSTORE_FILE;
|
||||
|
||||
/// The file name for the serialized `ValidatorDefinitions` struct.
|
||||
pub const CONFIG_FILENAME: &str = "validator_definitions.yml";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// The config file could not be opened.
|
||||
UnableToOpenFile(io::Error),
|
||||
/// The config file could not be parsed as YAML.
|
||||
UnableToParseFile(serde_yaml::Error),
|
||||
/// There was an error whilst performing the recursive keystore search function.
|
||||
UnableToSearchForKeystores(io::Error),
|
||||
/// The config file could not be serialized as YAML.
|
||||
UnableToEncodeFile(serde_yaml::Error),
|
||||
/// The config file could not be written to the filesystem.
|
||||
UnableToWriteFile(io::Error),
|
||||
}
|
||||
|
||||
/// Defines how the validator client should attempt to sign messages for this validator.
|
||||
///
|
||||
/// Presently there is only a single variant, however we expect more variants to arise (e.g.,
|
||||
/// remote signing).
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SigningDefinition {
|
||||
/// A validator that is defined by an EIP-2335 keystore on the local filesystem.
|
||||
#[serde(rename = "local_keystore")]
|
||||
LocalKeystore {
|
||||
voting_keystore_path: PathBuf,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voting_keystore_password_path: Option<PathBuf>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voting_keystore_password: Option<ZeroizeString>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A validator that may be initialized by this validator client.
|
||||
///
|
||||
/// Presently there is only a single variant, however we expect more variants to arise (e.g.,
|
||||
/// remote signing).
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ValidatorDefinition {
|
||||
pub enabled: bool,
|
||||
pub voting_public_key: PublicKey,
|
||||
#[serde(flatten)]
|
||||
pub signing_definition: SigningDefinition,
|
||||
}
|
||||
|
||||
/// A list of `ValidatorDefinition` that serves as a serde-able configuration file which defines a
|
||||
/// list of validators to be initialized by this validator client.
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct ValidatorDefinitions(Vec<ValidatorDefinition>);
|
||||
|
||||
impl ValidatorDefinitions {
|
||||
/// Open an existing file or create a new, empty one if it does not exist.
|
||||
pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
|
||||
let config_path = validators_dir.as_ref().join(CONFIG_FILENAME);
|
||||
if !config_path.exists() {
|
||||
let this = Self::default();
|
||||
this.save(&validators_dir)?;
|
||||
}
|
||||
Self::open(validators_dir)
|
||||
}
|
||||
|
||||
/// Open an existing file, returning an error if the file does not exist.
|
||||
pub fn open<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> {
|
||||
let config_path = validators_dir.as_ref().join(CONFIG_FILENAME);
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create_new(false)
|
||||
.open(&config_path)
|
||||
.map_err(Error::UnableToOpenFile)?;
|
||||
serde_yaml::from_reader(file).map_err(Error::UnableToParseFile)
|
||||
}
|
||||
|
||||
/// Perform a recursive, exhaustive search through `validators_dir` and add any keystores
|
||||
/// matching the `validator_dir::VOTING_KEYSTORE_FILE` file name.
|
||||
///
|
||||
/// Returns the count of *new* keystores that were added to `self` during this search.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// Determines the path for the password file based upon the scheme defined by
|
||||
/// `account_utils::default_keystore_password_path`.
|
||||
///
|
||||
/// If a keystore cannot be parsed the function does not exit early. Instead it logs an `error`
|
||||
/// and continues searching.
|
||||
pub fn discover_local_keystores<P: AsRef<Path>>(
|
||||
&mut self,
|
||||
validators_dir: P,
|
||||
secrets_dir: P,
|
||||
log: &Logger,
|
||||
) -> Result<usize, Error> {
|
||||
let mut keystore_paths = vec![];
|
||||
recursively_find_voting_keystores(validators_dir, &mut keystore_paths)
|
||||
.map_err(Error::UnableToSearchForKeystores)?;
|
||||
|
||||
let known_paths: HashSet<&PathBuf> =
|
||||
HashSet::from_iter(self.0.iter().map(|def| match &def.signing_definition {
|
||||
SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
..
|
||||
} => voting_keystore_path,
|
||||
}));
|
||||
|
||||
let mut new_defs = keystore_paths
|
||||
.into_iter()
|
||||
.filter_map(|voting_keystore_path| {
|
||||
if known_paths.contains(&voting_keystore_path) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let keystore_result = OpenOptions::new()
|
||||
.read(true)
|
||||
.create(false)
|
||||
.open(&voting_keystore_path)
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.and_then(|file| {
|
||||
Keystore::from_json_reader(file).map_err(|e| format!("{:?}", e))
|
||||
});
|
||||
|
||||
let keystore = match keystore_result {
|
||||
Ok(keystore) => keystore,
|
||||
Err(e) => {
|
||||
error!(
|
||||
log,
|
||||
"Unable to read validator keystore";
|
||||
"error" => e,
|
||||
"keystore" => format!("{:?}", voting_keystore_path)
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let voting_keystore_password_path = Some(default_keystore_password_path(
|
||||
&keystore,
|
||||
secrets_dir.as_ref(),
|
||||
))
|
||||
.filter(|path| path.exists());
|
||||
|
||||
let voting_public_key =
|
||||
match serde_yaml::from_str(&format!("0x{}", keystore.pubkey())) {
|
||||
Ok(pubkey) => pubkey,
|
||||
Err(e) => {
|
||||
error!(
|
||||
log,
|
||||
"Invalid keystore public key";
|
||||
"error" => format!("{:?}", e),
|
||||
"keystore" => format!("{:?}", voting_keystore_path)
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(ValidatorDefinition {
|
||||
enabled: true,
|
||||
voting_public_key,
|
||||
signing_definition: SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_password_path,
|
||||
voting_keystore_password: None,
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_defs_count = new_defs.len();
|
||||
|
||||
self.0.append(&mut new_defs);
|
||||
|
||||
Ok(new_defs_count)
|
||||
}
|
||||
|
||||
/// Encodes `self` as a YAML string it writes it to the `CONFIG_FILENAME` file in the
|
||||
/// `validators_dir` directory.
|
||||
///
|
||||
/// Will create a new file if it does not exist or over-write any existing file.
|
||||
pub fn save<P: AsRef<Path>>(&self, validators_dir: P) -> Result<(), Error> {
|
||||
let config_path = validators_dir.as_ref().join(CONFIG_FILENAME);
|
||||
let bytes = serde_yaml::to_vec(self).map_err(Error::UnableToEncodeFile)?;
|
||||
|
||||
if config_path.exists() {
|
||||
fs::write(config_path, &bytes).map_err(Error::UnableToWriteFile)
|
||||
} else {
|
||||
create_with_600_perms(&config_path, &bytes).map_err(Error::UnableToWriteFile)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a slice of all `ValidatorDefinition` in `self`.
|
||||
pub fn as_slice(&self) -> &[ValidatorDefinition] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
|
||||
/// Returns a mutable slice of all `ValidatorDefinition` in `self`.
|
||||
pub fn as_mut_slice(&mut self) -> &mut [ValidatorDefinition] {
|
||||
self.0.as_mut_slice()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform an exhaustive tree search of `dir`, adding any discovered voting keystore paths to
|
||||
/// `matches`.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns with an error immediately if any filesystem error is raised.
|
||||
pub fn recursively_find_voting_keystores<P: AsRef<Path>>(
|
||||
dir: P,
|
||||
matches: &mut Vec<PathBuf>,
|
||||
) -> Result<(), io::Error> {
|
||||
fs::read_dir(dir)?.try_for_each(|dir_entry| {
|
||||
let dir_entry = dir_entry?;
|
||||
let file_type = dir_entry.file_type()?;
|
||||
if file_type.is_dir() {
|
||||
recursively_find_voting_keystores(dir_entry.path(), matches)?
|
||||
} else if file_type.is_file() {
|
||||
if dir_entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map_or(false, |filename| filename == VOTING_KEYSTORE_FILE)
|
||||
{
|
||||
matches.push(dir_entry.path())
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
use crate::config::SLASHING_PROTECTION_FILENAME;
|
||||
use crate::{config::Config, fork_service::ForkService};
|
||||
use crate::{
|
||||
config::{Config, SLASHING_PROTECTION_FILENAME},
|
||||
fork_service::ForkService,
|
||||
initialized_validators::InitializedValidators,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use slashing_protection::{NotSafe, Safe, SlashingDatabase};
|
||||
use slog::{crit, error, warn, Logger};
|
||||
use slot_clock::SlotClock;
|
||||
use std::collections::HashMap;
|
||||
use std::iter::FromIterator;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use tempdir::TempDir;
|
||||
@ -42,7 +43,7 @@ impl PartialEq for LocalValidator {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ValidatorStore<T, E: EthSpec> {
|
||||
validators: Arc<RwLock<HashMap<PublicKey, LocalValidator>>>,
|
||||
validators: Arc<RwLock<InitializedValidators>>,
|
||||
slashing_protection: SlashingDatabase,
|
||||
genesis_validators_root: Hash256,
|
||||
spec: Arc<ChainSpec>,
|
||||
@ -54,7 +55,7 @@ pub struct ValidatorStore<T, E: EthSpec> {
|
||||
|
||||
impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
pub fn new(
|
||||
validators: Vec<(Keypair, ValidatorDir)>,
|
||||
validators: InitializedValidators,
|
||||
config: &Config,
|
||||
genesis_validators_root: Hash256,
|
||||
spec: ChainSpec,
|
||||
@ -70,18 +71,8 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
)
|
||||
})?;
|
||||
|
||||
let validator_key_values = validators.into_iter().map(|(kp, dir)| {
|
||||
(
|
||||
kp.pk.clone(),
|
||||
LocalValidator {
|
||||
validator_dir: dir,
|
||||
voting_keypair: kp,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
validators: Arc::new(RwLock::new(HashMap::from_iter(validator_key_values))),
|
||||
validators: Arc::new(RwLock::new(validators)),
|
||||
slashing_protection,
|
||||
genesis_validators_root,
|
||||
spec: Arc::new(spec),
|
||||
@ -98,20 +89,20 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
/// such as when relocating validator keys to a new machine.
|
||||
pub fn register_all_validators_for_slashing_protection(&self) -> Result<(), String> {
|
||||
self.slashing_protection
|
||||
.register_validators(self.validators.read().keys())
|
||||
.register_validators(self.validators.read().iter_voting_pubkeys())
|
||||
.map_err(|e| format!("Error while registering validators: {:?}", e))
|
||||
}
|
||||
|
||||
pub fn voting_pubkeys(&self) -> Vec<PublicKey> {
|
||||
self.validators
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(pubkey, _dir)| pubkey.clone())
|
||||
.iter_voting_pubkeys()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn num_voting_validators(&self) -> usize {
|
||||
self.validators.read().len()
|
||||
self.validators.read().num_enabled()
|
||||
}
|
||||
|
||||
fn fork(&self) -> Option<Fork> {
|
||||
@ -128,9 +119,8 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
// TODO: check this against the slot clock to make sure it's not an early reveal?
|
||||
self.validators
|
||||
.read()
|
||||
.get(validator_pubkey)
|
||||
.and_then(|local_validator| {
|
||||
let voting_keypair = &local_validator.voting_keypair;
|
||||
.voting_keypair(validator_pubkey)
|
||||
.and_then(|voting_keypair| {
|
||||
let domain = self.spec.get_domain(
|
||||
epoch,
|
||||
Domain::Randao,
|
||||
@ -179,8 +169,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
// We can safely sign this block.
|
||||
Ok(Safe::Valid) => {
|
||||
let validators = self.validators.read();
|
||||
let validator = validators.get(validator_pubkey)?;
|
||||
let voting_keypair = &validator.voting_keypair;
|
||||
let voting_keypair = validators.voting_keypair(validator_pubkey)?;
|
||||
|
||||
Some(block.sign(
|
||||
&voting_keypair.sk,
|
||||
@ -247,8 +236,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
// We can safely sign this attestation.
|
||||
Ok(Safe::Valid) => {
|
||||
let validators = self.validators.read();
|
||||
let validator = validators.get(validator_pubkey)?;
|
||||
let voting_keypair = &validator.voting_keypair;
|
||||
let voting_keypair = validators.voting_keypair(validator_pubkey)?;
|
||||
|
||||
attestation
|
||||
.sign(
|
||||
@ -309,7 +297,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
selection_proof: SelectionProof,
|
||||
) -> Option<SignedAggregateAndProof<E>> {
|
||||
let validators = self.validators.read();
|
||||
let voting_keypair = &validators.get(validator_pubkey)?.voting_keypair;
|
||||
let voting_keypair = &validators.voting_keypair(validator_pubkey)?;
|
||||
|
||||
Some(SignedAggregateAndProof::from_aggregate(
|
||||
validator_index,
|
||||
@ -330,7 +318,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
|
||||
slot: Slot,
|
||||
) -> Option<SelectionProof> {
|
||||
let validators = self.validators.read();
|
||||
let voting_keypair = &validators.get(validator_pubkey)?.voting_keypair;
|
||||
let voting_keypair = &validators.voting_keypair(validator_pubkey)?;
|
||||
|
||||
Some(SelectionProof::new::<E>(
|
||||
slot,
|
||||
|
Loading…
Reference in New Issue
Block a user